forked from pisilinux-rs/yali-rs
7209d58ea0
- parted "MB" sonekini onalık (10^6) okur, hesaplarımız binary (2^20):
3944MB → sektör 7703125 (hizasız) → "en yakın konum" hatası.
Tüm mkpart konumları artık "{}MiB" (tamsayı MiB = 2048 sektör katı).
- Bölümleme Planı şeridi yalnızca seçili disk için gösteriliyor
(state.selected_disk), tüm diskler aynı anda değil.
- AGENTS.md: parted birim tuzağı notu eklendi.
2375 lines
107 KiB
Rust
2375 lines
107 KiB
Rust
//! Disk bölümleme adımı — Otomatik ve Manuel mod destekli.
|
||
|
||
use eframe::egui;
|
||
use rust_i18n::t;
|
||
use crate::ui::theme;
|
||
use crate::funct::form_label;
|
||
// sysinfo::Disks mount point başına bir kayıt döndürür (partition'lar),
|
||
// ham disk listesi için lsblk kullanıyoruz.
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Mevcut disk bölümlerini oku
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// Verilen diskin (`/dev/sda`, `/dev/nvme0n1` vb.) mevcut bölümlerini
|
||
/// `lsblk` aracılığıyla okur ve `CustomPartition` listesi olarak döndürür.
|
||
///
|
||
/// Hem SATA/virtio tipi (`sda` → `sda1`, `sda2`) hem de NVMe tipi
|
||
/// (`nvme0n1` → `nvme0n1p1`, `nvme0n1p2`) aygıtları desteklenir.
|
||
/// Manuel bölümleme modunda disk seçildiğinde çağrılır; böylece kullanıcı
|
||
/// diskin mevcut bölüm düzenini görebilir.
|
||
fn read_existing_partitions(disk_path: &str) -> Vec<crate::installer::CustomPartition> {
|
||
use crate::installer::{CustomPartition, FsType};
|
||
|
||
let dev_name = disk_path.trim_start_matches("/dev/");
|
||
|
||
let out = std::process::Command::new("lsblk")
|
||
.args([
|
||
"-o", "NAME,SIZE,FSTYPE,MOUNTPOINT",
|
||
"-b", "--noheadings", "-l",
|
||
disk_path,
|
||
])
|
||
.output();
|
||
|
||
let mut parts = Vec::new();
|
||
|
||
if let Ok(o) = out {
|
||
if o.status.success() {
|
||
let text = String::from_utf8_lossy(&o.stdout);
|
||
for line in text.lines() {
|
||
let mut tokens = line.split_whitespace();
|
||
|
||
let name = match tokens.next() { Some(n) => n, None => continue };
|
||
|
||
// Diskin kendisini (örn. "sda") atla — yalnızca bölümleri al
|
||
if name == dev_name { continue; }
|
||
|
||
// Bu diske ait olmayan girişleri atla
|
||
// SATA: sda1, sda2 … NVMe: nvme0n1p1, nvme0n1p2 …
|
||
if !name.starts_with(dev_name) { continue; }
|
||
|
||
let size_bytes: u64 = tokens.next().unwrap_or("0").parse().unwrap_or(0);
|
||
let size_mb = size_bytes / (1024 * 1024);
|
||
|
||
let fstype_str = tokens.next().unwrap_or("");
|
||
let fstype = match fstype_str {
|
||
"ext4" => FsType::Ext4,
|
||
"ext3" => FsType::Ext3,
|
||
"btrfs" => FsType::Btrfs,
|
||
"xfs" => FsType::Xfs,
|
||
"vfat" => FsType::Fat32,
|
||
"ntfs" => FsType::Ntfs,
|
||
"swap" => FsType::Swap,
|
||
_ => FsType::Ext4, // bilinmeyen → varsayılan
|
||
};
|
||
|
||
// lsblk mount point alanı boşsa sonraki token gelmez; unwrap_or ile güvenli al
|
||
let mountpoint = tokens.next().unwrap_or("").to_string();
|
||
|
||
parts.push(CustomPartition {
|
||
disk: disk_path.to_string(),
|
||
device: format!("/dev/{}", name),
|
||
orig_device: format!("/dev/{}", name),
|
||
size_mb,
|
||
fstype,
|
||
mountpoint,
|
||
encrypt: false,
|
||
luks_password: String::new(),
|
||
luks_name: String::new(),
|
||
skip_format: true,
|
||
delete: false,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
parts
|
||
}
|
||
|
||
/// `/sys/block` altındaki gerçek blok aygıtlarını (partition değil) listeler.
|
||
/// Aygıt adına (`sda`, `nvme0n1`) bakarak türünü belirler.
|
||
fn detect_drive_type(name: &str) -> String {
|
||
if name.starts_with("nvme") {
|
||
return "NVMe SSD".into();
|
||
}
|
||
if name.starts_with("mmcblk") {
|
||
return "MMC/SD".into();
|
||
}
|
||
// removable ortam (USB flash bellek)
|
||
let removable_path = format!("/sys/block/{}/removable", name);
|
||
if let Ok(val) = std::fs::read_to_string(&removable_path) {
|
||
if val.trim() == "1" {
|
||
return "USB".into();
|
||
}
|
||
}
|
||
// döner disk mi? (rotational=1 → HDD, 0 → SSD)
|
||
let rotational_path = format!("/sys/block/{}/queue/rotational", name);
|
||
if let Ok(val) = std::fs::read_to_string(&rotational_path) {
|
||
if val.trim() == "0" {
|
||
return "SSD".into();
|
||
}
|
||
}
|
||
"HDD".into()
|
||
}
|
||
|
||
/// lsblk'dan gelen çıktı akıllıca ayrıştırılır; başarısız olursa /sys/block fallback devreye girer.
|
||
pub fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
|
||
// Önce -P (key=value pairs) ile dene, boşluklu MODEL/VENDOR güvenli
|
||
let out = std::process::Command::new("lsblk")
|
||
.args(["-d", "-o", "NAME,SIZE,MODEL,VENDOR", "-b", "--noheadings", "-P"])
|
||
.output();
|
||
|
||
let mut disks = Vec::new();
|
||
|
||
if let Ok(o) = out {
|
||
if o.status.success() {
|
||
let text = String::from_utf8_lossy(&o.stdout);
|
||
for line in text.lines() {
|
||
// NAME="sda" SIZE="42949672960" MODEL="VBOX HARDDISK" VENDOR="ATA"
|
||
let mut name_raw = String::new();
|
||
let mut vendor = String::new();
|
||
let mut model = String::new();
|
||
let mut size = 0u64;
|
||
|
||
for pair in line.split_ascii_whitespace() {
|
||
let mut parts = pair.splitn(2, '=');
|
||
let key = parts.next().unwrap_or("");
|
||
let val = parts.next().unwrap_or("").trim_matches('"');
|
||
match key {
|
||
"NAME" => name_raw = val.to_string(),
|
||
"SIZE" => size = val.parse().unwrap_or(0),
|
||
"VENDOR"=> vendor = val.to_string(),
|
||
"MODEL" => model = val.to_string(),
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
if name_raw.is_empty() || size == 0 { continue; }
|
||
|
||
if name_raw.starts_with("loop")
|
||
|| name_raw.starts_with("ram")
|
||
|| name_raw.starts_with("sr")
|
||
|| name_raw.starts_with("zram")
|
||
{ continue; }
|
||
|
||
disks.push(crate::installer::DiskInfo {
|
||
name: format!("/dev/{}", name_raw),
|
||
vendor,
|
||
model,
|
||
drive_type: detect_drive_type(&name_raw),
|
||
size_gb: size / 1_073_741_824,
|
||
size_bytes: size,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// lsblk başarısız olduysa /sys/block fallback
|
||
if disks.is_empty() {
|
||
if let Ok(rd) = std::fs::read_dir("/sys/block") {
|
||
for entry in rd.flatten() {
|
||
let name = entry.file_name().to_string_lossy().to_string();
|
||
|
||
if name.starts_with("loop")
|
||
|| name.starts_with("ram")
|
||
|| name.starts_with("sr")
|
||
|| name.starts_with("zram")
|
||
{ continue; }
|
||
|
||
let size_path = format!("/sys/block/{}/size", name);
|
||
let size_sectors: u64 = std::fs::read_to_string(&size_path)
|
||
.unwrap_or_default().trim().parse().unwrap_or(0);
|
||
let size_bytes = size_sectors * 512;
|
||
if size_bytes == 0 { continue; }
|
||
|
||
let vendor_path = format!("/sys/block/{}/device/vendor", name);
|
||
let vendor = std::fs::read_to_string(&vendor_path)
|
||
.unwrap_or_default().trim().to_string();
|
||
|
||
let model_path = format!("/sys/block/{}/device/model", name);
|
||
let model = std::fs::read_to_string(&model_path)
|
||
.unwrap_or_default().trim().to_string();
|
||
|
||
disks.push(crate::installer::DiskInfo {
|
||
name: format!("/dev/{}", name),
|
||
vendor,
|
||
model,
|
||
drive_type: detect_drive_type(&name),
|
||
size_gb: size_bytes / 1_073_741_824,
|
||
size_bytes,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
disks
|
||
}
|
||
|
||
use crate::installer::{
|
||
assign_partition_numbers, CustomPartition, DiskInfo, FsType, GlobalState, InstallerStep,
|
||
PartitionPlan, PartitionTableType, VolumeGroup,
|
||
};
|
||
|
||
const MIN_DISK_GB: u64 = 10;
|
||
const EFI_MB: u64 = 300;
|
||
const SWAP_MB_MIN: u64 = 512;
|
||
const SWAP_MB_MAX: u64 = 8 * 1024;
|
||
|
||
const COMMON_MOUNTPOINTS: &[&str] = &["/", "/boot", "/boot/efi", "/home", "/var", "/tmp", "swap"];
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Manuel bölümleme için yerel durum
|
||
// ─────────────────────────────────────────────
|
||
|
||
#[derive(Default)]
|
||
struct ManualState {
|
||
/// "Bölüm Ekle" diyaloğu açık mı?
|
||
adding: bool,
|
||
/// "Bölüm Düzenle" diyaloğu açık mı?
|
||
editing: bool,
|
||
/// Silinmiş (ataması kaldırılmış) bir bölümün ÜZERİNE yeni
|
||
/// tanım oluşturma modu. editing=true ile birlikte çalışır:
|
||
/// kaydet, mevcut satırın üzerine yazar.
|
||
create_over: bool,
|
||
/// Düzenlenecek bölüm indeksi
|
||
edit_idx: Option<usize>,
|
||
/// Seçili satır indeksi
|
||
selected_idx: Option<usize>,
|
||
/// Diyalog: seçili dosya sistemi
|
||
add_fstype: usize, // FsType listesindeki indeks
|
||
/// Diyalog: boyut alanı (MB, metin girişi)
|
||
add_size_str: String,
|
||
/// Diyalog: bağlama noktası
|
||
add_mountpoint: String,
|
||
/// Diyalog: LUKS şifrele
|
||
add_encrypt: bool,
|
||
/// Diyalog: LUKS parola
|
||
add_luks_password: String,
|
||
/// Silinmek istenen bölüm indeksi (onay isteği)
|
||
delete_idx: Option<usize>,
|
||
/// Genel hata mesajı
|
||
error: Option<String>,
|
||
/// Biçimlendirme atlansın mı?
|
||
add_skip_format: bool,
|
||
/// Yeni VG adı (LVM)
|
||
add_vg_name: String,
|
||
|
||
// ── LVM Logical Volume Ekleme State'leri ───────────────────
|
||
adding_lv: bool,
|
||
add_lv_name: String,
|
||
add_lv_vg_idx: usize,
|
||
add_lv_size_str: String,
|
||
add_lv_fstype: usize,
|
||
add_lv_mountpoint: String,
|
||
}
|
||
|
||
const FS_OPTIONS: &[(&str, FsType)] = &[
|
||
("ext4", FsType::Ext4),
|
||
("ext3", FsType::Ext3),
|
||
("btrfs", FsType::Btrfs),
|
||
("xfs", FsType::Xfs),
|
||
("FAT32 (EFI)", FsType::Fat32),
|
||
("NTFS", FsType::Ntfs),
|
||
("swap", FsType::Swap),
|
||
("LVM PV", FsType::Lvm),
|
||
];
|
||
|
||
// ─────────────────────────────────────────────
|
||
// PartitionStep
|
||
// ─────────────────────────────────────────────
|
||
|
||
pub struct PartitionStep {
|
||
scanned: bool,
|
||
is_uefi: bool,
|
||
manual: ManualState,
|
||
show_luks_password: bool,
|
||
}
|
||
|
||
impl Default for PartitionStep {
|
||
fn default() -> Self {
|
||
Self {
|
||
scanned: false,
|
||
is_uefi: std::path::Path::new("/sys/firmware/efi").exists(),
|
||
manual: ManualState::default(),
|
||
show_luks_password: false,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// RAM miktarına göre önerilen swap alanı (MB).
|
||
fn recommended_swap_mb() -> u64 {
|
||
use sysinfo::System;
|
||
let mut sys = System::new();
|
||
sys.refresh_memory();
|
||
let ram_mb = sys.total_memory() / 1_048_576;
|
||
let swap = if ram_mb <= 2048 {
|
||
ram_mb
|
||
} else if ram_mb <= 8192 {
|
||
ram_mb / 2
|
||
} else {
|
||
4096
|
||
};
|
||
swap.clamp(SWAP_MB_MIN, SWAP_MB_MAX)
|
||
}
|
||
|
||
/// Seçili disk için otomatik bölümleme planı hesapla.
|
||
fn make_plan(disk: &DiskInfo, is_uefi: bool) -> Option<PartitionPlan> {
|
||
if disk.size_gb < MIN_DISK_GB {
|
||
return None;
|
||
}
|
||
let total_mb = disk.size_bytes / 1_048_576;
|
||
let efi_mb = if is_uefi { EFI_MB } else { 0 };
|
||
let swap_mb = recommended_swap_mb();
|
||
let overhead = efi_mb + swap_mb + 512;
|
||
let root_mb = total_mb.saturating_sub(overhead);
|
||
if root_mb < 6144 { return None; }
|
||
Some(PartitionPlan {
|
||
disk: disk.name.clone(),
|
||
table_type: if is_uefi { PartitionTableType::Gpt } else { PartitionTableType::Mbr },
|
||
efi_mb,
|
||
swap_mb,
|
||
root_mb,
|
||
encrypt_root: false,
|
||
luks_password: String::new(),
|
||
})
|
||
}
|
||
|
||
/// Manuel listedeki bölümlerin geçerliliğini kontrol eder.
|
||
/// Silinmek işaretli bölümler kurulumda var olmayacağı için sayılmaz.
|
||
fn validate_manual(
|
||
parts: &[CustomPartition],
|
||
lvs: &[crate::installer::LogicalVolume],
|
||
is_uefi: bool,
|
||
) -> Option<String> {
|
||
let live = |p: &&CustomPartition| !p.delete;
|
||
let has_root = parts.iter().filter(live).any(|p| p.mountpoint == "/")
|
||
|| lvs.iter().any(|lv| lv.mountpoint == "/");
|
||
let has_efi = parts.iter().filter(live)
|
||
.any(|p| p.mountpoint == "/boot/efi" || p.fstype == FsType::Fat32);
|
||
|
||
if !has_root {
|
||
return Some(t!("mp_no_root").to_string());
|
||
}
|
||
if is_uefi && !has_efi {
|
||
return Some(t!("mp_no_efi").to_string());
|
||
}
|
||
None
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Çoklu disk yardımcıları
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// Verilen diskin toplam boyutunu MB cinsinden döndürür.
|
||
/// Disk listede yoksa 0 döner.
|
||
fn disk_size_mb(state: &GlobalState, disk: &str) -> u64 {
|
||
state.available_disks.iter()
|
||
.find(|d| d.name == disk)
|
||
.map(|d| d.size_bytes / 1_048_576)
|
||
.unwrap_or(0)
|
||
}
|
||
|
||
/// Bir diskteki, hariç tutulacak indeks dışındaki tanımlı bölümlerin
|
||
/// toplam boyutunu (MB) döndürür. size_mb == 0 ("kalan alan") ve
|
||
/// silinmek işaretli tanımlar toplama katkı vermez (boş alan sayılır).
|
||
fn used_mb_on_disk(parts: &[CustomPartition], disk: &str, exclude_idx: Option<usize>) -> u64 {
|
||
parts.iter().enumerate()
|
||
.filter(|(i, p)| &p.disk == disk && !p.delete && p.size_mb > 0 && Some(*i) != exclude_idx)
|
||
.map(|(_, p)| p.size_mb)
|
||
.sum()
|
||
}
|
||
|
||
/// Sabit boyutlu tanımların disk boyutunu aşıp aşmadığını kontrol eder.
|
||
/// "Kalan alan" (size 0) tanımları esnektir; katkı vermez.
|
||
fn manual_capacity_error(parts: &[CustomPartition], disks: &[DiskInfo]) -> Option<String> {
|
||
for d in disks {
|
||
let defs: Vec<&CustomPartition> = parts.iter()
|
||
.filter(|p| p.disk == d.name && !p.delete)
|
||
.collect();
|
||
if defs.is_empty() { continue; }
|
||
let cap = d.size_bytes / 1_048_576;
|
||
let fixed: u64 = defs.iter().map(|p| p.size_mb).sum();
|
||
if fixed > cap {
|
||
return Some(format!(
|
||
"{}: sabit tanımlar ({:.1} GB) disk boyutunu ({:.1} GB) aşıyor",
|
||
d.name, fixed as f64 / 1024.0, cap as f64 / 1024.0
|
||
));
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
impl InstallerStep for PartitionStep {
|
||
fn name(&self) -> String { t!("partition").to_string() }
|
||
|
||
fn on_enter(&mut self, state: &mut GlobalState) {
|
||
if !self.scanned {
|
||
state.available_disks = list_block_devices();
|
||
state.available_disks.retain(|d| d.size_gb >= 4);
|
||
self.scanned = true;
|
||
}
|
||
}
|
||
|
||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||
ui.heading(t!("partition_title"));
|
||
ui.label(t!("partition_description"));
|
||
ui.add_space(10.0);
|
||
|
||
if state.available_disks.is_empty() {
|
||
ui.colored_label(egui::Color32::RED, t!("no_disks_found"));
|
||
if ui.button(t!("rescan_disks")).clicked() {
|
||
self.scanned = false;
|
||
self.on_enter(state);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// ── Mod seçimi ─────────────────────────────────────────
|
||
ui.horizontal(|ui| {
|
||
if state.erase_disk {
|
||
let _ = theme::primary_button(ui, &t!("erase_disk_label"));
|
||
if theme::secondary_button(ui, &t!("manual_partition_label")).clicked() {
|
||
state.erase_disk = false;
|
||
// Manuel moda geçişte TÜM disklerin mevcut bölümlerini
|
||
// otomatik yükle; kullanıcı disk seçmek zorunda olmasın.
|
||
for d in &state.available_disks {
|
||
let fresh = read_existing_partitions(&d.name);
|
||
for f in fresh {
|
||
let known = state.custom_partitions.iter()
|
||
.any(|p| p.device == f.device || p.orig_device == f.orig_device);
|
||
if !known {
|
||
state.custom_partitions.push(f);
|
||
}
|
||
}
|
||
}
|
||
// Yeni bölümler için varsayılan hedef disk: ilk disk
|
||
if state.selected_disk.is_none() {
|
||
state.selected_disk = state.available_disks.first().map(|d| d.name.clone());
|
||
}
|
||
assign_partition_numbers(&mut state.custom_partitions);
|
||
}
|
||
} else {
|
||
if theme::secondary_button(ui, &t!("erase_disk_label")).clicked() {
|
||
state.erase_disk = true;
|
||
}
|
||
let _ = theme::primary_button(ui, &t!("manual_partition_label"));
|
||
}
|
||
});
|
||
ui.add_space(10.0);
|
||
|
||
if state.erase_disk {
|
||
self.show_auto_mode(ui, state);
|
||
} else {
|
||
self.show_manual_mode(ui, state);
|
||
}
|
||
}
|
||
|
||
fn is_complete(&self, state: &GlobalState) -> bool {
|
||
if state.erase_disk {
|
||
state.partition_plan.is_some()
|
||
} else {
|
||
!state.custom_partitions.is_empty()
|
||
&& validate_manual(&state.custom_partitions, &state.logical_volumes, self.is_uefi).is_none()
|
||
&& manual_capacity_error(&state.custom_partitions, &state.available_disks).is_none()
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Otomatik mod
|
||
// ─────────────────────────────────────────────
|
||
|
||
impl PartitionStep {
|
||
fn show_auto_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||
ui.group(|ui| {
|
||
ui.label(t!("select_disk_label"));
|
||
ui.add_space(8.0);
|
||
let disks = state.available_disks.clone();
|
||
let kart_genislik = 180.0;
|
||
let kart_yukseklik = 160.0;
|
||
|
||
egui::Grid::new("disk_grid")
|
||
.min_col_width(kart_genislik + 12.0)
|
||
.spacing([12.0, 12.0])
|
||
.show(ui, |ui| {
|
||
for (i, disk) in disks.iter().enumerate() {
|
||
let too_small = disk.size_gb < MIN_DISK_GB;
|
||
let selected = state.selected_disk.as_deref() == Some(&disk.name);
|
||
|
||
let (rect, response) = ui.allocate_exact_size(
|
||
egui::vec2(kart_genislik, kart_yukseklik),
|
||
egui::Sense::click(),
|
||
);
|
||
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
|
||
|
||
// arka plan
|
||
egui::Frame::none()
|
||
.fill(if selected {
|
||
egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1)
|
||
} else if response.hovered() {
|
||
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 180)
|
||
} else {
|
||
egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1)
|
||
})
|
||
.stroke(egui::Stroke::new(
|
||
if selected { 2.0_f32 } else { 1.0_f32 },
|
||
if selected { crate::ui::theme::c_accent() } else { crate::ui::theme::c_border() },
|
||
))
|
||
.rounding(egui::Rounding::same(8.0))
|
||
.inner_margin(egui::Margin::same(8.0))
|
||
.show(child_ui, |ui| {
|
||
ui.set_min_size(egui::vec2(kart_genislik - 16.0, kart_yukseklik - 16.0));
|
||
ui.add_space(8.0);
|
||
ui.add(
|
||
egui::Image::new(egui::include_image!("../../assets/disk.png"))
|
||
.max_width(64.0)
|
||
.max_height(64.0)
|
||
.rounding(egui::Rounding::same(6.0))
|
||
.tint(if too_small {
|
||
egui::Color32::from_gray(80)
|
||
} else {
|
||
egui::Color32::WHITE
|
||
}),
|
||
);
|
||
ui.add_space(6.0);
|
||
let marka = if disk.vendor.is_empty() {
|
||
&disk.model
|
||
} else {
|
||
&disk.vendor
|
||
};
|
||
ui.label(
|
||
egui::RichText::new(format!("{} GB", disk.size_gb))
|
||
.size(14.0)
|
||
.strong()
|
||
.color(crate::ui::theme::c_text()),
|
||
);
|
||
ui.label(
|
||
egui::RichText::new(marka)
|
||
.size(11.0)
|
||
.color(crate::ui::theme::c_text_dim()),
|
||
);
|
||
if too_small {
|
||
ui.colored_label(
|
||
crate::ui::theme::c_error(),
|
||
t!("disk_too_small"),
|
||
);
|
||
}
|
||
});
|
||
|
||
if response.clicked() && !too_small {
|
||
state.selected_disk = Some(disk.name.clone());
|
||
state.partition_plan = make_plan(disk, self.is_uefi);
|
||
}
|
||
|
||
if (i + 1) % 3 == 0 {
|
||
ui.end_row();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
if let Some(ref plan) = state.partition_plan {
|
||
ui.add_space(12.0);
|
||
ui.separator();
|
||
ui.add_space(8.0);
|
||
ui.label(egui::RichText::new(t!("partition_plan_title")).strong().color(crate::ui::theme::c_text()));
|
||
ui.add_space(6.0);
|
||
partition_bar(ui, plan);
|
||
ui.add_space(8.0);
|
||
|
||
// Bootstrap 5 tarzı çizgili tablo
|
||
let col_widths = [0.40, 0.25, 0.35];
|
||
let header_color = egui::Color32::from_rgb(33, 37, 41);
|
||
let header_text = egui::Color32::WHITE;
|
||
let baslik_yukseklik = 36.0;
|
||
let satir_yukseklik = 34.0;
|
||
let headers = [t!("plan_col_part"), t!("plan_col_fs"), t!("plan_col_size")];
|
||
|
||
struct Satir {
|
||
part: String,
|
||
fs: &'static str,
|
||
size: String,
|
||
}
|
||
let mut satirlar = Vec::new();
|
||
if plan.efi_mb > 0 {
|
||
satirlar.push(Satir {
|
||
part: format!("{}1 (EFI)", plan.disk),
|
||
fs: "FAT32",
|
||
size: format!("{} MB", plan.efi_mb),
|
||
});
|
||
}
|
||
let swap_part = if plan.efi_mb > 0 { 2 } else { 1 };
|
||
satirlar.push(Satir {
|
||
part: format!("{}{} (swap)", plan.disk, swap_part),
|
||
fs: "swap",
|
||
size: format!("{} MB", plan.swap_mb),
|
||
});
|
||
let root_part = swap_part + 1;
|
||
satirlar.push(Satir {
|
||
part: format!("{}{}{}", plan.disk, root_part, t!("plan_root_suffix")),
|
||
fs: "ext4",
|
||
size: format!("{:.1} GB", plan.root_mb as f64 / 1024.0),
|
||
});
|
||
|
||
egui::Frame::none()
|
||
.stroke(egui::Stroke::new(1.0_f32, crate::ui::theme::c_border()))
|
||
.rounding(egui::Rounding::same(6.0))
|
||
.show(ui, |ui| {
|
||
let tam = ui.available_width();
|
||
|
||
ui.horizontal(|ui| {
|
||
ui.set_height(baslik_yukseklik);
|
||
ui.spacing_mut().item_spacing.x = 0.0;
|
||
for (j, h) in headers.iter().enumerate() {
|
||
let w = tam * col_widths[j];
|
||
let (rect, _) = ui.allocate_exact_size(
|
||
egui::vec2(w, baslik_yukseklik),
|
||
egui::Sense::hover(),
|
||
);
|
||
let mut cell_ui = ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
|
||
egui::Frame::none()
|
||
.fill(header_color)
|
||
.inner_margin(egui::Margin::symmetric(10.0, 6.0))
|
||
.show(&mut cell_ui, |ui| {
|
||
ui.vertical_centered(|ui| {
|
||
ui.label(egui::RichText::new(h.as_ref()).strong().color(header_text).size(14.0));
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
for (row_idx, satir) in satirlar.iter().enumerate() {
|
||
let row_bg = if row_idx % 2 == 0 {
|
||
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 40)
|
||
} else {
|
||
egui::Color32::TRANSPARENT
|
||
};
|
||
let hucreler: [&str; 3] = [&satir.part, satir.fs, &satir.size];
|
||
ui.horizontal(|ui| {
|
||
ui.set_height(satir_yukseklik);
|
||
ui.spacing_mut().item_spacing.x = 0.0;
|
||
for (j, deger) in hucreler.iter().enumerate() {
|
||
let w = tam * col_widths[j];
|
||
let (rect, _) = ui.allocate_exact_size(
|
||
egui::vec2(w, satir_yukseklik),
|
||
egui::Sense::hover(),
|
||
);
|
||
let mut cell_ui = ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
|
||
egui::Frame::none()
|
||
.fill(row_bg)
|
||
.inner_margin(egui::Margin::symmetric(10.0, 4.0))
|
||
.show(&mut cell_ui, |ui| {
|
||
ui.vertical_centered(|ui| {
|
||
ui.label(egui::RichText::new(*deger).size(13.0).color(crate::ui::theme::c_text()));
|
||
});
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
ui.add_space(8.0);
|
||
ui.label(format!("{}: {}", t!("partition_table_type"), plan.table_type));
|
||
|
||
// ── LUKS şifreleme ─────────────────────────────────────
|
||
ui.add_space(12.0);
|
||
ui.separator();
|
||
ui.add_space(8.0);
|
||
let plan = state.partition_plan.as_mut().unwrap();
|
||
ui.checkbox(&mut plan.encrypt_root, t!("luks_enable_root"));
|
||
if plan.encrypt_root {
|
||
ui.horizontal(|ui| {
|
||
ui.label(t!("luks_password"));
|
||
if self.show_luks_password {
|
||
ui.text_edit_singleline(&mut plan.luks_password);
|
||
} else {
|
||
ui.add(egui::TextEdit::singleline(&mut plan.luks_password).password(true));
|
||
}
|
||
let eye = if self.show_luks_password { "\u{ECB5}" } else { "\u{ECB7}" };
|
||
if ui.small_button(eye).clicked() {
|
||
self.show_luks_password = !self.show_luks_password;
|
||
}
|
||
});
|
||
if plan.luks_password.len() < 4 && !plan.luks_password.is_empty() {
|
||
ui.colored_label(egui::Color32::YELLOW, t!("luks_password_weak"));
|
||
}
|
||
}
|
||
|
||
ui.add_space(10.0);
|
||
egui::Frame::none()
|
||
.fill(egui::Color32::from_rgba_unmultiplied(200, 60, 40, 25))
|
||
.rounding(5.0)
|
||
.inner_margin(egui::Margin::same(8.0))
|
||
.show(ui, |ui| {
|
||
ui.colored_label(
|
||
egui::Color32::from_rgb(210, 80, 60),
|
||
format!("⚠ {}: {}", t!("erase_warning"), plan.disk),
|
||
);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Manuel mod
|
||
// ─────────────────────────────────────────────
|
||
|
||
impl PartitionStep {
|
||
fn show_manual_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||
let kart_genislik = 180.0;
|
||
let kart_yukseklik = 160.0;
|
||
|
||
// ── Disk seçimi (hedef disk) ────────────────────────────
|
||
ui.group(|ui| {
|
||
ui.label(t!("select_disk_label"));
|
||
ui.add_space(8.0);
|
||
let disks = state.available_disks.clone();
|
||
|
||
egui::Grid::new("manual_disk_grid")
|
||
.min_col_width(kart_genislik + 12.0)
|
||
.spacing([12.0, 12.0])
|
||
.show(ui, |ui| {
|
||
for (i, disk) in disks.iter().enumerate() {
|
||
let selected = state.selected_disk.as_deref() == Some(&disk.name);
|
||
|
||
let (rect, response) = ui.allocate_exact_size(
|
||
egui::vec2(kart_genislik, kart_yukseklik),
|
||
egui::Sense::click(),
|
||
);
|
||
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
|
||
|
||
egui::Frame::none()
|
||
.fill(if selected {
|
||
egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1)
|
||
} else if response.hovered() {
|
||
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 180)
|
||
} else {
|
||
egui::Color32::from_rgba_unmultiplied(237, 237, 224, 1)
|
||
})
|
||
.stroke(egui::Stroke::new(
|
||
if selected { 2.0_f32 } else { 1.0_f32 },
|
||
if selected { crate::ui::theme::c_accent() } else { crate::ui::theme::c_border() },
|
||
))
|
||
.rounding(egui::Rounding::same(8.0))
|
||
.inner_margin(egui::Margin::same(8.0))
|
||
.show(child_ui, |ui| {
|
||
ui.set_min_size(egui::vec2(kart_genislik - 16.0, kart_yukseklik - 16.0));
|
||
ui.add_space(8.0);
|
||
ui.add(
|
||
egui::Image::new(egui::include_image!("../../assets/disk.png"))
|
||
.max_width(64.0)
|
||
.max_height(64.0)
|
||
.rounding(egui::Rounding::same(6.0)),
|
||
);
|
||
ui.add_space(6.0);
|
||
let marka = if disk.vendor.is_empty() {
|
||
&disk.model
|
||
} else {
|
||
&disk.vendor
|
||
};
|
||
|
||
ui.label(
|
||
egui::RichText::new(marka)
|
||
.size(11.0)
|
||
.color(crate::ui::theme::c_text_dim()),
|
||
);
|
||
|
||
ui.label(
|
||
egui::RichText::new(format!("{} GB", disk.size_gb))
|
||
.size(14.0)
|
||
.strong()
|
||
.color(crate::ui::theme::c_text()),
|
||
);
|
||
|
||
});
|
||
|
||
if response.clicked() {
|
||
// Aktif diski değiştir; ancak diğer disklerde yapılmış
|
||
// bölüm tanımlarını KORU. Seçilen diskin mevcut
|
||
// bölümlerini listeye ekle (aynı cihaz zaten tanımlıysa
|
||
// kullanıcının tanımı kazanır).
|
||
state.selected_disk = Some(disk.name.clone());
|
||
let fresh = read_existing_partitions(&disk.name);
|
||
for f in fresh {
|
||
let known = state.custom_partitions.iter()
|
||
.any(|p| p.device == f.device || p.orig_device == f.orig_device);
|
||
if !known {
|
||
state.custom_partitions.push(f);
|
||
}
|
||
}
|
||
self.manual.error = None;
|
||
}
|
||
|
||
if (i + 1) % 3 == 0 {
|
||
ui.end_row();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
ui.add_space(8.0);
|
||
|
||
let disk = match &state.selected_disk {
|
||
Some(d) => d.clone(),
|
||
None => {
|
||
ui.label(t!("mp_select_disk_first"));
|
||
return;
|
||
}
|
||
};
|
||
let _ = &disk; // aktif disk (yeni bölümler buna eklenir)
|
||
|
||
// ── Mevcut bölüm listesi tablosu ───────────────────────
|
||
ui.label(egui::RichText::new(t!("mp_plan_title")).strong().color(crate::ui::theme::c_text()));
|
||
ui.add_space(6.0);
|
||
|
||
let delete_idx: Option<usize> = None;
|
||
|
||
let col_widths = [0.25, 0.12, 0.16, 0.24, 0.23];
|
||
let tam_genislik = ui.available_width();
|
||
let _border_stroke = egui::Stroke::new(1.0_f32, crate::ui::theme::c_border());
|
||
|
||
// ── Veri satırları (diske göre gruplu) ────────────
|
||
// Görünen her bölüm satırını GlobalState'teki gerçek indeksine eşle;
|
||
// seçim/silme/düzenlemede bu global indeks kullanılır.
|
||
// Silinmek işaretli bölümler ayrı satırlar çizmez; İLK işaretlenenin
|
||
// konumunda TEK bir "Boş Alan" satırında birleşirler (GParted tarzı).
|
||
enum TableRow<'a> {
|
||
Part(usize, &'a CustomPartition),
|
||
FreeSpace { disk: String, total_mb: u64, first_idx: usize },
|
||
}
|
||
let mut visible_rows: Vec<TableRow> = Vec::new();
|
||
{
|
||
let mut seen_disks: Vec<&str> = Vec::new();
|
||
for part in &state.custom_partitions {
|
||
if !seen_disks.contains(&part.disk.as_str()) {
|
||
seen_disks.push(&part.disk);
|
||
}
|
||
}
|
||
for d in seen_disks {
|
||
// Boş alan = disk boyutu − (korunan fiziksel + YENİ tanımlar).
|
||
// Silinenler zaten boşalır; yeni tanımlar da bu boşluktan
|
||
// yerleşeceği için gösterimden düşülürler.
|
||
// Toplam baştan hesaplanır ki satır nerede çizilirse çizilsin
|
||
// değer hep nihai kalanı göstersin. Boş alan 0'a düşerse
|
||
// satır hiç çizilmez.
|
||
let used_total: u64 = state.custom_partitions.iter()
|
||
.filter(|p| p.disk == d && !p.delete)
|
||
.map(|p| p.size_mb)
|
||
.sum();
|
||
let free_total_mb = disk_size_mb(state, d).saturating_sub(used_total);
|
||
|
||
let mut fs_first: Option<usize> = None;
|
||
for (gi, part) in state.custom_partitions.iter().enumerate() {
|
||
if part.disk != d { continue; }
|
||
if part.delete {
|
||
if fs_first.is_none() { fs_first = Some(gi); }
|
||
continue; // silinen tanım kendi satırını çizmez
|
||
}
|
||
// İşaret bloğunun bittiği yerde boş-alan satırını bas
|
||
if let Some(first) = fs_first.take() {
|
||
if free_total_mb > 0 {
|
||
visible_rows.push(TableRow::FreeSpace {
|
||
disk: d.to_string(), total_mb: free_total_mb, first_idx: first,
|
||
});
|
||
}
|
||
}
|
||
visible_rows.push(TableRow::Part(gi, part));
|
||
}
|
||
// Grup sonu: işaretlenenler en sonadaysa burada bas
|
||
if let Some(first) = fs_first.take() {
|
||
if free_total_mb > 0 {
|
||
visible_rows.push(TableRow::FreeSpace {
|
||
disk: d.to_string(), total_mb: free_total_mb, first_idx: first,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
let total_rows = visible_rows.len();
|
||
let mut last_disk_header = String::new();
|
||
for (vis_idx, row) in visible_rows.iter().enumerate() {
|
||
let (disk_name, global_idx, part): (&str, usize, Option<&CustomPartition>) = match row {
|
||
TableRow::Part(gi, p) => (p.disk.as_str(), *gi, Some(p)),
|
||
TableRow::FreeSpace { disk, first_idx, .. } => (disk.as_str(), *first_idx, None),
|
||
};
|
||
// Disk grubu başlığı: yeni diske geçildiğinde bir kez çiz
|
||
if last_disk_header != disk_name {
|
||
last_disk_header = disk_name.to_string();
|
||
let disk_total_mb = disk_size_mb(state, disk_name);
|
||
let disk_total_str = if disk_total_mb >= 1024 {
|
||
format!("{:.1} GB", disk_total_mb as f64 / 1024.0)
|
||
} else {
|
||
format!("{} MB", disk_total_mb)
|
||
};
|
||
let is_active_disk = state.selected_disk.as_deref() == Some(disk_name);
|
||
let (hdr_rect, _) = ui.allocate_exact_size(
|
||
egui::vec2(tam_genislik, 26.0),
|
||
egui::Sense::hover(),
|
||
);
|
||
let painter = ui.painter();
|
||
painter.rect_filled(
|
||
hdr_rect,
|
||
egui::Rounding::ZERO,
|
||
crate::ui::theme::c_bg_widget(),
|
||
);
|
||
painter.text(
|
||
egui::pos2(hdr_rect.left() + 10.0, hdr_rect.center().y),
|
||
egui::Align2::LEFT_CENTER,
|
||
if is_active_disk {
|
||
format!("\u{EA6B} {} ({})", disk_name, disk_total_str)
|
||
} else {
|
||
format!("{} ({})", disk_name, disk_total_str)
|
||
},
|
||
egui::FontId::proportional(12.5),
|
||
crate::ui::theme::c_accent(),
|
||
);
|
||
|
||
// Bu diskin kendi sütun başlıkları
|
||
draw_partition_table_header(ui, tam_genislik, col_widths);
|
||
}
|
||
|
||
// ── Satır içeriği: bölüm mü, boş-alan satırı mı? ──
|
||
let (device_text, size_text, fstype_text, mount_text) = match part {
|
||
Some(p) => {
|
||
let size_str = if p.size_mb == 0 {
|
||
// Sembolik boyut ("kalan alan"): kurulumda diskin
|
||
// kalan boşluğunun tamamını alır → kapasiteyi göster.
|
||
let others: u64 = state.custom_partitions.iter()
|
||
.filter(|q| q.disk == p.disk && !q.delete && q.size_mb > 0)
|
||
.map(|q| q.size_mb)
|
||
.sum();
|
||
let rem = disk_size_mb(state, &p.disk).saturating_sub(others);
|
||
if rem >= 1024 {
|
||
format!("{:.1} GB", rem as f64 / 1024.0)
|
||
} else {
|
||
format!("{} MB", rem)
|
||
}
|
||
} else if p.size_mb >= 1024 {
|
||
format!("{:.1} GB", p.size_mb as f64 / 1024.0)
|
||
} else {
|
||
format!("{} MB", p.size_mb)
|
||
};
|
||
let mut fstype_label = p.fstype.label().to_string();
|
||
if p.encrypt {
|
||
fstype_label = format!("🔐 {}", fstype_label);
|
||
}
|
||
(
|
||
p.device.clone(),
|
||
size_str,
|
||
fstype_label,
|
||
p.mountpoint.clone(),
|
||
)
|
||
}
|
||
None => {
|
||
// Boş Alan satırı: silinen tüm tanımların toplamı.
|
||
// TableRow::FreeSpace'ten total_mb'e ulaşmak için row'ı
|
||
// yeniden eşle (disk adı üzerinden değil).
|
||
let total_mb = match row {
|
||
TableRow::FreeSpace { total_mb, .. } => *total_mb,
|
||
_ => 0,
|
||
};
|
||
let fs_str = if total_mb >= 1024 {
|
||
format!("{:.1} GB", total_mb as f64 / 1024.0)
|
||
} else {
|
||
format!("{} MB", total_mb)
|
||
};
|
||
(
|
||
format!("\u{EDFB} {}", t!("mp_free_space")),
|
||
fs_str,
|
||
String::new(),
|
||
String::new(),
|
||
)
|
||
}
|
||
};
|
||
|
||
let is_selected = self.manual.selected_idx == Some(global_idx);
|
||
let is_free_row = part.is_none();
|
||
let row_bg = if is_free_row {
|
||
egui::Color32::from_rgba_unmultiplied(180, 50, 40, 70)
|
||
} else if is_selected {
|
||
egui::Color32::from_rgba_unmultiplied(80, 100, 180, 80)
|
||
} else if vis_idx % 2 == 0 {
|
||
egui::Color32::from_rgba_unmultiplied(60, 60, 80, 60)
|
||
} else {
|
||
egui::Color32::TRANSPARENT
|
||
};
|
||
|
||
// Entire row as a single clickable target (painter-based, no widget nesting)
|
||
let (row_rect, row_response) = ui.allocate_exact_size(
|
||
egui::vec2(tam_genislik, 34.0),
|
||
egui::Sense::click(),
|
||
);
|
||
|
||
if row_response.clicked() {
|
||
self.manual.selected_idx = Some(global_idx);
|
||
}
|
||
row_response.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||
|
||
// Paint row background
|
||
let painter = ui.painter();
|
||
painter.rect_filled(row_rect, egui::Rounding::ZERO, row_bg);
|
||
|
||
// Paint cell texts directly (no child_ui/label widgets that steal clicks)
|
||
let cell_texts = [
|
||
device_text.as_str(),
|
||
size_text.as_str(),
|
||
fstype_text.as_str(),
|
||
mount_text.as_str(),
|
||
];
|
||
let text_color = if is_free_row {
|
||
egui::Color32::from_rgb(255, 140, 130)
|
||
} else {
|
||
crate::ui::theme::c_text()
|
||
};
|
||
let mut x = row_rect.left();
|
||
for (j, text) in cell_texts.iter().enumerate() {
|
||
let cell_w = tam_genislik * col_widths[j];
|
||
let text_rect = egui::Rect::from_min_size(
|
||
egui::pos2(x + 8.0, row_rect.top() + 4.0),
|
||
egui::vec2(cell_w - 16.0, 26.0),
|
||
);
|
||
painter.text(
|
||
text_rect.center(),
|
||
egui::Align2::CENTER_CENTER,
|
||
*text,
|
||
egui::FontId::proportional(13.0),
|
||
text_color,
|
||
);
|
||
x += cell_w;
|
||
}
|
||
|
||
// Format sütunu: ikon karakteri (konumlandırılmış)
|
||
// Boş-alan satırında ikon gösterilmez.
|
||
let icon_cell_w = tam_genislik * col_widths[4];
|
||
if let Some(p) = part {
|
||
let icon_rect = egui::Rect::from_min_size(
|
||
egui::pos2(x + (icon_cell_w - 20.0) / 2.0, row_rect.top() + (34.0 - 20.0) / 2.0),
|
||
egui::vec2(20.0, 20.0),
|
||
);
|
||
if p.skip_format {
|
||
// lock ikonu
|
||
ui.put(icon_rect, egui::Label::new(
|
||
egui::RichText::new("\u{EECD}")
|
||
.color(egui::Color32::from_rgb(180, 60, 40))
|
||
.size(18.0)
|
||
));
|
||
} else {
|
||
// check ikonu
|
||
ui.put(icon_rect, egui::Label::new(
|
||
egui::RichText::new("\u{EB7B}")
|
||
.color(egui::Color32::from_rgb(255, 251, 0))
|
||
.size(18.0)
|
||
));
|
||
};
|
||
}
|
||
|
||
if vis_idx < total_rows - 1 {
|
||
ui.separator();
|
||
}
|
||
}
|
||
|
||
// ── Bölümleme Planı (manuel) — disk başına ESKİ/YENİ karşılaştırma ──
|
||
if !state.custom_partitions.is_empty() {
|
||
ui.add_space(12.0);
|
||
ui.separator();
|
||
ui.add_space(8.0);
|
||
ui.label(egui::RichText::new(t!("partition_plan_title")).strong().color(crate::ui::theme::c_text()));
|
||
ui.add_space(6.0);
|
||
|
||
// Yalnızca seçili diskin ESKİ/YENİ şeridi gösterilir
|
||
let active_disk = state.selected_disk.clone().unwrap_or_default();
|
||
if !active_disk.is_empty()
|
||
&& state.custom_partitions.iter().any(|p| p.disk == active_disk)
|
||
{
|
||
manual_dual_bars(ui, state, &active_disk);
|
||
}
|
||
}
|
||
|
||
// Silme onayı
|
||
if let Some(idx) = delete_idx {
|
||
self.manual.delete_idx = Some(idx);
|
||
}
|
||
if let Some(idx) = self.manual.delete_idx {
|
||
let dev = state.custom_partitions.get(idx)
|
||
.map(|p| p.device.clone())
|
||
.unwrap_or_default();
|
||
egui::Window::new(t!("mp_delete_confirm_title"))
|
||
.collapsible(false)
|
||
.resizable(false)
|
||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||
.show(ui.ctx(), |ui| {
|
||
ui.label(t!("mp_confirm_delete", dev = dev));
|
||
ui.add_space(8.0);
|
||
ui.horizontal(|ui| {
|
||
if theme::danger_button(ui, &t!("mp_delete_confirm_yes")).clicked() {
|
||
if idx < state.custom_partitions.len() {
|
||
let is_physical = !state.custom_partitions[idx].orig_device.is_empty();
|
||
if is_physical {
|
||
// Eski Yali modeli (OperationDestroyDevice):
|
||
// bölüm kurulumda silinmek üzere İŞARETLENİR;
|
||
// listede görünür kalır, işaret geri alınabilir.
|
||
let p = &mut state.custom_partitions[idx];
|
||
p.delete = !p.delete;
|
||
if p.delete {
|
||
p.mountpoint.clear();
|
||
p.encrypt = false;
|
||
p.luks_password.clear();
|
||
p.luks_name.clear();
|
||
p.skip_format = true;
|
||
}
|
||
} else {
|
||
// Kullanıcının yeni eklediği tanım:
|
||
// listeden tamamen çıkar.
|
||
state.custom_partitions.remove(idx);
|
||
}
|
||
assign_partition_numbers(&mut state.custom_partitions);
|
||
}
|
||
self.manual.delete_idx = None;
|
||
self.manual.selected_idx = None;
|
||
}
|
||
if theme::secondary_button(ui, &t!("cancel")).clicked() {
|
||
self.manual.delete_idx = None;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
ui.add_space(8.0);
|
||
|
||
// ── Butonlar: Yeni Bölüm / Düzenle / Sil ───────────
|
||
ui.horizontal(|ui| {
|
||
let has_selection = self.manual.selected_idx.is_some();
|
||
let selected = self.manual.selected_idx
|
||
.and_then(|i| state.custom_partitions.get(i));
|
||
|
||
// Silinmiş (ataması kaldırılmış) fiziksel bölüm: mountpoint'i
|
||
// boş ama diskte gerçekten var. Bu satır için akış "Oluştur"dur.
|
||
let is_unassigned = selected
|
||
.map(|p| !p.orig_device.is_empty() && p.mountpoint.is_empty() && !p.delete)
|
||
.unwrap_or(false);
|
||
// Kurulumda silinmek üzere işaretlenmiş bölüm
|
||
let is_marked_delete = selected.map(|p| p.delete).unwrap_or(false);
|
||
|
||
// Yeni Bölüm
|
||
if theme::primary_button(ui, &format!("\u{EA13} {}", t!("mp_new_partition"))).clicked() {
|
||
self.manual.adding = true;
|
||
self.manual.editing = false;
|
||
self.manual.create_over = false;
|
||
self.manual.edit_idx = None;
|
||
// Varsayılan öneriler, AKTİF diskteki tanım sayısına göre yapılır
|
||
let active_disk = state.selected_disk.clone().unwrap_or_default();
|
||
let next_num = state.custom_partitions.iter()
|
||
.filter(|p| p.disk == active_disk)
|
||
.count() + 1;
|
||
self.manual.add_mountpoint.clear();
|
||
self.manual.add_encrypt = false;
|
||
self.manual.add_luks_password.clear();
|
||
self.manual.add_mountpoint = match next_num {
|
||
1 if self.is_uefi => "/boot/efi".to_string(),
|
||
1 => "/".to_string(),
|
||
2 if self.is_uefi => "swap".to_string(),
|
||
2 => "swap".to_string(),
|
||
3 => "/".to_string(),
|
||
_ => String::new(),
|
||
};
|
||
// Mountpoint'e göre otomatik boyut ata
|
||
self.manual.add_size_str = match self.manual.add_mountpoint.as_str() {
|
||
"/boot/efi" => "1024".to_string(), // 1GB
|
||
"swap" => "1024".to_string(), // Sabit 1GB
|
||
"/" => "remaining".to_string(), // Geri kalan alan
|
||
_ => String::new(),
|
||
};
|
||
self.manual.add_fstype = match self.manual.add_mountpoint.as_str() {
|
||
"swap" => FS_OPTIONS.iter().position(|(_, t)| *t == FsType::Swap).unwrap_or(0),
|
||
"/boot/efi" => FS_OPTIONS.iter().position(|(_, t)| *t == FsType::Fat32).unwrap_or(0),
|
||
_ => 0,
|
||
};
|
||
// Yeni bölümler varsayılan olarak biçimlendirilir
|
||
self.manual.add_skip_format = false;
|
||
}
|
||
|
||
// Düzenle / (silinmiş satırda) Oluştur — silinmek işaretli
|
||
// satırlarda düzenleme anlamsızdır, buton kilitlenir.
|
||
ui.add_enabled_ui(has_selection && !is_marked_delete, |ui| {
|
||
let edit_btn_label = if is_unassigned {
|
||
format!("\u{EA13} {}", t!("mp_create_here"))
|
||
} else {
|
||
format!("\u{EFE0} {}", t!("mp_edit_partition"))
|
||
};
|
||
if theme::secondary_button(ui, &edit_btn_label).clicked() {
|
||
if let Some(part) = selected {
|
||
if is_unassigned {
|
||
// Oluşturma akışı; ancak kaydedilince MEVCUT
|
||
// satırın üzerine yazılacak (edit_idx ayakta).
|
||
self.manual.create_over = true;
|
||
self.manual.editing = true;
|
||
self.manual.adding = false;
|
||
self.manual.edit_idx = self.manual.selected_idx;
|
||
self.manual.add_size_str.clear();
|
||
self.manual.add_mountpoint.clear();
|
||
self.manual.add_encrypt = false;
|
||
self.manual.add_luks_password.clear();
|
||
self.manual.add_skip_format = false;
|
||
} else {
|
||
self.manual.editing = true;
|
||
self.manual.adding = false;
|
||
self.manual.create_over = false;
|
||
self.manual.edit_idx = self.manual.selected_idx;
|
||
self.manual.add_size_str = if part.size_mb == 0 {
|
||
String::new()
|
||
} else {
|
||
part.size_mb.to_string()
|
||
};
|
||
self.manual.add_mountpoint = part.mountpoint.clone();
|
||
self.manual.add_fstype = FS_OPTIONS.iter()
|
||
.position(|(_, ft)| *ft == part.fstype)
|
||
.unwrap_or(0);
|
||
self.manual.add_encrypt = part.encrypt;
|
||
self.manual.add_luks_password = part.luks_password.clone();
|
||
self.manual.add_skip_format = part.skip_format;
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Sil / Geri Al
|
||
ui.add_enabled_ui(has_selection, |ui| {
|
||
let del_btn_label = if is_marked_delete {
|
||
format!("\u{EB7B} {}", t!("mp_restore"))
|
||
} else {
|
||
format!("\u{EC28} {}", t!("mp_delete"))
|
||
};
|
||
if theme::danger_button(ui, &del_btn_label).clicked() {
|
||
if is_marked_delete {
|
||
// İşareti anında geri al (onay gerekmez).
|
||
// Boş-alan satırı seçiliyken basıldıysa diskteki
|
||
// TÜM silinmek işaretli tanımlar geri alınır.
|
||
if let Some(i) = self.manual.selected_idx {
|
||
if let Some(sel) = state.custom_partitions.get(i) {
|
||
let sel_disk = sel.disk.clone();
|
||
let mut restored = false;
|
||
for p in state.custom_partitions.iter_mut() {
|
||
if p.disk == sel_disk && p.delete {
|
||
p.delete = false;
|
||
restored = true;
|
||
}
|
||
}
|
||
if restored {
|
||
assign_partition_numbers(&mut state.custom_partitions);
|
||
}
|
||
}
|
||
}
|
||
self.manual.selected_idx = None;
|
||
} else {
|
||
self.manual.delete_idx = self.manual.selected_idx;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── Bölüm Ekle / Düzenle diyaloğu ─────────────────
|
||
let dialog_open = self.manual.adding || self.manual.editing;
|
||
if dialog_open {
|
||
let is_edit = self.manual.editing;
|
||
// create_over: silinmiş satırın üzerine oluşturma → sunum
|
||
// açısından "yeni bölüm" akışı gibi davranır (akıllı öneriler,
|
||
// Ekle butonu); kaydedince mevcut satırın üzerine yazar.
|
||
let is_create_like = !is_edit || self.manual.create_over;
|
||
let title = if is_create_like { t!("mp_new_partition") } else { t!("mp_edit_partition") };
|
||
egui::Window::new(title)
|
||
.collapsible(false)
|
||
.resizable(false)
|
||
.default_size(egui::vec2(420.0, 300.0))
|
||
//.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||
.default_pos(ui.ctx().screen_rect().center() - egui::vec2(210.0, 150.0))
|
||
.show(ui.ctx(), |ui| {
|
||
ui.set_min_width(420.0);
|
||
ui.set_max_width(420.0);
|
||
ui.label(t!("mp_fstype"));
|
||
let prev_fs = self.manual.add_fstype;
|
||
let fs_label = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?");
|
||
theme::show_combo_box(ui, "fs_combo", fs_label, ui.available_width(), |ui, w| {
|
||
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
|
||
theme::selectable_value(ui, &mut self.manual.add_fstype, i, *label, w);
|
||
}
|
||
});
|
||
// FS tipi değişince akıllı boyut öner
|
||
if is_create_like && prev_fs != self.manual.add_fstype && self.manual.add_size_str.is_empty() {
|
||
let fs = FS_OPTIONS.get(self.manual.add_fstype).map(|x| &x.1);
|
||
match fs {
|
||
Some(FsType::Fat32) => self.manual.add_size_str = "300".to_string(),
|
||
Some(FsType::Swap) => self.manual.add_size_str = format!("{}", recommended_swap_mb()),
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
ui.add_space(6.0);
|
||
form_label(ui, &t!("mp_size_hint"));
|
||
ui.horizontal(|ui| {
|
||
ui.add_sized(
|
||
[160.0, 32.0], // Genişlik 160.0, Yükseklik 40.0 piksel
|
||
egui::TextEdit::singleline(&mut self.manual.add_size_str)
|
||
.text_color(egui::Color32::from_rgb(230, 230, 230))
|
||
.vertical_align(egui::Align::Center)
|
||
);
|
||
// ui.add(egui::TextEdit::singleline(&mut self.manual.add_size_str)
|
||
// .desired_width(120.0)
|
||
// .text_color(egui::Color32::from_rgb(230, 230, 230)));
|
||
let size_str = self.manual.add_size_str.trim();
|
||
if size_str == "remaining" {
|
||
ui.label(egui::RichText::new(t!("mp_size_remaining"))
|
||
.color(crate::ui::theme::c_success())
|
||
.size(12.0));
|
||
} else {
|
||
let parsed: u64 = size_str.parse().unwrap_or(0);
|
||
if parsed > 0 {
|
||
let gb = parsed as f64 / 1024.0;
|
||
ui.label(format!("{} MB", format_number_thousands(parsed)));
|
||
ui.label(egui::RichText::new(format!("(≈ {:.1} GB)", gb))
|
||
.color(crate::ui::theme::c_text_dim())
|
||
.size(11.0));
|
||
} else {
|
||
ui.label("MB");
|
||
}
|
||
}
|
||
});
|
||
// Slider — kullanılabilir alan kadar
|
||
// Hedef disk: düzenlemede bölümün kendi diski,
|
||
// eklemede aktif (seçili) disk. Boyut hesapları
|
||
// yalnızca bu disk üzerinden yapılır.
|
||
let dialog_disk = if is_edit {
|
||
self.manual.edit_idx
|
||
.and_then(|i| state.custom_partitions.get(i))
|
||
.map(|p| p.disk.clone())
|
||
.unwrap_or_default()
|
||
} else {
|
||
state.selected_disk.clone().unwrap_or_default()
|
||
};
|
||
let disk_total_mb = disk_size_mb(state, &dialog_disk);
|
||
let used_mb: u64 = used_mb_on_disk(&state.custom_partitions, &dialog_disk, self.manual.edit_idx);
|
||
let avail_mb = disk_total_mb.saturating_sub(used_mb).max(1);
|
||
let avail_gb = avail_mb as f64 / 1024.0;
|
||
ui.label(
|
||
egui::RichText::new(format!("{} \u{EA6B} {} (≈ {:.1} GB {})",
|
||
dialog_disk, format_number_thousands(avail_mb), avail_gb, t!("mp_size_available")))
|
||
.size(11.0)
|
||
.color(crate::ui::theme::c_text_dim()),
|
||
);
|
||
let mut size_val = self.manual.add_size_str.trim().parse::<f64>().unwrap_or(0.0);
|
||
ui.add_space(2.0);
|
||
if ui.add(egui::Slider::new(&mut size_val, 0.0..=avail_mb as f64)
|
||
.show_value(false)
|
||
.max_decimals(0)
|
||
).changed() {
|
||
if size_val == 0.0 {
|
||
self.manual.add_size_str = "0".to_string();
|
||
} else {
|
||
self.manual.add_size_str = format!("{}", size_val as u64);
|
||
}
|
||
}
|
||
|
||
ui.add_space(6.0);
|
||
ui.separator();
|
||
|
||
ui.label(t!("mp_mountpoint"));
|
||
ui.horizontal(|ui| {
|
||
let current_mp = if self.manual.add_mountpoint.is_empty() {
|
||
t!("mp_select_hint").to_string()
|
||
} else {
|
||
self.manual.add_mountpoint.clone()
|
||
};
|
||
let prev_mp = self.manual.add_mountpoint.clone();
|
||
theme::show_combo_box(ui, "mp_combo", ¤t_mp, 120.0, |ui, w| {
|
||
for mp in COMMON_MOUNTPOINTS {
|
||
theme::selectable_value(ui, &mut self.manual.add_mountpoint, mp.to_string(), *mp, w);
|
||
}
|
||
});
|
||
// ui.add(egui::TextEdit::singleline(&mut self.manual.add_mountpoint)
|
||
// .desired_width(160.0)
|
||
// .text_color(egui::Color32::from_rgb(230, 230, 230)));
|
||
ui.add_sized(
|
||
[160.0, 32.0], // Genişlik 160.0, Yükseklik 40.0 piksel
|
||
egui::TextEdit::singleline(&mut self.manual.add_mountpoint)
|
||
.text_color(egui::Color32::from_rgb(230, 230, 230))
|
||
.vertical_align(egui::Align::Center)
|
||
);
|
||
|
||
// Mountpoint değiştiğinde boyutu otomatik güncelle (eğer değer boşsa veya varsayılansa)
|
||
if prev_mp != self.manual.add_mountpoint {
|
||
let mp = self.manual.add_mountpoint.trim().to_lowercase();
|
||
let current_size = self.manual.add_size_str.trim();
|
||
let is_empty_or_default = current_size.is_empty()
|
||
|| current_size == "0"
|
||
|| current_size == "remaining";
|
||
|
||
if is_empty_or_default {
|
||
self.manual.add_size_str = match mp.as_str() {
|
||
"/boot/efi" => "1024".to_string(), // 1GB
|
||
"swap" => {
|
||
// Swap için minimum 1GB, varsa RAM boyutu kadar
|
||
if let Ok(mem) = std::fs::read_to_string("/proc/meminfo") {
|
||
if let Some(line) = mem.lines().find(|l| l.starts_with("MemTotal:")) {
|
||
let kb: u64 = line.split_whitespace()
|
||
.nth(1)
|
||
.and_then(|s| s.parse().ok())
|
||
.unwrap_or(4194304); // 4GB varsayılan
|
||
let mb = kb / 1024;
|
||
// Minimum 1GB, RAM boyutu ile karşılaştır
|
||
let swap_size = if mb < 1024 { 1024 } else { mb };
|
||
swap_size.to_string()
|
||
} else {
|
||
"1024".to_string() // 1GB varsayılan
|
||
}
|
||
} else {
|
||
"1024".to_string() // 1GB varsayılan
|
||
}
|
||
},
|
||
"/" => "remaining".to_string(), // Geri kalan alan
|
||
_ => self.manual.add_size_str.clone(), // Diğerleri için mevcut değeri koru
|
||
};
|
||
}
|
||
}
|
||
});
|
||
|
||
// mountpoint "swap" seçildiğinde FS tipini otomatik swap yap
|
||
let mp = self.manual.add_mountpoint.trim().to_lowercase();
|
||
let current_fs = FS_OPTIONS.get(self.manual.add_fstype).map(|x| &x.1);
|
||
let idx = |fs: FsType| FS_OPTIONS.iter().position(|(_, t)| *t == fs).unwrap_or(0);
|
||
if mp == "swap" && current_fs != Some(&FsType::Swap) {
|
||
self.manual.add_fstype = idx(FsType::Swap);
|
||
} else if mp == "/boot/efi" && current_fs != Some(&FsType::Fat32) {
|
||
self.manual.add_fstype = idx(FsType::Fat32);
|
||
}
|
||
|
||
ui.add_space(6.0);
|
||
ui.separator();
|
||
|
||
let fstype = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4);
|
||
let can_encrypt = matches!(fstype, FsType::Ext4 | FsType::Ext3 | FsType::Btrfs | FsType::Xfs);
|
||
|
||
// ui.label(t!("luks_enable_root"));
|
||
// ui.add_enabled_ui(can_encrypt, |ui| {
|
||
// ui.checkbox(&mut self.manual.add_encrypt, "");
|
||
// });
|
||
ui.add_enabled_ui(can_encrypt, |ui| {
|
||
// Checkbox'ı yazısıyla birlikte tek bir bileşen olarak ekliyoruz:
|
||
ui.checkbox(&mut self.manual.add_encrypt,
|
||
egui::RichText::new(t!("luks_enable_root")).color(egui::Color32::from_rgb(128, 128, 128))
|
||
);
|
||
|
||
});
|
||
if !can_encrypt { self.manual.add_encrypt = false; }
|
||
|
||
if self.manual.add_encrypt {
|
||
ui.add_space(6.0);
|
||
ui.label(t!("luks_password"));
|
||
let pw = &mut self.manual.add_luks_password;
|
||
ui.horizontal(|ui| {
|
||
if self.show_luks_password {
|
||
ui.add_sized(
|
||
[200.0, 32.0], // Genişlik 200.0, Yükseklik 32.0 piksel
|
||
egui::TextEdit::singleline(pw)
|
||
);
|
||
} else {
|
||
ui.add_sized(
|
||
[200.0, 32.0], // Genişlik 200.0, Yükseklik 32.0 piksel
|
||
egui::TextEdit::singleline(pw).password(true)
|
||
.text_color(egui::Color32::from_rgb(230, 230, 230))
|
||
.vertical_align(egui::Align::Center)
|
||
);
|
||
let eye = if self.show_luks_password { "\u{ECB5}" } else { "\u{ECB7}" };
|
||
if ui.small_button(eye).clicked() {
|
||
self.show_luks_password = !self.show_luks_password;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
ui.add_space(6.0);
|
||
ui.checkbox(&mut self.manual.add_skip_format,
|
||
egui::RichText::new(t!("mp_skip_format")).color(egui::Color32::from_rgb(128, 128, 128)));
|
||
|
||
ui.add_space(8.0);
|
||
ui.separator();
|
||
ui.horizontal(|ui| {
|
||
let ok_label = if is_create_like { t!("mp_add_btn") } else { t!("mp_save") };
|
||
if theme::primary_button(ui, &ok_label).clicked() {
|
||
// Hedef disk: düzenlemede bölümün diski, eklemede aktif disk
|
||
let target_disk = if is_edit {
|
||
self.manual.edit_idx
|
||
.and_then(|i| state.custom_partitions.get(i))
|
||
.map(|p| p.disk.clone())
|
||
.unwrap_or_else(|| disk.clone())
|
||
} else {
|
||
disk.clone()
|
||
};
|
||
let size_str = self.manual.add_size_str.trim();
|
||
let size_mb: u64 = if size_str == "remaining" {
|
||
// SEMBOLİK kalır (size_mb = 0): gerçek boyut
|
||
// kurulum anında, diskteki silme/oluşturma
|
||
// işlemleri bitince boş alana göre belirlenir.
|
||
// Somut sayıya çevirmek, sonradan başka bölüm
|
||
// boyutları değişirse taşmaya yol açar.
|
||
0
|
||
} else {
|
||
size_str.parse().unwrap_or(0)
|
||
};
|
||
let mp = self.manual.add_mountpoint.trim().to_string();
|
||
let fstype = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4);
|
||
let encrypt = self.manual.add_encrypt;
|
||
let luks_password = if encrypt { self.manual.add_luks_password.clone() } else { String::new() };
|
||
|
||
if mp.is_empty() {
|
||
self.manual.error = Some(t!("mp_error_empty_mountpoint").to_string());
|
||
} else if target_disk.is_empty() {
|
||
self.manual.error = Some(t!("mp_select_disk_first").to_string());
|
||
} else if is_edit {
|
||
if let Some(idx) = self.manual.edit_idx {
|
||
if idx < state.custom_partitions.len() {
|
||
let skip_format = self.manual.add_skip_format;
|
||
state.custom_partitions[idx] = CustomPartition {
|
||
disk: state.custom_partitions[idx].disk.clone(),
|
||
device: state.custom_partitions[idx].device.clone(),
|
||
orig_device: state.custom_partitions[idx].orig_device.clone(),
|
||
size_mb,
|
||
fstype,
|
||
mountpoint: mp,
|
||
encrypt,
|
||
luks_password,
|
||
luks_name: String::new(),
|
||
skip_format,
|
||
delete: false, // düzenleme işareti temizler
|
||
};
|
||
assign_partition_numbers(&mut state.custom_partitions);
|
||
}
|
||
}
|
||
self.manual.editing = false;
|
||
self.manual.create_over = false;
|
||
self.manual.edit_idx = None;
|
||
self.manual.selected_idx = None;
|
||
self.manual.error = None;
|
||
} else {
|
||
// Yeni bölüm aktif diske eklenir; bölüm numarası
|
||
// o diskteki mevcut tanım sayısından türetilir.
|
||
state.custom_partitions.push(CustomPartition {
|
||
disk: target_disk.clone(),
|
||
device: String::new(), // renumber ile atanır
|
||
orig_device: String::new(), // fiziksel karşılığı yok
|
||
size_mb,
|
||
fstype,
|
||
mountpoint: mp,
|
||
encrypt,
|
||
luks_password,
|
||
luks_name: String::new(),
|
||
skip_format: self.manual.add_skip_format,
|
||
delete: false,
|
||
});
|
||
assign_partition_numbers(&mut state.custom_partitions);
|
||
self.manual.add_encrypt = false;
|
||
self.manual.add_luks_password.clear();
|
||
self.manual.error = None;
|
||
self.manual.adding = false;
|
||
}
|
||
}
|
||
if theme::secondary_button(ui, &t!("cancel")).clicked() {
|
||
self.manual.adding = false;
|
||
self.manual.editing = false;
|
||
self.manual.create_over = false;
|
||
self.manual.edit_idx = None;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── LVM Yapılandırması ──────────────────────────────────
|
||
ui.add_space(12.0);
|
||
let has_lvm_pvs = state.custom_partitions.iter().any(|p| p.fstype == FsType::Lvm);
|
||
if has_lvm_pvs {
|
||
ui.label(egui::RichText::new("LVM Volume Groups").strong().size(14.0));
|
||
ui.add_space(4.0);
|
||
egui::Grid::new("lvm_vg_grid")
|
||
.num_columns(4)
|
||
.spacing([12.0, 6.0])
|
||
.striped(true)
|
||
.show(ui, |ui| {
|
||
ui.label("VG Name");
|
||
ui.label("PV Devices");
|
||
ui.label("Size");
|
||
ui.label("");
|
||
ui.end_row();
|
||
|
||
let mut remove_vg: Option<usize> = None;
|
||
for (vi, vg) in state.volume_groups.iter().enumerate() {
|
||
let pv_list = vg.pv_devices.join(", ");
|
||
let size_str = if vg.size_mb >= 1024 {
|
||
format!("{:.1} GB", vg.size_mb as f64 / 1024.0)
|
||
} else {
|
||
format!("{} MB", vg.size_mb)
|
||
};
|
||
ui.label(&vg.name);
|
||
ui.label(&pv_list);
|
||
ui.label(&size_str);
|
||
if ui.small_button("🗑").clicked() {
|
||
remove_vg = Some(vi);
|
||
}
|
||
ui.end_row();
|
||
}
|
||
if let Some(vi) = remove_vg {
|
||
state.volume_groups.remove(vi);
|
||
}
|
||
|
||
// Yeni VG ekleme
|
||
ui.horizontal(|ui| {
|
||
ui.text_edit_singleline(&mut self.manual.add_vg_name);
|
||
if ui.small_button("➕ VG").clicked()
|
||
&& !self.manual.add_vg_name.is_empty()
|
||
{
|
||
let pvs: Vec<String> = state.custom_partitions.iter()
|
||
.filter(|p| p.fstype == FsType::Lvm)
|
||
.map(|p| p.device.clone())
|
||
.collect();
|
||
let size: u64 = state.custom_partitions.iter()
|
||
.filter(|p| p.fstype == FsType::Lvm)
|
||
.map(|p| p.size_mb)
|
||
.sum();
|
||
state.volume_groups.push(VolumeGroup {
|
||
name: self.manual.add_vg_name.clone(),
|
||
pv_devices: pvs,
|
||
size_mb: size,
|
||
});
|
||
self.manual.add_vg_name.clear();
|
||
}
|
||
});
|
||
ui.label("");
|
||
ui.label("");
|
||
ui.end_row();
|
||
});
|
||
|
||
// ── LVM Logical Volumes Tablosu ──────────────────────────
|
||
let has_vgs = !state.volume_groups.is_empty();
|
||
if has_vgs {
|
||
ui.add_space(12.0);
|
||
ui.label(egui::RichText::new(t!("lvm_lv_title")).strong().size(14.0));
|
||
ui.add_space(4.0);
|
||
|
||
let mut remove_lv: Option<usize> = None;
|
||
egui::Grid::new("lvm_lv_grid")
|
||
.num_columns(5)
|
||
.spacing([12.0, 6.0])
|
||
.striped(true)
|
||
.show(ui, |ui| {
|
||
ui.label(egui::RichText::new(t!("lvm_lv_name")).strong().color(crate::ui::theme::c_text()));
|
||
ui.label(egui::RichText::new(t!("lvm_lv_vg")).strong().color(crate::ui::theme::c_text()));
|
||
ui.label(egui::RichText::new(t!("mp_size")).strong().color(crate::ui::theme::c_text()));
|
||
ui.label(egui::RichText::new(t!("mp_mountpoint")).strong().color(crate::ui::theme::c_text()));
|
||
ui.label(egui::RichText::new(t!("mp_action")).strong().color(crate::ui::theme::c_text()));
|
||
ui.end_row();
|
||
|
||
for (li, lv) in state.logical_volumes.iter().enumerate() {
|
||
let size_str = if lv.size_mb == 0 {
|
||
t!("mp_size_remaining").to_string()
|
||
} else {
|
||
format!("{} MB", lv.size_mb)
|
||
};
|
||
ui.label(&lv.name);
|
||
ui.label(&lv.vg_name);
|
||
ui.label(size_str);
|
||
ui.label(&lv.mountpoint);
|
||
if ui.small_button("🗑").clicked() {
|
||
remove_lv = Some(li);
|
||
}
|
||
ui.end_row();
|
||
}
|
||
});
|
||
|
||
if let Some(li) = remove_lv {
|
||
state.logical_volumes.remove(li);
|
||
}
|
||
|
||
ui.add_space(8.0);
|
||
// "Logical Volume Ekle" butonu
|
||
if theme::primary_button(ui, &format!("➕ {}", t!("lvm_lv_add"))).clicked() {
|
||
self.manual.adding_lv = true;
|
||
self.manual.add_lv_name.clear();
|
||
self.manual.add_lv_vg_idx = 0;
|
||
self.manual.add_lv_size_str.clear();
|
||
self.manual.add_lv_fstype = 0;
|
||
self.manual.add_lv_mountpoint.clear();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Logical Volume Ekleme Diyaloğu ────────────────────────
|
||
if self.manual.adding_lv {
|
||
egui::Window::new(t!("lvm_lv_add"))
|
||
.collapsible(false)
|
||
.resizable(false)
|
||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||
.show(ui.ctx(), |ui| {
|
||
egui::Grid::new("add_lv_grid")
|
||
.num_columns(2)
|
||
.spacing([12.0, 8.0])
|
||
.show(ui, |ui| {
|
||
// Hacim Grubu (VG) seçimi
|
||
ui.label(t!("lvm_lv_vg"));
|
||
let current_vg = state.volume_groups.get(self.manual.add_lv_vg_idx)
|
||
.map(|vg| vg.name.as_str())
|
||
.unwrap_or("?");
|
||
theme::show_combo_box(ui, "lv_vg_combo", current_vg, ui.available_width(), |ui, w| {
|
||
for (vi, vg) in state.volume_groups.iter().enumerate() {
|
||
theme::selectable_value(ui, &mut self.manual.add_lv_vg_idx, vi, vg.name.as_str(), w);
|
||
}
|
||
});
|
||
ui.end_row();
|
||
|
||
// LV Adı
|
||
ui.label(t!("lvm_lv_name"));
|
||
ui.text_edit_singleline(&mut self.manual.add_lv_name);
|
||
ui.end_row();
|
||
|
||
// Dosya sistemi
|
||
ui.label(t!("mp_fstype"));
|
||
let lv_fs_label = FS_OPTIONS.get(self.manual.add_lv_fstype).map(|x| x.0).unwrap_or("?");
|
||
theme::show_combo_box(ui, "lv_fs_combo", lv_fs_label, ui.available_width(), |ui, w| {
|
||
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
|
||
theme::selectable_value(ui, &mut self.manual.add_lv_fstype, i, *label, w);
|
||
}
|
||
});
|
||
ui.end_row();
|
||
|
||
// Boyut
|
||
ui.label(t!("mp_size_hint"));
|
||
ui.text_edit_singleline(&mut self.manual.add_lv_size_str);
|
||
ui.end_row();
|
||
|
||
// Bağlama noktası (2 aşamalı: combo + input)
|
||
ui.label(t!("mp_mountpoint"));
|
||
ui.horizontal(|ui| {
|
||
let current_mp = if self.manual.add_lv_mountpoint.is_empty() {
|
||
t!("mp_select_hint").to_string()
|
||
} else {
|
||
self.manual.add_lv_mountpoint.clone()
|
||
};
|
||
theme::show_combo_box(ui, "lv_mp_combo", ¤t_mp, 120.0, |ui, w| {
|
||
for mp in COMMON_MOUNTPOINTS {
|
||
theme::selectable_value(ui, &mut self.manual.add_lv_mountpoint, mp.to_string(), *mp, w);
|
||
}
|
||
});
|
||
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_lv_mountpoint)
|
||
.hint_text(t!("mp_add_dialog_hint"));
|
||
ui.add(mp_field);
|
||
});
|
||
ui.end_row();
|
||
});
|
||
|
||
ui.add_space(8.0);
|
||
ui.horizontal(|ui| {
|
||
if theme::primary_button(ui, &t!("mp_add_btn")).clicked() {
|
||
let size_mb: u64 = self.manual.add_lv_size_str.trim().parse().unwrap_or(0);
|
||
let mp = self.manual.add_lv_mountpoint.trim().to_string();
|
||
let lv_name = self.manual.add_lv_name.trim().to_string();
|
||
let fstype = FS_OPTIONS.get(self.manual.add_lv_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4);
|
||
|
||
if lv_name.is_empty() {
|
||
self.manual.error = Some("LV Adı boş olamaz.".to_string());
|
||
} else if mp.is_empty() {
|
||
self.manual.error = Some(t!("mp_error_empty_mountpoint").to_string());
|
||
} else if let Some(vg) = state.volume_groups.get(self.manual.add_lv_vg_idx) {
|
||
// VG sınır doğrulaması
|
||
let current_sum: u64 = state.logical_volumes.iter()
|
||
.filter(|lv| lv.vg_name == vg.name)
|
||
.map(|lv| lv.size_mb)
|
||
.sum();
|
||
if current_sum + size_mb > vg.size_mb && size_mb > 0 {
|
||
self.manual.error = Some(t!("lvm_lv_error_size").to_string());
|
||
} else {
|
||
state.logical_volumes.push(crate::installer::LogicalVolume {
|
||
name: lv_name,
|
||
vg_name: vg.name.clone(),
|
||
size_mb,
|
||
fstype,
|
||
mountpoint: mp,
|
||
});
|
||
self.manual.error = None;
|
||
self.manual.adding_lv = false;
|
||
}
|
||
} else {
|
||
self.manual.error = Some(t!("lvm_lv_error_vg").to_string());
|
||
}
|
||
}
|
||
if theme::secondary_button(ui, &t!("cancel")).clicked() {
|
||
self.manual.adding_lv = false;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Doğrulama mesajları ─────────────────────────────────
|
||
ui.add_space(6.0);
|
||
if let Some(ref err) = self.manual.error {
|
||
ui.colored_label(egui::Color32::from_rgb(217, 0, 91), err);
|
||
}
|
||
if !state.custom_partitions.is_empty() {
|
||
if let Some(warn) = manual_capacity_error(&state.custom_partitions, &state.available_disks) {
|
||
ui.colored_label(
|
||
egui::Color32::from_rgb(217, 0, 91),
|
||
format!("\u{ECA1} {}", warn),
|
||
);
|
||
} else if let Some(warn) = validate_manual(&state.custom_partitions, &state.logical_volumes, self.is_uefi) {
|
||
ui.colored_label(
|
||
egui::Color32::from_rgb(220, 160, 40),
|
||
format!("\u{ECA1} {}", warn),
|
||
);
|
||
} else {
|
||
ui.colored_label(
|
||
egui::Color32::from_rgb(7, 255, 0),
|
||
format!("\u{EB7B} {}", t!("mp_plan_valid")),
|
||
);
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Yardımcı: renkli bölüm şeridi (manuel)
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// Bölüm tablosunun sütun başlıklarını çizer.
|
||
/// Her disk grubu kendi başlık satırıyla ayrı bir "tablo" görünümü verir.
|
||
fn draw_partition_table_header(ui: &mut egui::Ui, tam_genislik: f32, col_widths: [f32; 5]) {
|
||
let header_color = egui::Color32::from_rgb(33, 37, 41);
|
||
let header_text = egui::Color32::WHITE;
|
||
let headers: [String; 5] = [
|
||
t!("mp_device").to_string(),
|
||
t!("mp_size").to_string(),
|
||
t!("mp_fstype").to_string(),
|
||
t!("mp_mountpoint").to_string(),
|
||
t!("mp_format_status").to_string(),
|
||
];
|
||
ui.horizontal(|ui| {
|
||
ui.set_height(30.0);
|
||
ui.spacing_mut().item_spacing.x = 0.0;
|
||
for (j, h) in headers.into_iter().enumerate() {
|
||
let (rect, _) = ui.allocate_exact_size(
|
||
egui::vec2(tam_genislik * col_widths[j], 30.0),
|
||
egui::Sense::hover(),
|
||
);
|
||
let child_ui = &mut ui.child_ui(rect, egui::Layout::top_down(egui::Align::Center));
|
||
egui::Frame::none()
|
||
.fill(header_color)
|
||
.stroke(egui::Stroke::new(1.0_f32, header_color))
|
||
.inner_margin(egui::Margin::symmetric(8.0, 4.0))
|
||
.show(child_ui, |ui| {
|
||
ui.label(egui::RichText::new(h).strong().color(header_text).size(13.0));
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/// Tek bir düzen şeridi çizer: (etiket, boyut_mb, renk) segmentleri.
|
||
/// Etiketi boş olan segmentler gri "boş alan" olarak gösterilir.
|
||
fn draw_layout_bar(ui: &mut egui::Ui, segments: &[(String, u64, egui::Color32)]) {
|
||
let bar_height = 28.0;
|
||
let (rect, _) = ui.allocate_exact_size(
|
||
egui::vec2(ui.available_width(), bar_height),
|
||
egui::Sense::hover(),
|
||
);
|
||
let painter = ui.painter();
|
||
if segments.is_empty() {
|
||
return;
|
||
}
|
||
|
||
let sized_total: f32 = segments.iter().map(|s| s.1 as f32).sum();
|
||
if sized_total <= 0.0 { return; }
|
||
let min_visible = 0.04;
|
||
|
||
let mut raw: Vec<f32> = segments.iter()
|
||
.map(|s| (s.1 as f32 / sized_total).max(min_visible))
|
||
.collect();
|
||
let sum: f32 = raw.iter().sum();
|
||
for v in raw.iter_mut() { *v /= sum; }
|
||
|
||
let mut x = rect.left();
|
||
let rounding = egui::Rounding::same(4.0);
|
||
for (i, ((label, _, color), frac)) in segments.iter().zip(raw.iter()).enumerate() {
|
||
let w = frac * rect.width();
|
||
let seg_rect = egui::Rect::from_min_size(
|
||
egui::pos2(x, rect.top()),
|
||
egui::vec2(w - 1.0, bar_height),
|
||
);
|
||
let seg_rounding = if i == 0 {
|
||
egui::Rounding { nw: rounding.nw, sw: rounding.sw, ne: 0.0, se: 0.0 }
|
||
} else if i == segments.len() - 1 {
|
||
egui::Rounding { nw: 0.0, sw: 0.0, ne: rounding.ne, se: rounding.se }
|
||
} else {
|
||
egui::Rounding::ZERO
|
||
};
|
||
painter.rect_filled(seg_rect, seg_rounding, *color);
|
||
if !label.is_empty() {
|
||
painter.text(
|
||
seg_rect.center(),
|
||
egui::Align2::CENTER_CENTER,
|
||
label,
|
||
egui::FontId::proportional(12.0),
|
||
egui::Color32::WHITE,
|
||
);
|
||
}
|
||
x += w;
|
||
}
|
||
}
|
||
|
||
/// Bir tanımın şerit etiketi ve rengi.
|
||
fn bar_style(part: &CustomPartition) -> (String, egui::Color32) {
|
||
let label = if part.mountpoint.is_empty() {
|
||
part.fstype.label().to_string()
|
||
} else {
|
||
part.mountpoint.clone()
|
||
};
|
||
let color = if part.fstype == FsType::Fat32 {
|
||
egui::Color32::from_rgb(80, 160, 220)
|
||
} else if part.fstype == FsType::Swap {
|
||
egui::Color32::from_rgb(220, 160, 40)
|
||
} else if part.fstype == FsType::Lvm || part.fstype == FsType::LvmLv {
|
||
egui::Color32::from_rgb(60, 170, 170)
|
||
} else if part.mountpoint == "/" {
|
||
egui::Color32::from_rgb(80, 190, 100)
|
||
} else {
|
||
egui::Color32::from_rgb(160, 130, 200)
|
||
};
|
||
(label, color)
|
||
}
|
||
|
||
const BAR_FREE_COLOR: egui::Color32 = egui::Color32::from_rgb(90, 90, 105);
|
||
|
||
/// Seçili disk için ESKİ (mevcut) ve YENİ (kurulum sonrası) düzen
|
||
/// şeritlerini üst üste çizer — eski Yali/GParted karşılaştırma görünümü.
|
||
fn manual_dual_bars(ui: &mut egui::Ui, state: &GlobalState, disk: &str) {
|
||
let defs: Vec<&CustomPartition> = state.custom_partitions.iter()
|
||
.filter(|p| p.disk == disk)
|
||
.collect();
|
||
if defs.is_empty() { return; }
|
||
|
||
let disk_mb = disk_size_mb(state, disk);
|
||
|
||
// ── Eski düzen: fiziksel bölümler (silinmek işaretliler dâhil —
|
||
// onlar hâlâ diskte duruyorlar) ──
|
||
let mut old_segs: Vec<(String, u64, egui::Color32)> = defs.iter()
|
||
.filter(|p| !p.orig_device.is_empty())
|
||
.map(|p| {
|
||
let (label, color) = bar_style(p);
|
||
(label, p.size_mb.max(1), color)
|
||
})
|
||
.collect();
|
||
|
||
// ── Yeni düzen: silinmeyecekler + yeni tanımlar + kalan boşluk ──
|
||
let mut new_segs: Vec<(String, u64, egui::Color32)> = defs.iter()
|
||
.filter(|p| !p.delete)
|
||
.map(|p| {
|
||
let (label, color) = bar_style(p);
|
||
(label, if p.size_mb == 0 { 0 } else { p.size_mb }, color)
|
||
})
|
||
.collect();
|
||
// Sembolik ("kalan") tanım varsa kalan boşluğun tamamını o alır
|
||
let has_symbolic = defs.iter().any(|p| !p.delete && p.size_mb == 0 && p.orig_device.is_empty());
|
||
let fixed_new: u64 = new_segs.iter().map(|s| s.1).sum();
|
||
let free_mb = disk_mb.saturating_sub(fixed_new);
|
||
|
||
if has_symbolic {
|
||
// Kalan alanı sembolik tanımın segmentine yaz
|
||
for s in new_segs.iter_mut() {
|
||
if s.1 == 0 { s.1 = free_mb; break; }
|
||
}
|
||
} else if free_mb > 0 {
|
||
new_segs.push((String::new(), free_mb, BAR_FREE_COLOR));
|
||
}
|
||
|
||
// Eski barda hiç segment yoksa disk tamamen boş demektir
|
||
if old_segs.is_empty() {
|
||
old_segs.push((String::new(), disk_mb, BAR_FREE_COLOR));
|
||
}
|
||
|
||
ui.label(egui::RichText::new(format!("\u{EDFB} {}", disk))
|
||
.size(12.5).color(crate::ui::theme::c_accent()));
|
||
ui.add_space(2.0);
|
||
ui.horizontal(|ui| {
|
||
ui.label(egui::RichText::new(t!("mp_bar_now")).size(11.5).color(crate::ui::theme::c_text_dim()));
|
||
draw_layout_bar(ui, &old_segs);
|
||
});
|
||
ui.horizontal(|ui| {
|
||
ui.label(egui::RichText::new(t!("mp_bar_after")).size(11.5).color(crate::ui::theme::c_text_dim()));
|
||
draw_layout_bar(ui, &new_segs);
|
||
});
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Yardımcı: renkli bölüm şeridi (otomatik)
|
||
// ─────────────────────────────────────────────
|
||
|
||
fn partition_bar(ui: &mut egui::Ui, plan: &PartitionPlan) {
|
||
let total = (plan.efi_mb + plan.swap_mb + plan.root_mb) as f32;
|
||
let bar_height = 28.0;
|
||
let (rect, _) = ui.allocate_exact_size(
|
||
egui::vec2(ui.available_width(), bar_height),
|
||
egui::Sense::hover(),
|
||
);
|
||
let painter = ui.painter();
|
||
|
||
struct Seg<'a> { frac: f32, color: egui::Color32, label: &'a str }
|
||
let mut segments: Vec<Seg> = Vec::new();
|
||
|
||
if plan.efi_mb > 0 {
|
||
segments.push(Seg {
|
||
frac: plan.efi_mb as f32 / total,
|
||
color: egui::Color32::from_rgb(80, 160, 220),
|
||
label: "EFI",
|
||
});
|
||
}
|
||
segments.push(Seg {
|
||
frac: plan.swap_mb as f32 / total,
|
||
color: egui::Color32::from_rgb(220, 160, 40),
|
||
label: "swap",
|
||
});
|
||
segments.push(Seg {
|
||
frac: plan.root_mb as f32 / total,
|
||
color: egui::Color32::from_rgb(80, 190, 100),
|
||
label: "/",
|
||
});
|
||
|
||
let mut x = rect.left();
|
||
let rounding = egui::Rounding::same(4.0);
|
||
for (i, seg) in segments.iter().enumerate() {
|
||
let w = seg.frac * rect.width();
|
||
let seg_rect = egui::Rect::from_min_size(
|
||
egui::pos2(x, rect.top()),
|
||
egui::vec2(w - 1.0, bar_height),
|
||
);
|
||
let seg_rounding = if i == 0 {
|
||
egui::Rounding { nw: rounding.nw, sw: rounding.sw, ne: 0.0, se: 0.0 }
|
||
} else if i == segments.len() - 1 {
|
||
egui::Rounding { nw: 0.0, sw: 0.0, ne: rounding.ne, se: rounding.se }
|
||
} else {
|
||
egui::Rounding::ZERO
|
||
};
|
||
painter.rect_filled(seg_rect, seg_rounding, seg.color);
|
||
painter.text(
|
||
seg_rect.center(),
|
||
egui::Align2::CENTER_CENTER,
|
||
seg.label,
|
||
egui::FontId::proportional(12.0),
|
||
egui::Color32::WHITE,
|
||
);
|
||
x += w;
|
||
}
|
||
}
|
||
|
||
/// Sayıyı nokta ile binlik ayraçlı formata çevirir (örn. 7000000 → 7.000.000).
|
||
fn format_number_thousands(n: u64) -> String {
|
||
let s = n.to_string();
|
||
let mut result = String::new();
|
||
for (i, c) in s.chars().enumerate() {
|
||
if i > 0 && (s.len() - i) % 3 == 0 {
|
||
result.push('.');
|
||
}
|
||
result.push(c);
|
||
}
|
||
result
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::installer::{LogicalVolume, PartitionTableType};
|
||
|
||
fn disk(name: &str, size_gb: u64) -> DiskInfo {
|
||
DiskInfo {
|
||
name: name.to_string(),
|
||
vendor: "ATA".to_string(),
|
||
model: "VBOX HARDDISK".to_string(),
|
||
drive_type: "HDD".to_string(),
|
||
size_gb,
|
||
size_bytes: size_gb * 1_073_741_824,
|
||
}
|
||
}
|
||
|
||
fn part(device: &str, size_mb: u64, fstype: FsType, mountpoint: &str) -> CustomPartition {
|
||
CustomPartition {
|
||
disk: crate::installer::device_to_disk(device),
|
||
device: device.to_string(),
|
||
orig_device: device.to_string(),
|
||
size_mb,
|
||
fstype,
|
||
mountpoint: mountpoint.to_string(),
|
||
encrypt: false,
|
||
luks_password: String::new(),
|
||
luks_name: String::new(),
|
||
skip_format: false,
|
||
delete: false,
|
||
}
|
||
}
|
||
|
||
fn step(uefi: bool) -> PartitionStep {
|
||
PartitionStep {
|
||
scanned: true,
|
||
is_uefi: uefi,
|
||
manual: ManualState::default(),
|
||
show_luks_password: false,
|
||
}
|
||
}
|
||
|
||
// ── make_plan ────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn plan_rejects_small_disk() {
|
||
let d = disk("/dev/sdb", 9);
|
||
assert!(make_plan(&d, true).is_none());
|
||
assert!(make_plan(&d, false).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn plan_uefi_large_disk() {
|
||
let d = disk("/dev/sda", 100);
|
||
let plan = make_plan(&d, true).expect("100GB disk için plan oluşmalı");
|
||
assert_eq!(plan.disk, "/dev/sda");
|
||
assert_eq!(plan.table_type, PartitionTableType::Gpt);
|
||
assert!(plan.efi_mb > 0, "UEFI planında EFI bölümü olmalı");
|
||
// EFI + swap + root, overhead payı düşülmüş disk boyutunu aşmamalı
|
||
assert!(plan.efi_mb + plan.swap_mb + plan.root_mb <= 100 * 1024);
|
||
}
|
||
|
||
#[test]
|
||
fn plan_bios_large_disk() {
|
||
let d = disk("/dev/sda", 100);
|
||
let plan = make_plan(&d, false).expect("100GB disk için plan oluşmalı");
|
||
assert_eq!(plan.table_type, PartitionTableType::Mbr);
|
||
assert_eq!(plan.efi_mb, 0, "BIOS modunda EFI bölümü olmamalı");
|
||
}
|
||
|
||
// ── validate_manual ───────────────────────────────────────
|
||
|
||
#[test]
|
||
fn manual_empty_plan_fails() {
|
||
assert!(validate_manual(&[], &[], true).is_some());
|
||
assert!(validate_manual(&[], &[], false).is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn manual_root_only_ok_on_bios() {
|
||
let parts = vec![part("/dev/sda1", 20_000, FsType::Ext4, "/")];
|
||
assert!(validate_manual(&parts, &[], false).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn manual_uefi_requires_efi_part() {
|
||
let parts = vec![part("/dev/sda1", 20_000, FsType::Ext4, "/")];
|
||
assert!(
|
||
validate_manual(&parts, &[], true).is_some(),
|
||
"UEFI modunda EFI bölümü zorunlu"
|
||
);
|
||
|
||
let with_efi = vec![
|
||
part("/dev/sda1", 300, FsType::Fat32, "/boot/efi"),
|
||
part("/dev/sda2", 20_000, FsType::Ext4, "/"),
|
||
];
|
||
assert!(validate_manual(&with_efi, &[], true).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn manual_root_can_come_from_lv() {
|
||
let parts = vec![part("/dev/sda1", 20_000, FsType::Lvm, "")];
|
||
let lvs = vec![LogicalVolume {
|
||
name: "root".to_string(),
|
||
vg_name: "vg0".to_string(),
|
||
size_mb: 10_000,
|
||
fstype: FsType::Ext4,
|
||
mountpoint: "/".to_string(),
|
||
}];
|
||
assert!(validate_manual(&parts, &lvs, false).is_none());
|
||
}
|
||
|
||
// ── InstallerStep arayüzü ─────────────────────────────────
|
||
|
||
#[test]
|
||
fn auto_mode_complete_only_with_plan() {
|
||
let st = step(true);
|
||
let mut state = crate::installer::GlobalState::default();
|
||
state.erase_disk = true;
|
||
|
||
assert!(!st.is_complete(&state), "plan yokken tamamlanmamalı");
|
||
state.partition_plan = make_plan(&disk("/dev/sda", 100), true);
|
||
assert!(st.is_complete(&state), "plan varken tamamlanmalı");
|
||
}
|
||
|
||
#[test]
|
||
fn manual_mode_complete_with_valid_partitions() {
|
||
let st = step(true);
|
||
let mut state = crate::installer::GlobalState::default();
|
||
state.erase_disk = false;
|
||
|
||
assert!(!st.is_complete(&state), "bölüm yokken tamamlanmamalı");
|
||
|
||
// Kök var ama EFI yok → tamamlanmamalı
|
||
state.custom_partitions.push(part("/dev/sda1", 20_000, FsType::Ext4, "/"));
|
||
assert!(!st.is_complete(&state), "eksik EFI bölümü ile tamamlanmamalı");
|
||
|
||
// Geçerli düzen → tamamlanmalı
|
||
state.custom_partitions.push(part("/dev/sda1", 300, FsType::Fat32, "/boot/efi"));
|
||
assert!(st.is_complete(&state), "geçerli düzen ile tamamlanmalı");
|
||
}
|
||
|
||
// ── device_to_disk / assign_partition_numbers ───────────────
|
||
|
||
#[test]
|
||
fn device_to_disk_variants() {
|
||
assert_eq!(crate::installer::device_to_disk("/dev/sda1"), "/dev/sda");
|
||
assert_eq!(crate::installer::device_to_disk("/dev/sdb12"), "/dev/sdb");
|
||
assert_eq!(crate::installer::device_to_disk("/dev/nvme0n1p2"), "/dev/nvme0n1");
|
||
assert_eq!(crate::installer::device_to_disk("/dev/mmcblk0p1"), "/dev/mmcblk0");
|
||
assert_eq!(crate::installer::device_to_disk("/dev/vda3"), "/dev/vda");
|
||
}
|
||
|
||
#[test]
|
||
fn renumber_keeps_disks_independent() {
|
||
let mut parts = vec![
|
||
part("/dev/sda1", 300, FsType::Fat32, "/boot/efi"),
|
||
part("/dev/sdb1", 20_000, FsType::Ext4, "/"),
|
||
part("/dev/sda2", 4096, FsType::Swap, "swap"),
|
||
];
|
||
// sdb'deki tek tanım silinirse numaralar disk bazlı kaymalı
|
||
parts.remove(1);
|
||
assign_partition_numbers(&mut parts);
|
||
assert_eq!(parts[0].device, "/dev/sda1");
|
||
assert_eq!(parts[0].disk, "/dev/sda");
|
||
assert_eq!(parts[1].device, "/dev/sda2");
|
||
assert_eq!(parts[1].disk, "/dev/sda");
|
||
}
|
||
|
||
#[test]
|
||
fn deleted_slot_is_reused_by_new_partition() {
|
||
// Eski Yali modeli: silinmek işaretlenen sda2'nin slotu boşalır;
|
||
// yeni tanım en küçük boş slotu (2) alır; sda3 yerinde kalır.
|
||
let mut parts = vec![
|
||
part("/dev/sda1", 300, FsType::Fat32, "/boot/efi"),
|
||
part("/dev/sda2", 4096, FsType::Swap, "swap"),
|
||
part("/dev/sda3", 20_000, FsType::Ext4, "/"),
|
||
];
|
||
parts[1].delete = true;
|
||
parts.push({
|
||
let mut p = part("/dev/sda4", 8192, FsType::Ext4, "");
|
||
p.orig_device = String::new(); // yeni tanım
|
||
p.device = String::new();
|
||
p
|
||
});
|
||
assign_partition_numbers(&mut parts);
|
||
assert_eq!(parts[0].device, "/dev/sda1"); // fiziksel: korunur
|
||
assert!(parts[1].delete); // işaret: dokunulmaz
|
||
assert_eq!(parts[2].device, "/dev/sda3"); // yerinde kalır
|
||
assert_eq!(parts[3].device, "/dev/sda2"); // yeni: boş slotu alır
|
||
}
|
||
|
||
#[test]
|
||
fn used_mb_excludes_deleted_partitions() {
|
||
let mut parts = vec![
|
||
part("/dev/sda1", 1024, FsType::Fat32, "/boot/efi"),
|
||
part("/dev/sda2", 4096, FsType::Swap, "swap"),
|
||
];
|
||
parts[1].delete = true;
|
||
// Silinmek işaretli bölüm boş alan sayılır
|
||
assert_eq!(used_mb_on_disk(&parts, "/dev/sda", None), 1024);
|
||
}
|
||
|
||
#[test]
|
||
fn validate_ignores_deleted_root() {
|
||
let mut parts = vec![part("/dev/sda1", 20_000, FsType::Ext4, "/")];
|
||
assert!(validate_manual(&parts, &[], false).is_none());
|
||
parts[0].delete = true;
|
||
assert!(
|
||
validate_manual(&parts, &[], false).is_some(),
|
||
"kök silinecekse plan geçersiz olmalı"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn capacity_error_on_overflow() {
|
||
let disks = [disk("/dev/sdb", 25)];
|
||
let mut s1 = part("/dev/sdb1", 1024, FsType::Fat32, "/boot/efi");
|
||
let mut s2 = part("/dev/sdb2", 3886, FsType::Swap, "swap");
|
||
let mut s3 = part("/dev/sdb3", 21_096, FsType::Ext4, "/");
|
||
for p in [&mut s1, &mut s2, &mut s3] { p.orig_device = String::new(); }
|
||
// 1024+3886+21096 = 26006 > 25600 MB → hata
|
||
assert!(manual_capacity_error(&[s1.clone(), s2.clone(), s3.clone()], &disks).is_some());
|
||
// Kök sembolikse ("kalan alan") sorun yok
|
||
s3.size_mb = 0;
|
||
assert!(manual_capacity_error(&[s1, s2, s3], &disks).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn used_mb_on_disk_isolates_disks() {
|
||
let parts = vec![
|
||
part("/dev/sda1", 1024, FsType::Fat32, "/boot/efi"),
|
||
part("/dev/sdb1", 4096, FsType::Swap, "swap"),
|
||
part("/dev/sda2", 8192, FsType::Ext4, "/"),
|
||
];
|
||
assert_eq!(used_mb_on_disk(&parts, "/dev/sda", None), 1024 + 8192);
|
||
assert_eq!(used_mb_on_disk(&parts, "/dev/sdb", None), 4096);
|
||
// Düzenlenen bölüm (idx 0) hariç tutulunca kalan 8192 olmalı
|
||
assert_eq!(used_mb_on_disk(&parts, "/dev/sda", Some(0)), 8192);
|
||
}
|
||
|
||
#[test]
|
||
fn disk_size_mb_reads_available_disks() {
|
||
let mut state = crate::installer::GlobalState::default();
|
||
state.available_disks.push(disk("/dev/sda", 25));
|
||
state.available_disks.push(disk("/dev/nvme0n1", 100));
|
||
assert_eq!(disk_size_mb(&state, "/dev/sda"), 25 * 1024);
|
||
assert_eq!(disk_size_mb(&state, "/dev/nvme0n1"), 100 * 1024);
|
||
assert_eq!(disk_size_mb(&state, "/dev/yok"), 0);
|
||
}
|
||
|
||
// ── format_number_thousands ───────────────────────────────
|
||
|
||
#[test]
|
||
fn formats_thousands() {
|
||
assert_eq!(format_number_thousands(0), "0");
|
||
assert_eq!(format_number_thousands(999), "999");
|
||
assert_eq!(format_number_thousands(1000), "1.000");
|
||
assert_eq!(format_number_thousands(7000000), "7.000.000");
|
||
}
|
||
|
||
// ── detect_drive_type ─────────────────────────────────────
|
||
|
||
#[test]
|
||
fn drive_type_by_name() {
|
||
assert_eq!(detect_drive_type("nvme0n1"), "NVMe SSD");
|
||
assert_eq!(detect_drive_type("mmcblk0"), "MMC/SD");
|
||
assert_eq!(detect_drive_type("nonexistent99"), "HDD");
|
||
}
|
||
|
||
// ── lsblk / sysfs okumaları (canlı sistem) ────────────────
|
||
|
||
#[test]
|
||
fn read_existing_partitions_never_panics() {
|
||
// Var olmayan disk → boş liste, çökmemeli
|
||
assert!(read_existing_partitions("/dev/does-not-exist").is_empty());
|
||
|
||
// Var olan disklerde dönen bölümler /dev/ ön ekli olmalı
|
||
for d in list_block_devices().iter().take(1) {
|
||
for p in read_existing_partitions(&d.name) {
|
||
assert!(p.device.starts_with("/dev/"));
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn list_block_devices_scrubs_loop_ram() {
|
||
for d in list_block_devices() {
|
||
let base = d.name.trim_start_matches("/dev/");
|
||
assert!(!base.starts_with("loop"));
|
||
assert!(!base.starts_with("ram"));
|
||
assert!(!base.starts_with("sr"));
|
||
assert!(!base.starts_with("zram"));
|
||
}
|
||
}
|
||
}
|