diff --git a/.idea/modules.xml b/.idea11/modules.xml similarity index 100% rename from .idea/modules.xml rename to .idea11/modules.xml diff --git a/.idea/vcs.xml b/.idea11/vcs.xml similarity index 100% rename from .idea/vcs.xml rename to .idea11/vcs.xml diff --git a/.idea/workspace.xml b/.idea11/workspace.xml similarity index 100% rename from .idea/workspace.xml rename to .idea11/workspace.xml diff --git a/.idea/yali-rs.iml b/.idea11/yali-rs.iml similarity index 100% rename from .idea/yali-rs.iml rename to .idea11/yali-rs.iml diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 2b73450..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "rust-analyzer.checkOnSave": false -} \ No newline at end of file diff --git a/slides/slides_en copy.toml122 b/slides/slides_en copy.toml122 new file mode 100644 index 0000000..1359d93 --- /dev/null +++ b/slides/slides_en copy.toml122 @@ -0,0 +1,243 @@ +# ╔══════════════════════════════════════════════════════════════════╗ +# ║ Yali-RS — Slide Content (English) ║ +# ╚══════════════════════════════════════════════════════════════════╝ + +# Slide transition duration (seconds) +interval_secs = 6 + +[[items]] +icon = "slides/img/pisilinux-logo.png" +title = "Welcome to Pisi GNU/Linux" +description = "Pisi is a Turkish-supported GNU/Linux distribution that combines speed, stability, and ease of use." + +[[items]] +icon = "slides/img/pisi-software-center.png" +title = "PISI Package Manager" +description = "Access thousands of packages with a single command. Install anything you need easily with `pisi install `." + +[[items]] +icon = "slides/img/community-101.png" +title = "Community Support" +description = "Our active community at forum.pisilinux.org is ready to answer your questions. Our wiki pages are always accessible." + +[[items]] +icon = "slides/img/pisi_speed.png" +title = "Speed & Performance" +description = "Thanks to an optimized kernel and startup services, PisiLinux boots in seconds and uses minimal resources." + +[[items]] +icon = "slides/img/securety.png" +title = "Security First" +description = "Keep your system secure with regular security updates, AppArmor profiles, and secure default settings." + +[[items]] +icon = "slides/img/customization-dark.png" +title = "Customization" +description = "Tailor everything to your taste — from the desktop environment and theme colors to panel layout and keyboard shortcuts." + +# ── Budgie ──────────────────────────────────────────────────────── +[[items]] +desktop = "budgie" +icon = "slides/img/budgie.png" +title = "PisiLinux Budgie" +description = "A modern, clean, and elegant Budgie desktop. Built on the GNOME stack and user-friendly." + +[[items]] +desktop = "budgie" +icon = "slides/img/budgie_applets.png" +title = "Budgie Desktop Applets" +description = "Easily add tools like weather, system monitor, and calendar via Budgie's panel applets." + +[[items]] +desktop = "budgie" +icon = "slides/img/budgie_raven.png" +title = "Raven Notification Center" +description = "Access volume, media controls, notifications, and quick settings from the Raven panel." + +[[items]] +desktop = "budgie" +icon = "slides/img/budgie_gtk_theme.png" +title = "GTK Theme Support" +description = "Budgie runs GTK themes and GNOME applications seamlessly, ensuring visual consistency." + +# ── Cinnamon ────────────────────────────────────────────────────── +[[items]] +desktop = "cinnamon" +icon = "slides/img/cinnamon.png" +title = "PisiLinux Cinnamon" +description = "Cinnamon combines the traditional desktop experience with modern features, offering a powerful and flexible environment." + +[[items]] +desktop = "cinnamon" +icon = "slides/img/cinnamon_panel.png" +title = "Cinnamon Panel" +description = "Customize your panel with applets, launchers, and system indicators for a classic yet powerful desktop." + +[[items]] +desktop = "cinnamon" +icon = "slides/img/cinnamon_settings.png" +title = "Settings Manager" +description = "Fine-tune every aspect of your Cinnamon desktop through the comprehensive Settings Manager." + +[[items]] +desktop = "cinnamon" +icon = "slides/img/cinnamon_desklets.png" +title = "Cinnamon Desklets" +description = "Keep notes, clocks, and system info always visible on your desktop with Cinnamon Desklets." + +# ── GNOME ───────────────────────────────────────────────────────── +[[items]] +desktop = "gnome" +icon = "slides/img/gnome.png" +title = "PisiLinux GNOME" +description = "Reduce distractions and focus on your work with the clean and focused GNOME desktop environment." + +[[items]] +desktop = "gnome" +icon = "slides/img/gnome_search.png" +title = "GNOME Search" +description = "Press the Super key and start typing. Instantly reach applications, files, and settings." + +[[items]] +desktop = "gnome" +icon = "slides/img/gnome_extensions.png" +title = "GNOME Extensions" +description = "Shape your desktop to your needs with hundreds of extensions from extensions.gnome.org." + +# ── KDE ─────────────────────────────────────────────────────────── +[[items]] +desktop = "kde" +icon = "slides/img/kde.png" +title = "PisiLinux KDE Plasma" +description = "Meet the modern, customizable, and powerful KDE Plasma desktop. Everything is at your fingertips." + +[[items]] +desktop = "kde" +icon = "slides/img/file_manager.png" +title = "Dolphin File Manager" +description = "Dolphin makes managing your files easy with tabbed browsing, dual-panel view, and powerful search." + +[[items]] +desktop = "kde" +icon = "slides/img/kde_connect.png" +title = "KDE Connect" +description = "Connect your phone to your computer. Notifications, file transfer, and clipboard sharing — all with KDE Connect." + +[[items]] +desktop = "kde" +icon = "slides/img/discover.png" +title = "Discover Software Center" +description = "Easily find, install, and update applications from the visual interface with Discover." + +# ── LXQt ────────────────────────────────────────────────────────── +[[items]] +desktop = "lxqt" +icon = "slides/img/lxqt.png" +title = "PisiLinux LXQt" +description = "A lightweight, fast, and modern LXQt desktop environment. Low resource consumption, high performance." + +[[items]] +desktop = "lxqt" +icon = "slides/img/lxqt_panel.png" +title = "LXQt Panel" +description = "The traditional panel layout puts the taskbar, application launcher, and system tray always within reach." + +[[items]] +desktop = "lxqt" +icon = "slides/img/file_manager.png" +title = "PCManFM-Qt File Manager" +description = "Manage files effortlessly with the lightweight and fast PCManFM-Qt. Tabbed navigation and a customizable interface." + +# ── LXDE ────────────────────────────────────────────────────────── +[[items]] +desktop = "lxde" +icon = "slides/img/lxde.png" +title = "PisiLinux LXDE" +description = "The legendary lightweight LXDE desktop. Known for extremely low resource usage and a reliable build." + +[[items]] +desktop = "lxde" +icon = "slides/img/lxde_openbox.png" +title = "Openbox-Based" +description = "LXDE uses the Openbox window manager. Fine-tune it for a smooth and responsive experience." + +[[items]] +desktop = "lxde" +icon = "slides/img/file_manager.png" +title = "PCManFM File Manager" +description = "PCManFM speeds up your file operations with its fast and lightweight build, supporting tree view and tabs." + +# ── Lumina ──────────────────────────────────────────────────────── +[[items]] +desktop = "lumina" +icon = "slides/img/lumina.png" +title = "PisiLinux Lumina" +description = "A lightweight and independent Lumina desktop. Qt-based, designed to run cleanly on BSD and Linux systems." + +[[items]] +desktop = "lumina" +icon = "slides/img/lumina_applications.png" +title = "Application Support" +description = "Lumina works well with core utilities and many popular Qt apps, providing a practical everyday experience." + +[[items]] +desktop = "lumina" +icon = "slides/img/lumina_theme.png" +title = "Theme Customization" +description = "Easily customize color schemes and interface styles with Lumina's flexible theme system." + +[[items]] +desktop = "lumina" +icon = "slides/img/lumina_panel.png" +title = "Lumina Panel" +description = "A clean panel with a taskbar, application launcher, and system tray keeps your desktop organized." + +# ── MATE ────────────────────────────────────────────────────────── +[[items]] +desktop = "mate" +icon = "slides/img/mate.png" +title = "PisiLinux MATE" +description = "MATE for those who love the traditional desktop layout. Developed with GTK+ 3 for a modern and smooth experience." + +[[items]] +desktop = "mate" +icon = "slides/img/file_manager.png" +title = "Caja File Manager" +description = "Manage your files easily with MATE's powerful Caja. Dual panel, tabbed navigation, and rich features." + +[[items]] +desktop = "mate" +icon = "slides/img/mate_settings.png" +title = "MATE Settings" +description = "Customize your MATE desktop with comprehensive settings for appearance, display, keyboard, and more." + +[[items]] +desktop = "mate" +icon = "slides/img/mate_applications.png" +title = "Rich Application Suite" +description = "MATE comes with a rich set of classic applications — from text editors to image viewers, all well-integrated." + +# ── XFCE ────────────────────────────────────────────────────────── +[[items]] +desktop = "xfce" +icon = "slides/img/xfce.png" +title = "PisiLinux XFCE" +description = "A lightweight, fast, and user-friendly XFCE desktop environment. Low resource consumption, high performance." + +[[items]] +desktop = "xfce" +icon = "slides/img/xfce_panel.png" +title = "XFCE Panel" +description = "The traditional panel layout puts the taskbar, application menu, and system tray always within reach." + +[[items]] +desktop = "xfce" +icon = "slides/img/file_manager.png" +title = "Thunar File Manager" +description = "Manage your files easily with the fast and lightweight Thunar. Tree view, tabbed navigation, and customizable features." + +[[items]] +desktop = "xfce" +icon = "slides/img/xfce_settings.png" +title = "Whisker Menu" +description = "A modern alternative to the classic application menu. Find apps easily and organize your favorites with Whisker Menu." diff --git a/src/main.rs b/src/main.rs index 3a3b25c..8d9725d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -509,6 +509,7 @@ impl eframe::App for YaliApp { .inner_margin(egui::Margin::same(20.0))) .show(ctx, |ui| { egui::ScrollArea::vertical() + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) .id_source("main_scroll") .show(ui, |ui| { if let Some(step) = self.steps.get_mut(cur) { diff --git a/src/steps/partition.rs_24082026 b/src/steps/partition.rs_24082026 new file mode 100644 index 0000000..7a4664e --- /dev/null +++ b/src/steps/partition.rs_24082026 @@ -0,0 +1,2438 @@ +//! 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 new file mode 100644 index 0000000..fc6bd8d --- /dev/null +++ b/src/steps/partition.rs_orj20062026 @@ -0,0 +1,2382 @@ +//! 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")); + } + } +}