diff --git a/.gitignore b/.gitignore index 7ec2a63..cab1db9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,11 @@ Cargo.lock *.swo *.bak +# Eski sürüm yedekleri (git'e alınmamalı — git zaten var) +src/steps/*_orj*.rs* +src/steps/partition.rs_[0-9]* +*_copy.rs +*_copy.toml* + # OS files .DS_Store diff --git a/locales/tr.toml b/locales/tr.toml index 5646692..4fab7d1 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -125,7 +125,6 @@ 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 b443e5b..db7bb4e 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -113,7 +113,6 @@ 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, @@ -123,6 +122,7 @@ pub enum FsType { Ntfs, Swap, Lvm, // LVM fiziksel bölüm (PV) + #[allow(dead_code)] LvmLv, // LVM mantıksal bölüm (LV) } @@ -150,7 +150,6 @@ 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 2b018e4..0fd036c 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -1,27 +1,23 @@ //! 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; - -// ───────────────────────────────────────────── -// 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) - } -} +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 { use crate::installer::{CustomPartition, FsType}; @@ -45,7 +41,11 @@ 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,9 +60,10 @@ fn read_existing_partitions(disk_path: &str) -> Vec FsType::Fat32, "ntfs" => FsType::Ntfs, "swap" => FsType::Swap, - _ => FsType::Ext4, + _ => 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 { @@ -85,6 +86,8 @@ fn read_existing_partitions(disk_path: &str) -> Vec String { if name.starts_with("nvme") { return "NVMe SSD".into(); @@ -92,12 +95,14 @@ 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" { @@ -107,7 +112,9 @@ 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(); @@ -118,6 +125,7 @@ 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(); @@ -156,6 +164,7 @@ 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() { @@ -198,7 +207,7 @@ pub fn list_block_devices() -> Vec { use crate::installer::{ assign_partition_numbers, CustomPartition, DiskInfo, FsType, GlobalState, InstallerStep, - PartitionPlan, PartitionTableType, + PartitionPlan, PartitionTableType, VolumeGroup, }; const MIN_DISK_GB: u64 = 10; @@ -206,33 +215,46 @@ 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, - //create_over: 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, + /// Seçili satır indeksi selected_idx: Option, - selected_partition: Option, - selected_disk: Option, - selected_free_space: Option, - show_add_dialog: bool, - show_edit_dialog: bool, - add_fstype: 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, + /// 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, @@ -241,6 +263,17 @@ 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 // ───────────────────────────────────────────── @@ -263,6 +296,7 @@ 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(); @@ -278,6 +312,7 @@ 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; @@ -299,6 +334,8 @@ 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], @@ -318,38 +355,60 @@ fn validate_manual( } // Aynı mountpoint'ta iki bölüm tanımlı mı kontrolü - let mountpoints: std::collections::HashSet<&String> = parts.iter() - .filter(live) - .map(|p| &p.mountpoint) - .collect(); - if mountpoints.len() != parts.iter().filter(live).count() { + let live_parts: Vec<&CustomPartition> = parts.iter().filter(live).collect(); + let mountpoints: std::collections::HashSet<&str> = + live_parts.iter().map(|p| p.mountpoint.as_str()).collect(); + if mountpoints.len() != live_parts.len() { return Some(t!("mp_duplicate_mountpoint").to_string()); } - // Swap bölümünün fstype'ı swap olma kontrolü - let has_swap = parts.iter().filter(live).any(|p| p.fstype == FsType::Swap); - if has_swap { - let swap_parts: Vec<&CustomPartition> = parts.iter().filter(live).filter(|p| p.fstype == FsType::Swap).collect(); - for sp in &swap_parts { - if sp.size_mb < 512 { - return Some(format!("{} swap boyutu 512 MB'den az", sp.device)); - } - if sp.size_mb > 8192 { - return Some(format!("{} swap boyutu 8 GB'yi aşıyor", sp.device)); - } + // Swap bölümlerinin boyut sınırları (512 MB – 8 GB) + for sp in live_parts.iter().filter(|p| p.fstype == FsType::Swap) { + if sp.size_mb > 0 && sp.size_mb < 512 { + return Some(format!("{} swap boyutu 512 MB'den az", sp.device)); + } + if sp.size_mb > 8192 { + return Some(format!("{} swap boyutu 8 GB'yi aşıyor", sp.device)); } } - // Tüm bölümlerin boyutları 0'dan büyük olma kontrolü - for p in parts.iter().filter(live) { + // Boyut 0 ("kalan alan") tanımın mountpoint'i boşsa anlamsız + for p in &live_parts { if p.size_mb == 0 && p.mountpoint.is_empty() { - return Some(format!("{} boyut 0 (kalan alan) belirtilmiş ama mountpoint boş", p.device)); + return Some(format!( + "{} boyut 0 (kalan alan) belirtilmiş ama mountpoint boş", p.device + )); } } 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() @@ -393,11 +452,14 @@ 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 { @@ -408,6 +470,7 @@ 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()); } @@ -442,118 +505,10 @@ impl InstallerStep for PartitionStep { } // ───────────────────────────────────────────── -// Otomatik ve Manuel Mod Metotları +// Otomatik mod // ───────────────────────────────────────────── 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")); @@ -576,6 +531,7 @@ 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) @@ -647,8 +603,10 @@ 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; @@ -741,6 +699,7 @@ 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); @@ -777,68 +736,1757 @@ impl PartitionStep { }); } } +} - 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; - } - }); - } +// ───────────────────────────────────────────── +// 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; + 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()); - 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); - } - } - }); + // 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); + } + } + }); }); + + // // ── 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); - self.show_manual_partition_table(ui, state); + + 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")), + ); + + } + } } -} \ No newline at end of file +} + +// ───────────────────────────────────────────── +// 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ı"); + + // EFI eklendi ama disk seçilmedi → yine tamamlanmamalı + state.custom_partitions.push(part("/dev/sda1", 300, FsType::Fat32, "/boot/efi")); + assert!( + !st.is_complete(&state), + "disk seçilmeden tamamlanmamalı" + ); + + // Disk seçildi → geçerli düzen, tamamlanmalı + state.selected_disk = Some("/dev/sda".to_string()); + assert!(st.is_complete(&state), "geçerli düzen ile tamamlanmalı"); + } + + #[test] + fn manual_mode_requires_selected_disk() { + let st = step(true); + let mut state = crate::installer::GlobalState::default(); + state.erase_disk = false; + state.custom_partitions.push(part("/dev/sda1", 20_000, FsType::Ext4, "/")); + state.custom_partitions.push(part("/dev/sda2", 300, FsType::Fat32, "/boot/efi")); + + assert!( + state.selected_disk.is_none(), + "test ön koşulu: disk seçilmemiş olmalı" + ); + assert!( + !st.is_complete(&state), + "geçerli bölüm düzeni olsa bile disk seçilmeden tamamlanmamalı" + ); + } + + // ── 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")); + } + } +} diff --git a/src/steps/partition.rs_24082026 b/src/steps/partition.rs_24082026 deleted file mode 100644 index 7a4664e..0000000 --- a/src/steps/partition.rs_24082026 +++ /dev/null @@ -1,2438 +0,0 @@ -//! 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 { - 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 { - // Ö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, - /// 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) - 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, - 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 { - 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 { - 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) -> 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() - .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, 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)) - .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!("{} (EFI)", format_partition_device(&plan.disk, 1)), - 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)", format_partition_device(&plan.disk, swap_part)), - fs: "swap", - size: format!("{} MB", plan.swap_mb), - }); - let root_part = swap_part + 1; - satirlar.push(Satir { - part: format!("{}{}", format_partition_device(&plan.disk, root_part), t!("plan_root_suffix")), - fs: "ext4", - size: format!("{:.1} GB", plan.root_mb as f64 / 1024.0), - }); - - egui::Frame::none() - .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; - - 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); - } - } - }); - }); - - - // // ── 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")), - ); - - } - } - } -} - -// ───────────────────────────────────────────── -// 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")); - } - } -} diff --git a/src/steps/partition.rs_orj20062026 b/src/steps/partition.rs_orj20062026 deleted file mode 100644 index fc6bd8d..0000000 --- a/src/steps/partition.rs_orj20062026 +++ /dev/null @@ -1,2382 +0,0 @@ -//! 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 { - 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 { - // Ö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, - /// 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) - 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, - 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 { - 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 { - 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) -> 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() - .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 = 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 = 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(), - egui::Color32::from_hex("#001eff").unwrap(), - ); - 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), - egui::Color32::from_hex("#ffffff").unwrap(),//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), - ); - // 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)), - }; - painter.text(text_rect.center(), 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) = 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::().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(); - 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(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(0.0, 0.0)); - painter.rect_filled(rect, egui::Rounding::ZERO, header_color); - painter.text(inner.center(), 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; - } -} - -/// 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")); - } - } -} diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 3b53bbe..b09feb4 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -52,8 +52,7 @@ 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 {