diff --git a/branding_en.toml b/branding_en.toml index a0e15c0..063e8b5 100644 --- a/branding_en.toml +++ b/branding_en.toml @@ -15,7 +15,7 @@ pisi_logo_dark = "assets/pisi-logo-dark.png" pisi_logo_light = "assets/pisi-logo-light.png" # Title of the installation window -title = "PiSi Linux Installation" +title = "YALI Installation Wizard" # Subtitle displayed in the left sidebar subtitle = "YALI" diff --git a/branding_tr.toml b/branding_tr.toml index 4ee45d7..80ddfd0 100644 --- a/branding_tr.toml +++ b/branding_tr.toml @@ -20,7 +20,7 @@ pisi_logo_dark = "assets/pisi-logo-dark.png" pisi_logo_light = "assets/pisi-logo-light.png" # Kurulum penceresinin başlığı (pencere çubuğu) -title = "PiSi Linux Kurulumu" +title = "YALI Kurulum Aracı" # Sol kenar çubuğunda görüntülenen dağıtım ismi subtitle = "YALI" diff --git a/locales/en.toml b/locales/en.toml index 59bd520..5ef8cfb 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -45,6 +45,7 @@ erase_warning = "WARNING: All data on the selected disk will be erased" users = "Users" users_title = "User Account" users_description = "Create a user account for your system." +real_name_label = "Full Name" username_label = "Username" password_label = "Password" password_confirm_label = "Confirm password" @@ -154,6 +155,10 @@ pwd_very_strong = "Very strong" use_same_for_admin = "Use the same password for the administrator (root) account" # ── Checker ─────────────────────────────────────────────── +check_root = "Permissions" +check_root_pass = "Running with root privileges." +check_root_fail = "Not running as root! The installer requires root privileges. Please run with 'sudo yali-rs' or 'pkexec yali-rs'." + check_disk_space = "Disk Space" check_disk_fail = "Maximum disk size is %{size} GB. Minimum 10 GB is required for installation." check_disk_warn = "%{size} GB disk available. 20 GB or more recommended." @@ -248,6 +253,7 @@ mp_error_empty_mountpoint = "Mount point cannot be empty." mp_plan_valid = "✓ Partition plan is valid. You can proceed." mp_select_disk_first = "← Select a disk first." mp_add_dialog_hint = "/ or /home or swap" +mp_select_hint = "Select…" mp_add_btn = "✓ Add" mp_size_hint = "Size (MB, 0=remaining):" mp_delete_confirm_yes = "✓ Yes, Delete" diff --git a/locales/tr.toml b/locales/tr.toml index d314ef5..3a02769 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -45,6 +45,7 @@ erase_warning = "UYARI: Seçilen diskteki tüm veriler silinecektir" users = "Kullanıcı" users_title = "Kullanıcı Bilgileri" users_description = "Sisteminiz için bir kullanıcı hesabı oluşturun." +real_name_label = "Ad Soyad" username_label = "Kullanıcı adı" password_label = "Şifre" password_confirm_label = "Şifre (tekrar)" @@ -86,7 +87,7 @@ retry_install = "Yeniden Dene" finish_next = "Tamamlandı →" # ── Finish ──────────────────────────────────────────────── -finish_description = "PisiLinux başarıyla kuruldu. Şimdi sisteminizi yeniden başlatabilirsiniz." +finish_description = "Pisi GNU/Linux başarıyla kuruldu. Şimdi sisteminizi yeniden başlatabilirsiniz." finish_restart = "🔄 Yeniden Başlat" finish_live = "Canlı Masaüstüne Dön" install_in_progress = "Kurulum devam ediyor, lütfen bekleyin…" @@ -131,6 +132,10 @@ pwd_strong = "Güçlü" pwd_very_strong = "Çok güçlü" # ── Checker ─────────────────────────────────────────────── +check_root = "Yetki" +check_root_pass = "Root yetkisi mevcut." +check_root_fail = "Root yetkisi yok! Kurulum için yönetici (root) olarak çalıştırılmalıdır. Lütfen 'sudo yali-rs' veya 'pkexec yali-rs' ile tekrar deneyin." + check_disk_space = "Disk Alanı" check_disk_fail = "Maksimum disk boyutu %{size} GB. Kurulum için minimum 10 GB gerekli." check_disk_warn = "%{size} GB disk mevcut. 20 GB veya üzeri önerilir." @@ -220,6 +225,7 @@ mp_error_empty_mountpoint = "Bağlama noktası boş olamaz." mp_plan_valid = "✓ Bölüm planı geçerli. İleri tuşuna basabilirsiniz." mp_select_disk_first = "← Önce bir disk seçin." mp_add_dialog_hint = "/ veya /home veya swap" +mp_select_hint = "Seç…" mp_add_btn = "✓ Ekle" mp_size_hint = "Boyut (MB, 0=kalan):" mp_delete_confirm_yes = "✓ Evet, Sil" diff --git a/src/autoinstall/checker.rs b/src/autoinstall/checker.rs index 13a6b56..116d06e 100644 --- a/src/autoinstall/checker.rs +++ b/src/autoinstall/checker.rs @@ -39,6 +39,7 @@ impl CheckResult { pub fn run_all() -> Vec { vec![ + check_root(), check_ram(), check_disk_space(), check_internet(), @@ -234,6 +235,29 @@ fn check_live_env() -> CheckResult { } } +/// Kök kullanıcı (root) yetkisi kontrolü. +/// mount, swapon, parted gibi komutlar root gerektirdiğinden +/// installer'ın root olarak çalıştırılması zorunludur. +fn check_root() -> CheckResult { + let is_root = std::process::Command::new("id") + .arg("-u") + .output() + .ok() + .and_then(|o| { + if o.status.success() { + Some(String::from_utf8_lossy(&o.stdout).trim() == "0") + } else { + None + } + }) + .unwrap_or(false); + if is_root { + CheckResult::pass(t!("check_root"), t!("check_root_pass")) + } else { + CheckResult::fail(t!("check_root"), t!("check_root_fail")) + } +} + /// NVMe ve diğer modern disk arayüzlerinin çekirdek desteğini kontrol eder. fn check_nvme_support() -> CheckResult { // /dev/nvme* cihazları var mı? diff --git a/src/installer.rs b/src/installer.rs index 8d15cb7..f34e297 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -2,6 +2,48 @@ use eframe::egui; use sysinfo::System; use rust_i18n::t; +/// Aktif masaüstü ortamını algılar. +/// Önce `XDG_CURRENT_DESKTOP` ortam değişkenine bakar (normal kullanıcı). +/// sudo ile çalışıldığında env var kaybolur, bu durumda `pgrep` ile +/// çalışan desktop session process'lerini kontrol eder. +pub fn detect_desktop_environment() -> String { + // 1. XDG_CURRENT_DESKTOP ortam değişkeni + if let Ok(de) = std::env::var("XDG_CURRENT_DESKTOP") { + let de = de.trim().to_lowercase(); + if !de.is_empty() { + return de; + } + } + + // 2. sudo ile çalışıldıysa env kaybolur — aktif session process'lerini kontrol et + let de_map: &[(&str, &[&str])] = &[ + ("plasma", &["plasmashell", "startplasma"]), + ("xfce", &["xfce4-session", "xfdesktop"]), + ("gnome", &["gnome-session"]), + ("cinnamon", &["cinnamon-session"]), + ("budgie", &["budgie-wm"]), + ("lxqt", &["lxqt-session"]), + ("mate", &["mate-session"]), + ("lumina", &["lumina-desktop"]), + ("lxde", &["lxsession"]), + ]; + for (de_name, procs) in de_map { + for proc in *procs { + if let Ok(out) = std::process::Command::new("pgrep") + .args(["-x", proc]) + .output() + { + if out.status.success() { + return de_name.to_string(); + } + } + } + } + + // 3. Hiçbiri bulunamazsa varsayılan + "plasma".to_string() +} + /// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur. /// /// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1` @@ -154,6 +196,7 @@ pub struct GlobalState { pub logical_volumes: Vec, pub keyboard_layout: String, pub keyboard_variant: String, + pub real_name: String, pub username: String, pub password: String, pub password_confirm: String, @@ -169,7 +212,6 @@ pub struct GlobalState { pub bootloader_timeout: u32, pub bootloader_password: String, pub kernel_options: String, - pub license_accepted: bool, pub root_password: String, pub root_password_confirm: String, pub display_manager: String, @@ -217,6 +259,7 @@ impl Default for GlobalState { logical_volumes: Vec::new(), keyboard_layout: "tr".to_string(), keyboard_variant: String::new(), + real_name: String::new(), username: String::new(), password: String::new(), password_confirm: String::new(), @@ -232,7 +275,6 @@ impl Default for GlobalState { bootloader_timeout: 5, bootloader_password: String::new(), kernel_options: String::new(), - license_accepted: false, root_password: String::new(), root_password_confirm: String::new(), network_wifi_password: String::new(), @@ -251,17 +293,7 @@ impl Default for GlobalState { selected_package_groups: Vec::new(), display_manager: "sddm".to_string(), autologin: true, - desktop_environment: { - let de = std::env::var("XDG_CURRENT_DESKTOP") - .unwrap_or_default() - .trim() - .to_lowercase(); - if de.is_empty() { - "plasma".to_string() - } else { - de - } - }, + desktop_environment: detect_desktop_environment(), branding_icons: std::collections::HashMap::new(), branding_pisi_dark: String::new(), branding_pisi_light: String::new(), diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index a8c890c..36751c9 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -167,7 +167,8 @@ impl Job for MountBindJob { ("/run", format!("{}/run", mt)), // COMAR/dbus için gerekli ]; for (src, target) in &bind_dirs { - run_cmd("mkdir", &["-p", target]).await?; + tokio::fs::create_dir_all(target).await + .map_err(|e| format!("{} oluşturulamadı: {}", target, e))?; ui.log(format!("mount --bind {} {}", src, target)); run_cmd("mount", &["--bind", src, target]).await?; } @@ -175,7 +176,8 @@ impl Job for MountBindJob { // efivarfs — UEFI kurulumunda grub-install için zorunlu let efivarfs_target = format!("{}/sys/firmware/efi/efivars", mt); if std::path::Path::new("/sys/firmware/efi/efivars").exists() { - run_cmd("mkdir", &["-p", &efivarfs_target]).await?; + tokio::fs::create_dir_all(&efivarfs_target).await + .map_err(|e| format!("{} oluşturulamadı: {}", efivarfs_target, e))?; ui.log(format!("mount --bind /sys/firmware/efi/efivars {}", efivarfs_target)); run_cmd("mount", &["--bind", "/sys/firmware/efi/efivars", &efivarfs_target]).await?; } @@ -308,6 +310,12 @@ impl Job for InstallGrubJob { fn name(&self) -> &str { "job_bootloader" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + // Ana dizinleri oluştur (demo modunda rsync çalışmaz) + let etc_default = format!("{}/etc/default", self.mount); + let _ = tokio::fs::create_dir_all(&etc_default).await; + let grub_d = format!("{}/etc/grub.d", self.mount); + let _ = tokio::fs::create_dir_all(&grub_d).await; + // GRUB şifresi (varsa) — bcrypt hash ile /etc/grub.d/40_custom yazılır if !self.password.is_empty() { ui.log("GRUB şifresi yapılandırılıyor…"); @@ -776,7 +784,8 @@ impl Job for MountPartitionsJob { if self.plan.efi_mb > 0 { let efi_dir = format!("{}/boot/efi", mount); ui.log(format!("mkdir -p {}", efi_dir)); - run_cmd("mkdir", &["-p", &efi_dir]).await?; + tokio::fs::create_dir_all(&efi_dir).await + .map_err(|e| format!("{} oluşturulamadı: {}", efi_dir, e))?; let efi_part = part_path(disk, 1); ui.log(format!("mount {} {}", efi_part, efi_dir)); run_cmd("mount", &[&efi_part, &efi_dir]).await?; @@ -1175,6 +1184,9 @@ impl Job for ConfigureHostnameJob { fn name(&self) -> &str { "job_hostname" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + let etc_dir = format!("{}/etc", self.mount); + let _ = tokio::fs::create_dir_all(&etc_dir).await; + // /etc/hostname let hostname_path = format!("{}/etc/hostname", self.mount); ui.log(format!("Yazılıyor: {}", hostname_path)); @@ -1210,6 +1222,10 @@ impl Job for ConfigureLocaleJob { fn name(&self) -> &str { "job_locale" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + let etc_dir = format!("{}/etc", self.mount); + let _ = tokio::fs::create_dir_all(&etc_dir).await; + let _ = tokio::fs::create_dir_all(format!("{}/etc/env.d", self.mount)).await; + // /etc/locale.conf (systemd-style) let conf_path = format!("{}/etc/locale.conf", self.mount); let conf = format!( @@ -1361,7 +1377,8 @@ impl Job for ConfigureKeyboardJob { // /etc/X11/xorg.conf.d/00-keyboard.conf (X11 için) let x11_dir = format!("{}/etc/X11/xorg.conf.d", self.mount); let x11_path = format!("{}/00-keyboard.conf", x11_dir); - run_cmd("mkdir", &["-p", &x11_dir]).await?; + tokio::fs::create_dir_all(&x11_dir).await + .map_err(|e| format!("{} oluşturulamadı: {}", x11_dir, e))?; let options_line = if self.variant.is_empty() { String::new() @@ -1450,7 +1467,10 @@ impl Job for CreateUserJob { } // sudoers: wheel grubu sudo yetkisi alsın - let sudoers_path = format!("{}/etc/sudoers.d/wheel", self.mount); + let sudoers_dir = format!("{}/etc/sudoers.d", self.mount); + tokio::fs::create_dir_all(&sudoers_dir).await + .map_err(|e| format!("{} oluşturulamadı: {}", sudoers_dir, e))?; + let sudoers_path = format!("{}/wheel", sudoers_dir); ui.log(format!("Yazılıyor: {}", sudoers_path)); tokio::fs::write(&sudoers_path, "%wheel ALL=(ALL:ALL) ALL\n") .await @@ -1510,6 +1530,9 @@ impl Job for GenerateFstabJob { fn name(&self) -> &str { "job_fstab" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + let etc_dir = format!("{}/etc", self.mount); + let _ = tokio::fs::create_dir_all(&etc_dir).await; + let fstab_path = format!("{}/etc/fstab", self.mount); ui.log(format!("Yazılıyor: {}", fstab_path)); @@ -1872,7 +1895,8 @@ impl Job for MountCustomPartitionsJob { for item in mount_items { let target_path = format!("{}{}", self.mount, item.mountpoint); ui.log(format!("Bağlama noktası dizini oluşturuluyor: {}", target_path)); - run_cmd("mkdir", &["-p", &target_path]).await?; + tokio::fs::create_dir_all(&target_path).await + .map_err(|e| format!("{} oluşturulamadı: {}", target_path, e))?; ui.log(format!("Bağlanıyor: {} ➔ {}", item.device, target_path)); run_cmd("mount", &[&item.device, &target_path]).await?; } @@ -1893,6 +1917,9 @@ impl Job for FstabFromCustomPartitionsJob { fn name(&self) -> &str { "job_fstab" } async fn run(&self, ui: &UiSender) -> Result<(), String> { + let etc_dir = format!("{}/etc", self.mount); + let _ = tokio::fs::create_dir_all(&etc_dir).await; + let fstab_path = format!("{}/etc/fstab", self.mount); ui.log(format!("Yazılıyor: {}", fstab_path)); diff --git a/src/main.rs b/src/main.rs index 09c17c2..4611f5d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ mod autoinstall; mod branding; use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep}; -use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, LicenseStep, NetworkStep, DisplayManagerStep}; +use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep}; use eframe::egui; use rust_i18n::t; @@ -219,7 +219,6 @@ impl Default for YaliApp { steps: vec![ Box::new(WelcomeStep::default()), Box::new(LocationStep::default()), - Box::new(LicenseStep), Box::new(KeyboardStep::default()), Box::new(NetworkStep::default()), Box::new(PartitionStep::default()), diff --git a/src/steps/display_manager.rs b/src/steps/display_manager.rs index 92decef..fe3e2b7 100644 --- a/src/steps/display_manager.rs +++ b/src/steps/display_manager.rs @@ -3,6 +3,7 @@ use rust_i18n::t; use crate::installer::{GlobalState, InstallerStep}; const DM_OPTIONS: &[&str] = &["sddm", "lightdm", "gdm"]; +const DE_OPTIONS: &[&str] = &["plasma", "xfce", "gnome", "cinnamon", "budgie", "lxde", "lxqt", "mate", "lumina"]; #[derive(Default)] pub struct DisplayManagerStep; @@ -13,14 +14,9 @@ impl InstallerStep for DisplayManagerStep { } fn on_enter(&mut self, state: &mut GlobalState) { - let desktop = std::env::var("XDG_CURRENT_DESKTOP") - .unwrap_or_default() - .trim() - .to_lowercase(); + let desktop = crate::installer::detect_desktop_environment(); if !desktop.is_empty() { state.desktop_environment = desktop; - } else if state.desktop_environment.is_empty() { - state.desktop_environment = "plasma".to_string(); } } @@ -48,13 +44,24 @@ impl InstallerStep for DisplayManagerStep { }); ui.end_row(); - // Masaüstü ortamı (otomatik algılanır) + // Masaüstü ortamı (seçilebilir) ui.label(t!("display_manager_de")); - if !state.desktop_environment.is_empty() && state.desktop_environment != "default" { - ui.colored_label(egui::Color32::from_rgb(120, 180, 120), &state.desktop_environment); + let current_de = if state.desktop_environment.is_empty() || state.desktop_environment == "default" { + "plasma" } else { - ui.colored_label(egui::Color32::from_rgb(200, 160, 40), t!("display_manager_de_not_found")); - } + state.desktop_environment.as_str() + }; + egui::ComboBox::from_id_source("de_combo") + .selected_text(current_de) + .show_ui(ui, |ui| { + for de in DE_OPTIONS { + ui.selectable_value( + &mut state.desktop_environment, + de.to_string(), + *de, + ); + } + }); ui.end_row(); // Otomatik oturum açma diff --git a/src/steps/mod.rs b/src/steps/mod.rs index 06f282b..ef0646d 100644 --- a/src/steps/mod.rs +++ b/src/steps/mod.rs @@ -5,7 +5,6 @@ pub mod execution; pub mod finish; pub mod partition; pub mod bootloader; -pub mod license; pub mod network; pub mod rescue; pub mod display_manager; @@ -18,7 +17,6 @@ pub use execution::ExecutionStep; pub use finish::FinishStep; pub use partition::PartitionStep; pub use bootloader::BootloaderStep; -pub use license::LicenseStep; pub use network::NetworkStep; pub use rescue::RescueStep; pub use display_manager::DisplayManagerStep; diff --git a/src/steps/partition.rs b/src/steps/partition.rs index 3ca422d..d255a48 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -172,10 +172,12 @@ use crate::installer::{ }; const MIN_DISK_GB: u64 = 10; -const EFI_MB: u64 = 512; +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 // ───────────────────────────────────────────── @@ -597,8 +599,9 @@ impl PartitionStep { .spacing([12.0, 8.0]) .show(ui, |ui| { - // Dosya sistemi + // Dosya sistemi (boyut önerisi ile) ui.label(t!("mp_fstype")); + let prev_fs = self.manual.add_fstype; egui::ComboBox::from_id_source("fs_combo") .selected_text(FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?")) .show_ui(ui, |ui| { @@ -608,6 +611,15 @@ impl PartitionStep { ); } }); + // FS tipi değişince akıllı boyut öner + if 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.end_row(); // Boyut @@ -615,11 +627,28 @@ impl PartitionStep { ui.text_edit_singleline(&mut self.manual.add_size_str); ui.end_row(); - // Bağlama noktası + // Bağlama noktası (2 aşamalı: combo + input) ui.label(t!("mp_mountpoint")); - let mp_field = egui::TextEdit::singleline(&mut self.manual.add_mountpoint) - .hint_text(t!("mp_add_dialog_hint")); - ui.add(mp_field); + 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() + }; + egui::ComboBox::from_id_source("mp_combo") + .selected_text(¤t_mp) + .width(120.0) + .show_ui(ui, |ui| { + for mp in COMMON_MOUNTPOINTS { + if ui.selectable_label(false, *mp).clicked() { + self.manual.add_mountpoint = mp.to_string(); + } + } + }); + let mp_field = egui::TextEdit::singleline(&mut self.manual.add_mountpoint) + .hint_text(t!("mp_add_dialog_hint")); + ui.add(mp_field); + }); ui.end_row(); // LUKS şifreleme @@ -858,11 +887,28 @@ impl PartitionStep { ui.text_edit_singleline(&mut self.manual.add_lv_size_str); ui.end_row(); - // Bağlama noktası + // Bağlama noktası (2 aşamalı: combo + input) ui.label(t!("mp_mountpoint")); - let mp_field = egui::TextEdit::singleline(&mut self.manual.add_lv_mountpoint) - .hint_text(t!("mp_add_dialog_hint")); - ui.add(mp_field); + 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() + }; + egui::ComboBox::from_id_source("lv_mp_combo") + .selected_text(¤t_mp) + .width(120.0) + .show_ui(ui, |ui| { + for mp in COMMON_MOUNTPOINTS { + if ui.selectable_label(false, *mp).clicked() { + self.manual.add_lv_mountpoint = mp.to_string(); + } + } + }); + 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(); }); diff --git a/src/steps/users.rs b/src/steps/users.rs index c4a0a26..3178e9b 100644 --- a/src/steps/users.rs +++ b/src/steps/users.rs @@ -23,6 +23,33 @@ impl Default for UsersStep { } } +/// Gerçek addan geçerli bir Linux kullanıcı adı türetir. +/// - Küçük harfe çevirir +/// - Türkçe karakterleri ASCII'ye dönüştürür (ş→s, ğ→g, ı→i, ö→o, ü→u, ç→c) +/// - Boşlukları `-`'ye çevirir +/// - Geçersiz karakterleri temizler +fn derive_username(real_name: &str) -> String { + let mut name = real_name.to_lowercase(); + name = name + .replace('ş', "s") + .replace('ğ', "g") + .replace('ı', "i") + .replace('ö', "o") + .replace('ü', "u") + .replace('ç', "c") + .replace(' ', "-") + .replace('_', "-"); + let valid: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect(); + let trimmed = valid.trim_matches('-').to_string(); + if trimmed.is_empty() { + "user".to_string() + } else if trimmed.len() > 32 { + trimmed[..32].trim_end_matches('-').to_string() + } else { + trimmed + } +} + /// Kullanıcı adının geçerliliğini kontrol eder. /// Linux kuralları: küçük harf veya alt çizgi ile başlamalı, /// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli. @@ -75,7 +102,21 @@ impl InstallerStep for UsersStep { } fn on_enter(&mut self, state: &mut GlobalState) { - // Hostname boşsa username'den öner + // Gerçek ad varsa ama kullanıcı adı yoksa türet + if !state.real_name.is_empty() && state.username.is_empty() { + state.username = derive_username(&state.real_name); + } + // Hostname boşsa önce /etc/hostname dene + if state.hostname.is_empty() { + let sys_hostname = std::fs::read_to_string("/etc/hostname") + .unwrap_or_default() + .trim() + .to_string(); + if !sys_hostname.is_empty() && sys_hostname != "localhost" { + state.hostname = sys_hostname; + } + } + // Hâlâ boşsa ve username varsa username-pc öner if state.hostname.is_empty() && !state.username.is_empty() { state.hostname = format!("{}-pc", state.username); } @@ -99,23 +140,37 @@ impl InstallerStep for UsersStep { .inner_margin(egui::Margin::same(12.0)) .show(ui, |ui| { ui.set_width(ui.available_width()); - egui::Grid::new("users_grid") - .num_columns(2) - .spacing([12.0, 10.0]) - .show(ui, |ui| { - // Kullanıcı adı - ui.label(egui::RichText::new(t!("username_label")).strong().color(theme::c_text())); - ui.horizontal(|ui| { - ui.text_edit_singleline(&mut state.username); - if !state.username.is_empty() { - if is_valid_username(&state.username) { - ui.colored_label(theme::c_success(), "✓"); - } else { - ui.colored_label(theme::c_error(), "✗"); + egui::Grid::new("users_grid") + .num_columns(2) + .spacing([12.0, 10.0]) + .show(ui, |ui| { + // Gerçek ad + ui.label(egui::RichText::new(t!("real_name_label")).strong().color(theme::c_text())); + ui.horizontal(|ui| { + let prev = state.real_name.clone(); + ui.text_edit_singleline(&mut state.real_name); + if state.real_name != prev && !state.real_name.is_empty() { + state.username = derive_username(&state.real_name); } - } - }); - ui.end_row(); + if !state.real_name.is_empty() { + ui.colored_label(theme::c_success(), "✓"); + } + }); + ui.end_row(); + + // Kullanıcı adı (otomatik türetilir, düzenlenebilir) + ui.label(egui::RichText::new(t!("username_label")).strong().color(theme::c_text())); + ui.horizontal(|ui| { + ui.text_edit_singleline(&mut state.username); + if !state.username.is_empty() { + if is_valid_username(&state.username) { + ui.colored_label(theme::c_success(), "✓"); + } else { + ui.colored_label(theme::c_error(), "✗"); + } + } + }); + ui.end_row(); // Şifre ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text())); @@ -159,9 +214,6 @@ impl InstallerStep for UsersStep { // Hostname ui.label(egui::RichText::new(t!("hostname_label")).strong().color(theme::c_text())); ui.horizontal(|ui| { - if state.hostname.is_empty() && !state.username.is_empty() { - state.hostname = format!("{}-pc", state.username); - } ui.text_edit_singleline(&mut state.hostname); if !state.hostname.is_empty() { if is_valid_hostname(&state.hostname) { diff --git a/yali-rs b/yali-rs deleted file mode 100644 index 04564bd..0000000 Binary files a/yali-rs and /dev/null differ