Files
yali-rs/src/steps/partition.rs
T
Erkan IŞIK 7209d58ea0 fix: parted birim tuzağı — mkpart argümanları MiB sonekine geçti
- parted "MB" sonekini onalık (10^6) okur, hesaplarımız binary (2^20):
  3944MB → sektör 7703125 (hizasız) → "en yakın konum" hatası.
  Tüm mkpart konumları artık "{}MiB" (tamsayı MiB = 2048 sektör katı).
- Bölümleme Planı şeridi yalnızca seçili disk için gösteriliyor
  (state.selected_disk), tüm diskler aynı anda değil.
- AGENTS.md: parted birim tuzağı notu eklendi.
2026-08-23 14:35:01 +03:00

2375 lines
107 KiB
Rust
Raw Blame History

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