feat: çoklu disk desteği ve eski Yali tarzı granular bölümleme işlemleri

- CustomPartition artık disk alanı taşıyor; bölümler diske göre gruplanıyor
- Manuel modda TÜM disklerin mevcut bölümleri otomatik listeleniyor
- Tablo her disk için kendi sütun başlıklarını gösteriyor
- Silinmek işaretli bölümler tek 'Boş Alan' satırında birleşiyor (GParted tarzı);
  Boş Alan = disk − (korunanlar + yeni tanımlar), 0'a düşerse satır gizlenir
- Sil/Geri Al: fiziksel bölümler silinmek İŞARETLENİR (OperationDestroyDevice
  karşılığı), kullanıcı tanımı olanlar listeden tamamen kalkar
- Kurulum job'ları granular modele geçti: mktable ile tüm disk sıfırlanmıyor;
  rm → boş alana mkpart (sysfs geometrisi) → yalnızca istenenler mkfs.
  İşlemi olmayan disklere hiç dokunulmaz
- Slot ataması parted davranışıyla birebir: silinen numara yeniden kullanılır
- 'Kalan alan' sembolik kaldı; boyut kurulum anında boşluktan belirlenir
  (taşma imkânsız) + kapasite aşımı uyarısı/kilidi eklendi
- Özet ekranı ve bölüm planı: disk başına Mevcut/Kurulum Sonrası çift bar,
  kapasite gösterimleri düzeltildi
- Remixicon simge düzeltmesi (hard-drive), mp_* yeni çeviri anahtarları
- Testler: slot yeniden kullanımı, kapasite aşımı, disk-bazlı hesaplar (22/22)
This commit is contained in:
Erkan IŞIK
2026-08-22 21:29:31 +03:00
parent 379d32ac38
commit 5c148472c4
7 changed files with 1055 additions and 256 deletions
+7
View File
@@ -141,6 +141,13 @@ display_manager_autologin_hint = "(first user)"
display_manager_de_not_found = "(not detected)" display_manager_de_not_found = "(not detected)"
display_manager_note = "Note: The display manager will be enabled by default on the installed system." display_manager_note = "Note: The display manager will be enabled by default on the installed system."
mp_size_remaining = "Remaining Space" mp_size_remaining = "Remaining Space"
mp_size_available = "Available"
mp_create_here = "Create Here"
mp_will_delete = "will be deleted"
mp_restore = "Restore"
mp_free_space = "Free Space"
mp_bar_now = "Current Layout"
mp_bar_after = "After Installation"
# ── UI / General ────────────────────────────────────────── # ── UI / General ──────────────────────────────────────────
installer = "YALI Installation Tool" installer = "YALI Installation Tool"
+7
View File
@@ -117,6 +117,13 @@ disk_too_small = "Disk çok küçük"
summary_partition_plan = "Bölümleme" summary_partition_plan = "Bölümleme"
summary_custom_partitions = "Manuel Bölümler" summary_custom_partitions = "Manuel Bölümler"
mp_size_remaining = "Kalan Alan" mp_size_remaining = "Kalan Alan"
mp_size_available = "Kullanılabilir"
mp_create_here = "Buraya Oluştur"
mp_will_delete = "silinecek"
mp_restore = "Geri Al"
mp_free_space = "Boş Alan"
mp_bar_now = "Mevcut Durum"
mp_bar_after = "Kurulum Sonrası"
# ── UI / Genel ──────────────────────────────────────────── # ── UI / Genel ────────────────────────────────────────────
installer = "PisiLinux Kurulum Aracı" installer = "PisiLinux Kurulum Aracı"
+79
View File
@@ -167,11 +167,86 @@ pub struct LogicalVolume {
pub mountpoint: String, pub mountpoint: String,
} }
/// Cihaz yolundan ait olduğu diskin yolunu çıkarır.
///
/// - `/dev/sda1` → `/dev/sda`
/// - `/dev/nvme0n1p2` → `/dev/nvme0n1`
/// - `/dev/mmcblk0p1` → `/dev/mmcblk0`
#[allow(dead_code)] // genel yardımcı; testlerde ve gelecekteki işlerde kullanılır
pub fn device_to_disk(device: &str) -> String {
let base = device.trim_start_matches("/dev/");
// NVMe/MMC/loop aygıtlarında bölüm ayracı "p" + rakamdır
let needs_p = base.contains("nvme") || base.contains("mmcblk") || base.contains("loop");
if let Some(pos) = base.rfind('p') {
if needs_p && base[pos + 1..].chars().all(|c| c.is_ascii_digit()) && !base[pos + 1..].is_empty() {
return format!("/dev/{}", &base[..pos]);
}
}
// SATA/virtio: sondaki tüm rakamları at
let trimmed = base.trim_end_matches(|c: char| c.is_ascii_digit());
format!("/dev/{}", trimmed)
}
/// Cihaz adından bölüm numarasını çıkarır ("/dev/sda3" → 3,
/// "/dev/nvme0n1p2" → 2). Sonda rakam yoksa None döner.
pub fn part_number(device: &str) -> Option<u32> {
let base = device.trim_start_matches("/dev/");
let digits: String = base.chars().rev().take_while(|c| c.is_ascii_digit()).collect();
digits.chars().rev().collect::<String>().parse().ok()
}
/// Bölüm tanımlarına parted'ın gerçek slot davranışını izleyerek
/// aygıt numarası atar.
///
/// Eski Yali modeliyle aynı mantık: fiziksel (silinmemiş) bölümler
/// kendi numaralarını korur; silinmek işaretlenenlerin slotları boşalır
/// ve yeni tanımlar en küçük boş slotu sırayla doldurur. Örn. sda2
/// silinip yeni bölüm eklenirse yeni bölüm sda2 olur; sda3 yerinde kalır.
pub fn assign_partition_numbers(parts: &mut [CustomPartition]) {
use std::collections::BTreeSet;
// Disk bazlı kullanılan slot kümeleri
let mut used: std::collections::HashMap<String, BTreeSet<u32>> = std::collections::HashMap::new();
for p in parts.iter() {
if p.delete { continue; } // silinecekler slot açar
if !p.orig_device.is_empty() { // fiziksel: kendi numarası
if let Some(n) = part_number(&p.orig_device) {
used.entry(p.disk.clone()).or_default().insert(n);
}
}
}
for p in parts.iter_mut() {
if p.delete {
// Silinecek bölüm: kurulumda yok olacak; adı olduğu gibi kalır,
// yeni slot tüketmez.
continue;
}
match part_number(&p.orig_device) {
Some(n) => { p.device = part_path(&p.disk, n); }
None => {
let set = used.entry(p.disk.clone()).or_default();
// en küçük boş slot
let mut n = 1u32;
while set.contains(&n) { n += 1; }
set.insert(n);
p.device = part_path(&p.disk, n);
}
}
}
}
/// Kullanıcının manuel olarak tanımladığı tek bir bölüm. /// Kullanıcının manuel olarak tanımladığı tek bir bölüm.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CustomPartition { pub struct CustomPartition {
/// Bölümün ait olduğu disk (ör. "/dev/sda").
/// Çoklu disk kurulumunda bölümler diske göre gruplanır.
pub disk: String,
/// Bölüm aygit adı; oluşturulacak (ör. "/dev/sda1") /// Bölüm aygit adı; oluşturulacak (ör. "/dev/sda1")
pub device: String, pub device: String,
/// Taramada bulunan fiziksel cihaz adı (örn. "/dev/sda3").
/// Kullanıcı tarafından yeni eklenen tanımlarda boştur.
/// Silme işleminde satırın listede kalıp yeniden
/// tanımlanabilmesini sağlar.
pub orig_device: String,
/// Boyut MB cinsinden; 0 = "kalan alan" /// Boyut MB cinsinden; 0 = "kalan alan"
pub size_mb: u64, pub size_mb: u64,
/// Dosya sistemi /// Dosya sistemi
@@ -186,6 +261,10 @@ pub struct CustomPartition {
pub luks_name: String, pub luks_name: String,
/// false = biçimlendir, true = olduğu gibi bırak (mevcut bölümleri koru) /// false = biçimlendir, true = olduğu gibi bırak (mevcut bölümleri koru)
pub skip_format: bool, pub skip_format: bool,
/// true = kurulum sırasında bu bölüm diskten SİLİNECEK
/// (eski Yali'deki OperationDestroyDevice karşılığı).
/// Satır listede görünür kalır; istenirse işaret geri alınır.
pub delete: bool,
} }
/// Kurulum boyunca toplanan tüm veriler. /// Kurulum boyunca toplanan tüm veriler.
+1 -1
View File
@@ -633,7 +633,7 @@ pub fn build_custom_job_queue(
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source }; let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
let mut jobs: Vec<Box<dyn Job>> = vec![ let mut jobs: Vec<Box<dyn Job>> = vec![
Box::new(CustomPartitionJob { disk: disk.clone(), custom_partitions: custom_partitions.clone() }), Box::new(CustomPartitionJob { custom_partitions: custom_partitions.clone() }),
Box::new(SetupLvmJob { volume_groups: volume_groups.clone(), logical_volumes: logical_volumes.clone() }), Box::new(SetupLvmJob { volume_groups: volume_groups.clone(), logical_volumes: logical_volumes.clone() }),
Box::new(MountCustomPartitionsJob { custom_partitions: custom_partitions.clone(), logical_volumes: logical_volumes.clone(), mount: mt.clone() }), Box::new(MountCustomPartitionsJob { custom_partitions: custom_partitions.clone(), logical_volumes: logical_volumes.clone(), mount: mt.clone() }),
Box::new(MountBindJob { mount: mt.clone() }), Box::new(MountBindJob { mount: mt.clone() }),
+200 -60
View File
@@ -448,9 +448,75 @@ async fn read_partition_uuids(
// MANUEL BÖLÜMLEME İÇİN DESTEKLEYİCİ JOB'LAR // MANUEL BÖLÜMLEME İÇİN DESTEKLEYİCİ JOB'LAR
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
/// Manuel bölümleme ayarlarını disk üzerinde uygulayan ve biçimlendiren Job. /// Diskteki boş bölgeleri MB cinsinden hesaplar (sysfs tabanlı).
///
/// `exclude_nums`: silinecek işaretli bölümler — boşluk olarak
/// davranılır (kurulum sırasında önce silindikleri için).
/// Dönen değer: (başlangıç_mb, bitiş_mb) aralıkları; disk sonunda
/// GPT yedek başlığı için 1MB pay bırakılır.
fn disk_free_regions_mb(disk: &str, exclude_nums: &[u32]) -> Result<Vec<(u64, u64)>, String> {
let base = disk.trim_start_matches("/dev/");
const SECTOR: u64 = 512;
const MB: u64 = 1024 * 1024;
let disk_sectors: u64 = std::fs::read_to_string(format!("/sys/block/{}/size", base))
.map_err(|e| format!("{} boyutu okunamadı: {}", disk, e))?
.trim().parse().unwrap_or(0);
if disk_sectors == 0 {
return Err(format!("{} disk boyutu okunamadı", disk));
}
let total_mb = disk_sectors * SECTOR / MB;
let usable_total = total_mb.saturating_sub(1); // disk sonu güvenlik payı
// Dolu aralıklar
let mut occupied: Vec<(u64, u64)> = Vec::new();
if let Ok(entries) = std::fs::read_dir(format!("/sys/block/{}", base)) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !name.starts_with(base) || name == base { continue; }
let num = match crate::installer::part_number(&format!("/dev/{}", name)) {
Some(n) => n,
None => continue,
};
if exclude_nums.contains(&num) { continue; }
let start_sectors: u64 = std::fs::read_to_string(entry.path().join("start"))
.ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
let len_sectors: u64 = std::fs::read_to_string(entry.path().join("size"))
.ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
if len_sectors == 0 { continue; }
let start_mb = (start_sectors * SECTOR + MB - 1) / MB;
let end_mb = ((start_sectors + len_sectors) * SECTOR) / MB;
if end_mb > start_mb {
occupied.push((start_mb, end_mb.min(usable_total)));
}
}
}
occupied.sort();
// Boşlukları çıkar (ilk 1MB boot alanı hariç)
const HEADROOM_MB: u64 = 1;
let mut regions: Vec<(u64, u64)> = Vec::new();
let mut cursor = HEADROOM_MB.min(usable_total);
for (s, e) in &occupied {
if *s > cursor { regions.push((cursor, *s)); }
cursor = cursor.max(*e);
}
if cursor < usable_total { regions.push((cursor, usable_total)); }
regions.retain(|(s, e)| e > s);
Ok(regions)
}
/// Manuel bölümleme ayarlarını granular (eski Yali Operation modeli)
/// yaklaşımla uygulayan Job.
///
/// Eski Yali'nin DeviceTree/Operation mimarisiyle aynı felsefe:
/// - Dokunulmayan disklere ve bölümlere HİÇ dokunulmaz (veriler korunur)
/// - Silinmek işaretli bölümler `parted rm` ile kaldırılır
/// (OperationDestroyDevice karşılığı)
/// - Yeni tanımlar diskin BOŞ alanlarına yerleştirilir; tüm disk
/// sıfırlanmaz (OperationCreateDevice karşılığı)
/// - skip_format=false olanlar biçimlendirilir (OperationCreateFormat)
pub struct CustomPartitionJob { pub struct CustomPartitionJob {
pub disk: String,
pub custom_partitions: Vec<CustomPartition>, pub custom_partitions: Vec<CustomPartition>,
} }
@@ -460,68 +526,141 @@ impl Job for CustomPartitionJob {
async fn run(&self, ui: &UiSender) -> Result<(), String> { async fn run(&self, ui: &UiSender) -> Result<(), String> {
let is_uefi = std::path::Path::new("/sys/firmware/efi").exists(); let is_uefi = std::path::Path::new("/sys/firmware/efi").exists();
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
prepare_disk(&self.disk, ui).await;
let is_gpt = is_uefi; // Genellikle UEFI ise GPT kullanılır
// MBR'de "primary" zorunludur, GPT'de ise bu alan isim (label) alanıdır. // MBR'de "primary" zorunludur, GPT'de ise bu alan isim (label) alanıdır.
let part_type_or_name = if is_gpt { "" } else { "primary" }; let part_type_or_name = if is_uefi { "" } else { "primary" };
// 1. Bölüm tablosu oluştur // 0. İşlem yapılan diskleri tanım sırasına göre topla
let table_label = if is_uefi { "gpt" } else { "msdos" }; let mut disks: Vec<String> = Vec::new();
ui.log(format!("parted {} mktable {}", self.disk, table_label)); for part in &self.custom_partitions {
run_parted(&["-s", &self.disk, "mktable", table_label]).await?; if !disks.contains(&part.disk) {
disks.push(part.disk.clone());
let mut next_mb: u64 = 1; // İlk MB boot sektörü
// 2. Bölümleri parted ile oluştur
for (i, part) in self.custom_partitions.iter().enumerate() {
let part_num = (i + 1) as u32;
let end_str = if part.size_mb == 0 {
"100%".to_string()
} else {
format!("{}MB", next_mb + part.size_mb)
};
let start_str = format!("{}MB", next_mb);
let fs_label = match part.fstype {
FsType::Fat32 => "fat32",
FsType::Swap => "linux-swap",
FsType::Ext4 => "ext4",
FsType::Ext3 => "ext3",
FsType::Btrfs => "btrfs",
FsType::Xfs => "xfs",
FsType::Ntfs => "ntfs",
// LVM PV için parted "ext4" ile işaretle, pvcreate sonra
FsType::Lvm => "ext4",
// LVM LV doğrudan parted ile oluşturulmaz
FsType::LvmLv => continue,
};
ui.log(format!(
"Bölüm {} oluşturuluyor: {} - {} ({})",
part_num, start_str, end_str, fs_label
));
run_parted(&[
"-s", &self.disk, "mkpart", part_type_or_name, fs_label,
&start_str, &end_str
]).await?;
if part.mountpoint == "/boot/efi" || part.fstype == FsType::Fat32 {
run_parted(&["-s", &self.disk, "set", &part_num.to_string(), "esp", "on"]).await?;
}
if part.size_mb > 0 {
next_mb += part.size_mb;
} }
} }
// 2.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle for disk in &disks {
sync_partition_table(&self.disk).await; let parts: Vec<&CustomPartition> = self.custom_partitions.iter()
.filter(|p| p.disk == *disk).collect();
// 3. Biçimlendir (LUKS ile şifreleme desteklenir) // Bu disk için planlanan işlemler
let destroys: Vec<u32> = parts.iter()
.filter(|p| p.delete && !p.orig_device.is_empty())
.filter_map(|p| crate::installer::part_number(&p.orig_device))
.collect();
let mut creates: Vec<&CustomPartition> = parts.iter()
.filter(|p| !p.delete && p.orig_device.is_empty() && p.fstype != FsType::LvmLv)
.map(|p| *p)
.collect();
// Sabit boyutlular ÖNCE yerleşir; "kalan alan" (size 0)
// tanımları EN SON, geriye kalan boşluğa yerleştirilir.
// (Stable sort → aynı grupta tanım sırası korunur.)
creates.sort_by_key(|p| p.size_mb == 0);
let has_format = parts.iter().any(|p| !p.delete && !p.skip_format);
if destroys.is_empty() && creates.is_empty() && !has_format {
ui.log(format!("⏭️ {}: işlem yok, korunuyor", disk));
continue; // eski Yali gibi dokunulmayan diske girilmez
}
ui.log(format!("── Disk işleniyor: {} ──", disk));
// Bağlamaları ayır, swap kapat
prepare_disk(disk, ui).await;
// ── FAZ A: SİLMEK İŞARETLİ BÖLÜMLER (büyükten küçüğe numara) ──
let mut del_sorted = destroys.clone();
del_sorted.sort_unstable_by(|a, b| b.cmp(a));
for n in &del_sorted {
ui.log(format!("parted {} rm {} (bölüm siliniyor)", disk, n));
run_parted(&["-s", disk, "rm", &n.to_string()]).await?;
}
if !del_sorted.is_empty() {
sync_partition_table(disk).await;
}
// ── FAZ B: YENİ BÖLÜMLER (boş alana yerleştir) ──────────────
if !creates.is_empty() {
let mut regions = disk_free_regions_mb(disk, &destroys)?;
for part in &creates {
let fs_label = match part.fstype {
FsType::Fat32 => "fat32",
FsType::Swap => "linux-swap",
FsType::Ext4 => "ext4",
FsType::Ext3 => "ext3",
FsType::Btrfs => "btrfs",
FsType::Xfs => "xfs",
FsType::Ntfs => "ntfs",
// LVM PV için parted "ext4" ile işaretle, pvcreate sonra
FsType::Lvm => "ext4",
FsType::LvmLv => continue,
};
// Yerleşim: boyut belirtilmişse sığan İLK boş bölge,
// "kalan alan" ise en büyük bölgenin tamamı.
let need = part.size_mb;
let placement = if need == 0 {
let idx = regions.iter().enumerate()
.max_by_key(|(_, (s, e))| e.saturating_sub(*s))
.map(|(i, _)| i);
match idx {
Some(i) => {
let r = regions.remove(i);
Some(r)
}
None => None,
}
} else {
let mut found = None;
for (i, (s, e)) in regions.iter().enumerate() {
if e.saturating_sub(*s) >= need {
found = Some((i, (*s, s + need)));
break;
}
}
match found {
Some((i, seg)) => {
let (_, e) = regions[i];
let start = seg.0;
let new_start = start + need;
if e > new_start {
regions[i] = (new_start, e);
} else {
regions.remove(i);
}
Some(seg)
}
None => None,
}
};
let (start_mb, end_mb) = placement.ok_or_else(|| format!(
"{} üzerinde {} MB için yeterli boş alan yok", disk, need))?;
let start_str = format!("{}MB", start_mb);
let end_str = format!("{}MB", end_mb);
let slot = crate::installer::part_number(&part.device).unwrap_or(0);
ui.log(format!(
"{} üzerinde yeni bölüm oluşturuluyor: {} - {} ({})",
disk, start_str, end_str, fs_label
));
run_parted(&[
"-s", disk, "mkpart", part_type_or_name, fs_label,
&start_str, &end_str
]).await?;
if part.mountpoint == "/boot/efi" || part.fstype == FsType::Fat32 {
run_parted(&["-s", disk, "set", &slot.to_string(), "esp", "on"]).await?;
}
}
sync_partition_table(disk).await;
}
// ── FAZ C: BİÇİMLENDİRME aşağıdaki ortak döngüde ────────────
}
// Biçimlendir (LUKS ile şifreleme desteklenir) — tüm diskler.
// Silinmek işaretli bölümlere hiç dokunulmaz.
for part in &self.custom_partitions { for part in &self.custom_partitions {
if part.delete { continue; }
let path = &part.device; let path = &part.device;
// LUKS şifreleme (kök veya isteğe bağlı bölümler) // LUKS şifreleme (kök veya isteğe bağlı bölümler)
@@ -667,7 +806,8 @@ impl Job for MountCustomPartitionsJob {
async fn run(&self, ui: &UiSender) -> Result<(), String> { async fn run(&self, ui: &UiSender) -> Result<(), String> {
// 1. Kök (/) bölümünü (fiziksel partition veya LVM LV) bul ve bağla // 1. Kök (/) bölümünü (fiziksel partition veya LVM LV) bul ve bağla
let root_device = if let Some(root_part) = self.custom_partitions.iter().find(|p| p.mountpoint == "/") { let root_device = if let Some(root_part) = self.custom_partitions.iter()
.find(|p| p.mountpoint == "/" && !p.delete) {
self.device_path(root_part) self.device_path(root_part)
} else if let Some(root_lv) = self.logical_volumes.iter().find(|lv| lv.mountpoint == "/") { } else if let Some(root_lv) = self.logical_volumes.iter().find(|lv| lv.mountpoint == "/") {
format!("/dev/{}/{}", root_lv.vg_name, root_lv.name) format!("/dev/{}/{}", root_lv.vg_name, root_lv.name)
@@ -687,7 +827,7 @@ impl Job for MountCustomPartitionsJob {
let mut mount_items = Vec::new(); let mut mount_items = Vec::new();
for part in &self.custom_partitions { for part in &self.custom_partitions {
if part.mountpoint != "/" && part.fstype != FsType::Swap && !part.mountpoint.is_empty() { if part.mountpoint != "/" && part.fstype != FsType::Swap && !part.mountpoint.is_empty() && !part.delete {
mount_items.push(MountItem { mount_items.push(MountItem {
device: self.device_path(part), device: self.device_path(part),
mountpoint: part.mountpoint.clone(), mountpoint: part.mountpoint.clone(),
@@ -747,7 +887,7 @@ impl Job for FstabFromCustomPartitionsJob {
); );
for part in &self.custom_partitions { for part in &self.custom_partitions {
if part.mountpoint.is_empty() { continue; } if part.mountpoint.is_empty() || part.delete { continue; }
let is_encrypted = part.encrypt && !part.luks_password.is_empty(); let is_encrypted = part.encrypt && !part.luks_password.is_empty();
+706 -180
View File
File diff suppressed because it is too large Load Diff
+55 -15
View File
@@ -43,15 +43,25 @@ impl InstallerStep for SummaryStep {
}; };
summary_row(ui, t!("summary_keyboard"), &kb); summary_row(ui, t!("summary_keyboard"), &kb);
let disk_str = match &state.selected_disk { let disk_str = if state.erase_disk {
Some(d) => { match &state.selected_disk {
if state.erase_disk { Some(d) => format!("{} \u{EA6B} {}", d, t!("summary_erase_mode")),
format!("{} \u{EA6B} {}", d, t!("summary_erase_mode")) None => t!("summary_not_selected").to_string(),
} else { }
format!("{} \u{EA6B} {}", d, t!("summary_manual_mode")) } else if !state.custom_partitions.is_empty() {
// Manuel modda tanım yapılan TÜM diskleri listele
let mut disks: Vec<&str> = Vec::new();
for part in &state.custom_partitions {
if !disks.contains(&part.disk.as_str()) {
disks.push(&part.disk);
} }
} }
None => t!("summary_not_selected").to_string(), format!("{} \u{EA6B} {}", disks.join(", "), t!("summary_manual_mode"))
} else {
match &state.selected_disk {
Some(d) => format!("{} \u{EA6B} {}", d, t!("summary_manual_mode")),
None => t!("summary_not_selected").to_string(),
}
}; };
summary_row(ui, t!("summary_disk"), &disk_str); summary_row(ui, t!("summary_disk"), &disk_str);
@@ -73,15 +83,45 @@ impl InstallerStep for SummaryStep {
} }
} else if !state.custom_partitions.is_empty() { } else if !state.custom_partitions.is_empty() {
let mut plan_lines = Vec::new(); let mut plan_lines = Vec::new();
// Bölümleri ait oldukları diske göre gruplu listele
let mut seen_disks: Vec<&str> = Vec::new();
for part in &state.custom_partitions { for part in &state.custom_partitions {
let size_str = if part.size_mb == 0 { if !seen_disks.contains(&part.disk.as_str()) {
t!("mp_size_remaining").to_string() seen_disks.push(&part.disk);
} else if part.size_mb >= 1024 { }
format!("{:.1} GB", part.size_mb as f64 / 1024.0) }
} else { for d in seen_disks {
format!("{} MB", part.size_mb) plan_lines.push(format!("\u{EDFB} {}", d));
}; for part in state.custom_partitions.iter().filter(|p| p.disk == d) {
plan_lines.push(format!("{} \u{EA6B} {} ({}, {})", part.device, part.mountpoint, part.fstype, size_str)); let size_str = if part.size_mb == 0 {
// Sembolik boyut: kalan kapasiteyi göster
let others: u64 = state.custom_partitions.iter()
.filter(|q| q.disk == part.disk && !q.delete && q.size_mb > 0)
.map(|q| q.size_mb)
.sum();
let cap = state.available_disks.iter()
.find(|dk| dk.name == part.disk)
.map(|dk| dk.size_bytes / 1_048_576)
.unwrap_or(0);
let rem = cap.saturating_sub(others);
if rem >= 1024 {
format!("{:.1} GB", rem as f64 / 1024.0)
} else {
format!("{} MB", rem)
}
} else if part.size_mb >= 1024 {
format!("{:.1} GB", part.size_mb as f64 / 1024.0)
} else {
format!("{} MB", part.size_mb)
};
if part.delete {
plan_lines.push(format!("{} \u{EA6B} {} ({}, {}) \u{ECA1} {}",
part.device, "—", "—", size_str, t!("mp_will_delete")));
} else {
plan_lines.push(format!("{} \u{EA6B} {} ({}, {})",
part.device, part.mountpoint, part.fstype, size_str));
}
}
} }
summary_row(ui, t!("summary_custom_partitions"), &plan_lines.join("\n")); summary_row(ui, t!("summary_custom_partitions"), &plan_lines.join("\n"));
} }