diff --git a/locales/tr.toml b/locales/tr.toml index 4fab7d1..5646692 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -125,6 +125,7 @@ mp_free_space = "Boş Alan" mp_bar_now = "Mevcut Durum" mp_bar_after = "Kurulum Sonrası" + # ── UI / Genel ──────────────────────────────────────────── installer = "PisiLinux Kurulum Aracı" app_name = "YALI" diff --git a/src/installer.rs b/src/installer.rs index db7bb4e..b443e5b 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -113,6 +113,7 @@ impl std::fmt::Display for PartitionTableType { /// Manuel bölümleme için desteklenen dosya sistemleri. #[derive(Clone, Debug, PartialEq)] +#[allow(dead_code)] pub enum FsType { Ext4, Ext3, @@ -122,7 +123,6 @@ pub enum FsType { Ntfs, Swap, Lvm, // LVM fiziksel bölüm (PV) - #[allow(dead_code)] LvmLv, // LVM mantıksal bölüm (LV) } @@ -150,6 +150,7 @@ impl std::fmt::Display for FsType { /// LVM Volume Group yapılandırması. #[derive(Clone, Debug, Default)] +#[allow(dead_code)] pub struct VolumeGroup { pub name: String, pub pv_devices: Vec, // PV olarak kullanılacak bölüm aygıtları diff --git a/src/steps/partition.rs b/src/steps/partition.rs index 7a4664e..2d5aa49 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -1,23 +1,27 @@ //! Disk bölümleme adımı — Otomatik ve Manuel mod destekli. use eframe::egui; +use egui::Color32; +use egui_extras::{Column, TableBuilder}; 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. + +// ───────────────────────────────────────────── +// Yardımcı Fonksiyonlar ve Sabitler +// ───────────────────────────────────────────── + +fn format_partition_device(disk: &str, index: usize) -> String { + if disk.contains("nvme") || disk.contains("mmcblk") { + format!("{}p{}", disk, index) + } else { + format!("{}{}", disk, index) + } +} // ───────────────────────────────────────────── // 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 { use crate::installer::{CustomPartition, FsType}; @@ -41,11 +45,7 @@ fn read_existing_partitions(disk_path: &str) -> Vec 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); @@ -60,10 +60,9 @@ fn read_existing_partitions(disk_path: &str) -> Vec FsType::Fat32, "ntfs" => FsType::Ntfs, "swap" => FsType::Swap, - _ => FsType::Ext4, // bilinmeyen → varsayılan + _ => FsType::Ext4, }; - // 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 { @@ -86,8 +85,6 @@ fn read_existing_partitions(disk_path: &str) -> Vec String { if name.starts_with("nvme") { return "NVMe SSD".into(); @@ -95,14 +92,12 @@ fn detect_drive_type(name: &str) -> String { 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" { @@ -112,9 +107,7 @@ fn detect_drive_type(name: &str) -> String { "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 { - // Ö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(); @@ -125,7 +118,6 @@ pub fn list_block_devices() -> Vec { 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(); @@ -164,7 +156,6 @@ pub fn list_block_devices() -> Vec { } } - // 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() { @@ -207,7 +198,7 @@ pub fn list_block_devices() -> Vec { use crate::installer::{ assign_partition_numbers, CustomPartition, DiskInfo, FsType, GlobalState, InstallerStep, - PartitionPlan, PartitionTableType, VolumeGroup, + PartitionPlan, PartitionTableType, }; const MIN_DISK_GB: u64 = 10; @@ -215,46 +206,33 @@ 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)] +#[allow(dead_code)] 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 + //create_over: bool, edit_idx: Option, - /// Seçili satır indeksi selected_idx: Option, - /// Diyalog: seçili dosya sistemi - add_fstype: usize, // FsType listesindeki indeks - /// Diyalog: boyut alanı (MB, metin girişi) + selected_partition: Option, + selected_disk: Option, + selected_free_space: Option, + show_add_dialog: bool, + show_edit_dialog: bool, + add_fstype: usize, 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, - /// Genel hata mesajı error: Option, - /// 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, @@ -263,17 +241,6 @@ struct ManualState { 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 // ───────────────────────────────────────────── @@ -296,7 +263,6 @@ impl Default for PartitionStep { } } -/// RAM miktarına göre önerilen swap alanı (MB). fn recommended_swap_mb() -> u64 { use sysinfo::System; let mut sys = System::new(); @@ -312,7 +278,6 @@ fn recommended_swap_mb() -> u64 { 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 { if disk.size_gb < MIN_DISK_GB { return None; @@ -334,8 +299,6 @@ fn make_plan(disk: &DiskInfo, is_uefi: bool) -> Option { }) } -/// 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], @@ -356,31 +319,6 @@ fn validate_manual( 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) -> 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 { for d in disks { let defs: Vec<&CustomPartition> = parts.iter() @@ -424,14 +362,11 @@ impl InstallerStep for PartitionStep { 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 { @@ -442,7 +377,6 @@ impl InstallerStep for PartitionStep { } } } - // 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()); } @@ -476,10 +410,118 @@ impl InstallerStep for PartitionStep { } // ───────────────────────────────────────────── -// Otomatik mod +// Otomatik ve Manuel Mod Metotları // ───────────────────────────────────────────── impl PartitionStep { + pub fn show_manual_partition_table(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { + egui::Frame::canvas(ui.style()) + .fill(ui.visuals().panel_fill) + .stroke(ui.visuals().widgets.noninteractive.bg_stroke) + .inner_margin(4.0) + .show(ui, |ui| { + TableBuilder::new(ui) + .striped(true) + .resizable(false) + .cell_layout(egui::Layout::left_to_right(egui::Align::Center)) + .column(Column::initial(180.0).at_least(140.0)) + .column(Column::initial(130.0).at_least(90.0)) + .column(Column::initial(90.0).at_least(60.0)) + .column(Column::initial(120.0).at_least(80.0)) + .column(Column::initial(80.0).at_least(60.0)) + .column(Column::remainder()) + .header(26.0, |mut header| { + header.col(|ui| { ui.strong(t!("mp_device")); }); + header.col(|ui| { ui.strong(t!("column_mount_point")); }); + header.col(|ui| { ui.strong(t!("column_label")); }); + header.col(|ui| { ui.strong(t!("column_type")); }); + header.col(|ui| { ui.strong(t!("column_format")); }); + header.col(|ui| { ui.strong(t!("column_size")); }); + }) + .body(|mut body| { + for disk in &state.available_disks { + let disk_label = format!("💾 {} ({})", disk.model, disk.name); + + body.row(22.0, |mut row| { + row.col(|ui| { + ui.strong(disk_label); + }); + for _ in 1..6 { + row.col(|_| {}); + } + }); + + let disk_partitions: Vec<_> = state.custom_partitions + .iter() + .filter(|p| p.disk == disk.name) + .collect(); + + for part in disk_partitions { + let is_part_selected = self.manual.selected_partition.as_deref() == Some(&part.device); + + body.row(22.0, |mut row| { + row.col(|ui| { + if ui.selectable_label(is_part_selected, format!(" {}", part.device)).clicked() { + self.manual.selected_partition = Some(part.device.clone()); + self.manual.selected_disk = Some(disk.name.clone()); + self.manual.selected_free_space = None; + } + }); + row.col(|ui| { ui.label(&part.mountpoint); }); + row.col(|_| {}); + row.col(|ui| { ui.label(part.fstype.label()); }); + row.col(|ui| { + if !part.skip_format { + ui.label( + egui::RichText::new("✔") + .color(Color32::from_rgb(46, 125, 50)) + .strong(), + ); + } + }); + row.col(|ui| { ui.label(format!("{}", part.size_mb)); }); + }); + } + + let used_total: u64 = state.custom_partitions.iter() + .filter(|p| p.disk == disk.name && !p.delete) + .map(|p| p.size_mb) + .sum(); + let free_mb = (disk.size_bytes / (1024 * 1024)).saturating_sub(used_total); + + if free_mb > 10 { + let is_free_selected = self.manual.selected_free_space == Some(disk.name.clone()); + + body.row(22.0, |mut row| { + row.col(|ui| { + let label_text = egui::RichText::new(format!(" {}", t!("free_space"))) + .color(ui.visuals().weak_text_color()); + + if ui.selectable_label(is_free_selected, label_text).clicked() { + self.manual.selected_free_space = Some(disk.name.clone()); + self.manual.selected_partition = None; + self.manual.selected_disk = Some(disk.name.clone()); + } + }); + for _ in 1..5 { + row.col(|_| {}); + } + row.col(|ui| { + ui.label( + egui::RichText::new(format!("{}", free_mb)) + .color(ui.visuals().weak_text_color()), + ); + }); + }); + } + } + }); + }); + + ui.add_space(8.0); + self.render_action_buttons(ui, state); + } + fn show_auto_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { ui.group(|ui| { ui.label(t!("select_disk_label")); @@ -502,7 +544,6 @@ impl PartitionStep { ); 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, 255) @@ -574,10 +615,8 @@ impl PartitionStep { 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; @@ -670,7 +709,6 @@ impl PartitionStep { 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); @@ -707,1732 +745,68 @@ impl PartitionStep { }); } } -} -// ───────────────────────────────────────────── -// Manuel mod -// ───────────────────────────────────────────── + fn render_action_buttons(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { + ui.horizontal(|ui| { + let has_partition_selected = self.manual.selected_partition.is_some(); + let has_free_space_selected = self.manual.selected_free_space.is_some(); + + ui.add_enabled_ui(has_free_space_selected || self.manual.selected_disk.is_some(), |ui| { + if ui.button(format!("+ {}", t!("mp_add_btn"))).clicked() { + self.manual.show_add_dialog = true; + self.manual.adding = true; + } + }); + + ui.add_enabled_ui(has_partition_selected, |ui| { + if ui.button(format!("✏ {}", t!("mp_edit_partition"))).clicked() { + self.manual.show_edit_dialog = true; + self.manual.editing = true; + } + }); + + ui.add_enabled_ui(has_partition_selected, |ui| { + if ui.button(format!("\u{EC28} {}", t!("mp_delete"))).clicked() { + if let Some(ref sel_dev) = self.manual.selected_partition { + if let Some(pos) = state.custom_partitions.iter().position(|p| p.device == *sel_dev) { + self.manual.delete_idx = Some(pos); + } + } + } + }); + + if ui.button(format!("\u{F33F} {}", t!("mp_restore"))).clicked() { + state.custom_partitions.clear(); + self.manual.selected_partition = None; + self.manual.selected_free_space = None; + } + }); + } -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; - ui.group(|ui| { ui.label(egui::RichText::new("Kurulum Yapılacak Diski Seçin:").strong()); ui.add_space(6.0); - // Seçili disk için etiket let selected_label = state.selected_disk.as_ref() .and_then(|disk_name| state.available_disks.iter().find(|d| d.name == *disk_name)) .map(|disk| format!("{} - {} ({:.1} GB)", disk.name, disk.model, disk.size_bytes as f64 / 1_073_741_824.0)) .unwrap_or_else(|| "Disk Seçin".to_string()); - // ComboBox Oluşturma - egui::ComboBox::from_id_source("disk_selection_combobox") - .selected_text(selected_label) - .width(ui.available_width() - 20.0) // Genişliği panele yay - .show_ui(ui, |ui| { - for disk in &state.available_disks { - let label = format!("{} - {} ({:.1} GB)", disk.name, disk.model, disk.size_bytes as f64 / 1_073_741_824.0); - - if ui.selectable_value(&mut state.selected_disk, Some(disk.name.clone()), label).changed() { - // Disk değiştiğinde tetiklenecek işlemler (örn: disk bölümlerini yeniden okuma) - println!("Seçilen Disk Değişti: {}", disk.name); - } - } - }); + egui::ComboBox::from_id_source("disk_selection_combobox") + .selected_text(selected_label) + .width(ui.available_width() - 20.0) + .show_ui(ui, |ui| { + for disk in &state.available_disks { + let label = format!("{} - {} ({:.1} GB)", disk.name, disk.model, disk.size_bytes as f64 / 1_073_741_824.0); + if ui.selectable_value(&mut state.selected_disk, Some(disk.name.clone()), label).changed() { + println!("Seçilen Disk Değişti: {}", disk.name); + } + } + }); }); - - // // ── 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, 255) - // } else if response.hovered() { - // egui::Color32::from_rgba_unmultiplied(60, 60, 80, 180) - // } else { - // egui::Color32::from_rgba_unmultiplied(237, 237, 224, 40) - // }) - // .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 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 = 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 = 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::select_disk_label(), - ); - - // 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), - ); - // Tablo okunabilirliği: metin sütunları sola, sayı (Boyut) - // sağa yaslı; aygıt adı monospace (/dev/sdb1 vs sdb10 ayrımı). - let (anchor, font) = match j { - 0 => (egui::Align2::LEFT_CENTER, egui::FontId::monospace(12.5)), - 1 => (egui::Align2::RIGHT_CENTER, egui::FontId::monospace(12.5)), - _ => (egui::Align2::LEFT_CENTER, egui::FontId::proportional(13.0)), - }; - // Hizalama noktası anchor'a göre seçilir: LEFT_CENTER için hücrenin - // SOL kenarı, RIGHT_CENTER için SAĞ kenarı. Her ikisinde de rect.center() - // kullanmak metnin hücre ORTASINDAN başlamasına yol açıyordu; uzun - // aygıt adlarında (örn. "/dev/nvme0n1p1") bu, metnin yarısının bir - // sonraki sütuna taşıp üstüne binmesine (görsel olarak karakterlerin - // kayıp/örtülü görünmesine) sebep oluyordu. - let anchor_pos = match anchor { - egui::Align2::LEFT_CENTER => text_rect.left_center(), - egui::Align2::RIGHT_CENTER => text_rect.right_center(), - _ => text_rect.center(), - }; - painter.text(anchor_pos, anchor, *text, font, 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) = 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 => "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::().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 = 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 = 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 = 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(); - let overflows = if size_mb == 0 { - // "Kalan alan" (sembolik): VG'de hiç boş yer kalmamışsa hata. - current_sum >= vg.size_mb - } else { - current_sum + size_mb > vg.size_mb - }; - if overflows { - 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")), - ); - - } - } + self.show_manual_partition_table(ui, state); } -} - -// ───────────────────────────────────────────── -// 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(0, 0, 0); - 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; - // Başlık hizası içerikle aynı: Aygıt/Dosya Sistemi/Bağlama sola, - // Boyut sağa, Biçim ortalanmış. - let header_aligns = [ - egui::Align2::LEFT_CENTER, - egui::Align2::RIGHT_CENTER, - egui::Align2::LEFT_CENTER, - egui::Align2::LEFT_CENTER, - egui::Align2::CENTER_CENTER, - ]; - 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 painter = ui.painter(); - // İçerikle aynı 8px iç boşluk - let inner = rect.shrink2(egui::vec2(8.0, 0.0)); - painter.rect_filled(rect, egui::Rounding::ZERO, header_color); - // Hizalama noktası anchor'a göre seçilir (bkz. satır çiziminde aynı düzeltme): - // rect.center() her anchor için kullanılırsa LEFT_CENTER/RIGHT_CENTER - // başlıklar hücre ortasından başlar/biter, uzun başlıklarda taşma olur. - let anchor_pos = match header_aligns[j] { - egui::Align2::LEFT_CENTER => inner.left_center(), - egui::Align2::RIGHT_CENTER => inner.right_center(), - _ => inner.center(), - }; - painter.text(anchor_pos, header_aligns[j], h, egui::FontId::proportional(13.0), header_text); - } - }); -} - -/// 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 = 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 = 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; - } -} - -/// Disk adına bölüm numarasını doğru ekle: `sda`+1 → `sda1`, ama -/// `nvme0n1`/`mmcblk0`/`loop0` gibi rakamla biten aygıtlarda araya -/// `p` konur → `nvme0n1p1`, `mmcblk0p1`. (bkz. `device_to_disk` — aynı -/// kuralın tersini uygular.) -fn format_partition_device(disk: &str, num: u64) -> String { - if disk.chars().last().map(|c| c.is_ascii_digit()).unwrap_or(false) { - format!("{}p{}", disk, num) - } else { - format!("{}{}", disk, num) - } -} - -/// 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")); - } - } -} +} \ No newline at end of file diff --git a/src/ui/theme.rs b/src/ui/theme.rs index b09feb4..3b53bbe 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -52,7 +52,8 @@ pub fn c_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering:: pub fn c_footer_bg() -> Color32 { color_from_hex(BG_FOOTER.load(Ordering::Relaxed)) } pub fn c_root_bg() -> Color32 { color_from_hex(BG_ROOT.load(Ordering::Relaxed)) } pub fn c_keyboard_bg() -> Color32 { color_from_hex(KEYBOARD_BG.load(Ordering::Relaxed)) } -pub fn select_disk_label() -> Color32 { color_from_hex(0xFFFFFF) } +//pub fn select_disk_label() -> Color32 { color_from_hex(0xFFFFFF) } + pub fn set_theme_mode(is_dark: bool) { if is_dark {