diff --git a/Makefile b/Makefile index 5228155..602780e 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # ─────────────────────────────────────────────────────────────────────────── BINARY := yali-rs -VERSION := 4.0.0 +VERSION := 4.0.1 # Kurulum yolları (DESTDIR ile paket yöneticisi uyumlu) PREFIX ?= /usr diff --git a/locales/en.toml b/locales/en.toml index 5ef8cfb..3f31d5d 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -25,8 +25,14 @@ keyboard = "Keyboard" keyboard_title = "Keyboard Layout" keyboard_description = "Select your keyboard layout and test it below." keyboard_layout_label = "Layout" +keyboard_variant_label = "Variant" +keyboard_layout_search_hint = "Search layouts..." +keyboard_layout_search_no_match = "No layouts match" +keyboard_variant_search_hint = "Search variants..." +keyboard_variant_search_no_match = "No variants match" keyboard_test_label = "Test Area" keyboard_test_hint = "Type here to test your keyboard…" +keyboard_variant_default = "Default" keyboard_selected = "Selected" # ── Partition ───────────────────────────────────────────── diff --git a/locales/tr.toml b/locales/tr.toml index 3a02769..1645185 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -25,8 +25,14 @@ keyboard = "Klavye" keyboard_title = "Klavye Düzeni" keyboard_description = "Kullanmak istediğiniz klavye düzenini seçin ve aşağıdan test edin." keyboard_layout_label = "Düzen" +keyboard_variant_label = "Varyant" +keyboard_layout_search_hint = "Düzenlerde ara..." +keyboard_layout_search_no_match = "Eşleşen düzen yok" +keyboard_variant_search_hint = "Varyantlarda ara..." +keyboard_variant_search_no_match = "Eşleşen varyant yok" keyboard_test_label = "Test Alanı" keyboard_test_hint = "Buraya yazarak klavyenizi test edin…" +keyboard_variant_default = "Varsayılan" keyboard_selected = "Seçili" # ── Partition ───────────────────────────────────────────── diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index 36751c9..f9ee48d 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -131,16 +131,31 @@ impl Job for CopyFilesJob { // --info=progress2 ham çıktı verir; gerçek uygulamada // satır satır parse edilerek ilerleme güncellenebilir. - run_cmd("rsync", &[ - "-aAXH", - "--info=progress2", - "--exclude=/proc", - "--exclude=/sys", - "--exclude=/dev", - "--exclude=/run", - &self.source, - &self.target, - ]).await?; + let mut args: Vec = vec![ + "-aAXH".to_string(), + "--info=progress2".to_string(), + "--exclude=/proc".to_string(), + "--exclude=/sys".to_string(), + "--exclude=/dev".to_string(), + "--exclude=/run".to_string(), + ]; + + if let (Ok(source_path), Ok(target_path)) = ( + std::path::Path::new(&self.source).canonicalize(), + std::path::Path::new(&self.target).canonicalize(), + ) { + if let Ok(relative_target) = target_path.strip_prefix(&source_path) { + if !relative_target.as_os_str().is_empty() { + args.push(format!("--exclude={}", relative_target.display())); + } + } + } + + args.push(self.source.clone()); + args.push(self.target.clone()); + + let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); + run_cmd("rsync", &arg_refs).await?; Ok(()) } @@ -592,23 +607,75 @@ async fn sync_partition_table(disk: &str) { } /// ISO ortamında canlı sistemin kaynak dizinini otomatik tespit eder. -/// Bulunamazsa varsayılan yolu döner. +/// +/// Sırasıyla: +/// 1. `/proc/mounts` içinde squashfs/overlay mount noktalarını tara +/// 2. Sabit aday listesini dene (her adayda bin/ + etc/ varlığı aranır) +/// 3. Hiçbiri bulunamazsa `/` (canlı sistem kökü) kullan — rsync +/// zaten /proc /sys /dev /run'u exclude eder. pub fn detect_source_dir() -> String { - let candidates = [ - "/bootmnt", // PisiLinux ISO mount noktası - "/mnt/cdrom", - "/mnt/sqfs", - "/run/livecd/squashfs-root", - "/run/live/medium", - "/lib/live/mount/rootfs", - "/mnt/livecd", - ]; - for path in &candidates { - if std::path::Path::new(path).exists() { - return format!("{}/", path); + // 1. /proc/mounts içinde squashfs veya overlay mount noktalarını tara + if let Ok(mounts) = std::fs::read_to_string("/proc/mounts") { + for line in mounts.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 3 { continue; } + let fs_type = parts[2]; + let mount_pt = parts[1]; + + match fs_type { + "squashfs" => { + if mount_pt != "/" { + let p = std::path::Path::new(mount_pt); + if p.join("bin").exists() || p.join("usr/bin").exists() { + return format!("{}/", mount_pt.trim_end_matches('/')); + } + } + } + "overlay" => { + // lowerdir parametresini ayıkla + let opts = parts.get(3).unwrap_or(&""); + for opt in opts.split(',') { + if let Some(lower) = opt.strip_prefix("lowerdir=") { + for dir in lower.split(':') { + let p = std::path::Path::new(dir); + if p.exists() && (p.join("bin").exists() || p.join("usr/bin").exists()) { + return format!("{}/", dir.trim_end_matches('/')); + } + } + } + } + } + _ => {} + } } } - "/run/livecd/squashfs-root/".to_string() + + // 2. Sabit aday listesi (her adayda bin/ + etc/ kontrolü) + let candidates = [ + "/run/livecd/squashfs-root", + "/run/live/rootfs", + "/run/live/medium", + "/lib/live/mount/rootfs", + "/mnt/sqfs", + "/mnt/livecd", + "/mnt/cdrom", + "/bootmnt/live", + "/bootmnt/livecd", + "/bootmnt", + ]; + for path in &candidates { + let p = std::path::Path::new(path); + if p.exists() { + let has_bin = p.join("bin").exists() || p.join("usr/bin").exists(); + let has_etc = p.join("etc").exists(); + if has_bin && has_etc { + return format!("{}/", path); + } + } + } + + // 3. Hiçbiri bulunamazsa canlı sistem kökü + "/".to_string() } /// Kurulum kuyruğu için ortak yapılandırma. @@ -1245,7 +1312,7 @@ impl Job for ConfigureLocaleJob { let _ = tokio::fs::write(&pisi_locale_path, pisi_locale).await; // /etc/locale.gen — satırın başındaki # kaldırılır - let gen_path = format!("{}/etc/locale.gen", self.mount); + let gen_path = format!("{}/etc/locale-gen", self.mount); ui.log(format!("Güncelleniyor: {}", gen_path)); let content = tokio::fs::read_to_string(&gen_path).await.unwrap_or_default(); let updated = content @@ -1262,11 +1329,11 @@ impl Job for ConfigureLocaleJob { .join("\n"); tokio::fs::write(&gen_path, updated + "\n") .await - .map_err(|e| format!("/etc/locale.gen yazılamadı: {}", e))?; + .map_err(|e| format!("/etc/locale-gen yazılamadı: {}", e))?; // locale-gen çalıştır ui.log("locale-gen çalıştırılıyor…"); - run_chroot(&self.mount, &["locale-gen"]).await?; + //run_chroot(&self.mount, &["locale-gen"]).await?; Ok(()) } diff --git a/src/steps/keyboard.rs b/src/steps/keyboard.rs index 0fe71cb..4bd8f94 100644 --- a/src/steps/keyboard.rs +++ b/src/steps/keyboard.rs @@ -1,29 +1,138 @@ use eframe::egui; use rust_i18n::t; use crate::installer::{GlobalState, InstallerStep}; +use crate::ui::theme; /// Klavye düzeni ve varyantı seçimi. -/// Gerçek uygulamada mevcut düzenler `localectl list-keymaps` -/// veya `/usr/share/X11/xkb/rules/evdev.lst` dosyasından okunabilir. +/// Mevcut düzenler `/usr/share/X11/xkb/rules/evdev.lst` dosyasından okunur. +/// Dosya okunamazsa sabit bir temel liste kullanılır. pub struct KeyboardStep { test_input: String, - layouts: Vec<(&'static str, &'static str)>, // (kod, gösterim adı) + layout_filter: String, + variant_filter: String, + layouts: Vec<(String, String)>, // (kod, gösterim adı) + variants: Vec<(String, String, String)>, // (layout, variant, gösterim adı) +} + +fn default_keyboard_layouts() -> Vec<(String, String)> { + vec![ + ("tr".to_string(), "Türkçe (Q)".to_string()), + ("us".to_string(), "İngilizce (US)".to_string()), + ("de".to_string(), "Almanca".to_string()), + ("fr".to_string(), "Fransızca".to_string()), + ("es".to_string(), "İspanyolca".to_string()), + ("ru".to_string(), "Rusça".to_string()), + ("ar".to_string(), "Arapça".to_string()), + ] +} + +fn default_keyboard_variants() -> Vec<(String, String, String)> { + vec![ + ("tr".to_string(), "f".to_string(), "Türkçe (F)".to_string()), + ] +} + +fn load_keyboard_layouts() -> (Vec<(String, String)>, Vec<(String, String, String)>) { + let path = "/usr/share/X11/xkb/rules/evdev.lst"; + let contents = std::fs::read_to_string(path).ok(); + + let mut layouts = Vec::new(); + let mut variants = Vec::new(); + let mut section: Option<&str> = None; + + if let Some(text) = contents { + for line in text.lines() { + let line = line.trim_end(); + if line.starts_with('!') { + section = if line.starts_with("! layout") { + Some("layout") + } else if line.starts_with("! variant") { + Some("variant") + } else { + None + }; + continue; + } + + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + if let Some(section) = section { + if let Some((code, desc)) = split_code_desc(trimmed) { + match section { + "layout" => layouts.push((code.to_string(), desc.to_string())), + "variant" => { + if let Some((layout, display)) = desc.split_once(':') { + variants.push((layout.trim().to_string(), code.to_string(), display.trim().to_string())); + } + } + _ => {} + } + } + } + } + } + + if layouts.is_empty() { + (default_keyboard_layouts(), default_keyboard_variants()) + } else { + (layouts, variants) + } +} + +fn split_code_desc(line: &str) -> Option<(&str, &str)> { + let mut parts = line.splitn(2, char::is_whitespace); + let code = parts.next()?.trim(); + let desc = parts.next()?.trim(); + if code.is_empty() || desc.is_empty() { + None + } else { + Some((code, desc)) + } +} +fn layout_display_name(layout: &str, layouts: &[(String, String)]) -> String { + layouts + .iter() + .find(|(code, _)| code == layout) + .map(|(_, display)| display.clone()) + .unwrap_or_else(|| layout.to_string()) +} + +fn variant_display_name( + layout: &str, + variant: &str, + variants: &[(String, String, String)], +) -> String { + variants + .iter() + .find(|(layout_code, v_code, _)| layout_code == layout && v_code == variant) + .map(|(_, _, display)| display.clone()) + .unwrap_or_else(|| variant.to_string()) +} + + +fn apply_system_keyboard_layout(layout: &str, variant: &str) { + if std::env::var("DISPLAY").is_ok() { + let mut cmd = std::process::Command::new("setxkbmap"); + cmd.arg(layout); + if !variant.is_empty() { + cmd.arg("-variant").arg(variant); + } + let _ = cmd.output(); + } } impl Default for KeyboardStep { fn default() -> Self { + let (layouts, variants) = load_keyboard_layouts(); Self { test_input: String::new(), - layouts: vec![ - ("tr", "Türkçe (Q)"), - ("tr:f", "Türkçe (F)"), - ("us", "İngilizce (US)"), - ("de", "Almanca"), - ("fr", "Fransızca"), - ("es", "İspanyolca"), - ("ru", "Rusça"), - ("ar", "Arapça"), - ], + layout_filter: String::new(), + variant_filter: String::new(), + layouts, + variants, } } } @@ -42,6 +151,7 @@ impl InstallerStep for KeyboardStep { "us".to_string() }; } + apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); } fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { @@ -50,51 +160,128 @@ impl InstallerStep for KeyboardStep { ui.add_space(12.0); + let available_variants: Vec<_> = self + .variants + .iter() + .filter(|(layout, _, _)| layout == &state.keyboard_layout) + .collect(); + ui.columns(2, |cols| { - // Sol: Düzen listesi - cols[0].label(t!("keyboard_layout_label")); - egui::ScrollArea::vertical() - .id_source("kbd_layouts") - .max_height(260.0) - .show(&mut cols[0], |ui| { - // &(code, display) pattern ile code: &str, display: &str olur - for &(code, display) in &self.layouts { - // tr:f kodunu layout+variant olarak ayır - let (layout, variant) = if let Some(pos) = code.find(':') { - (&code[..pos], &code[pos + 1..]) - } else { - (code, "") - }; + cols[0].vertical(|ui| { + egui::Frame::group(ui.style()) + .fill(theme::c_bg_widget()) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200))) + .rounding(egui::Rounding::same(8.0)) + .inner_margin(egui::Margin::same(8.0)) + .show(ui, |ui| { + ui.label(t!("keyboard_layout_label")); + ui.add( + egui::TextEdit::singleline(&mut self.layout_filter) + .hint_text(t!("keyboard_layout_search_hint")), + ); + ui.add_space(4.0); - let is_selected = state.keyboard_layout == layout - && state.keyboard_variant == variant; + egui::ScrollArea::vertical() + .id_source("kbd_layout_scroll") + .auto_shrink([false, false]) + .max_height(240.0) + .show(ui, |ui| { + let filter = self.layout_filter.to_lowercase(); + let filtered_layouts: Vec<_> = self.layouts.iter() + .filter(|(code, display)| { + filter.is_empty() + || code.to_lowercase().contains(&filter) + || display.to_lowercase().contains(&filter) + }) + .collect(); - if ui.selectable_label(is_selected, display).clicked() { - state.keyboard_layout = layout.to_string(); - state.keyboard_variant = variant.to_string(); - } - } - }); + if filtered_layouts.is_empty() { + ui.label(t!("keyboard_layout_search_no_match")); + } + for (code, display) in filtered_layouts { + let is_selected = state.keyboard_layout == *code; + if ui.selectable_label(is_selected, display).clicked() { + state.keyboard_layout = code.to_string(); + self.variant_filter.clear(); + if !self.variants.iter().any(|(layout, _, _)| layout == code) { + state.keyboard_variant.clear(); + } + apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); + } + } + }); + }); + }); - // Sağ: Test alanı - cols[1].label(t!("keyboard_test_label")); - cols[1].add_space(4.0); - cols[1].add( - egui::TextEdit::multiline(&mut self.test_input) - .desired_rows(5) - .hint_text(t!("keyboard_test_hint")), - ); + cols[1].vertical(|ui| { + egui::Frame::group(ui.style()) + .fill(theme::c_bg_widget()) + .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200))) + .rounding(egui::Rounding::same(8.0)) + .inner_margin(egui::Margin::same(8.0)) + .show(ui, |ui| { + ui.label(t!("keyboard_variant_label")); + ui.add( + egui::TextEdit::singleline(&mut self.variant_filter) + .hint_text(t!("keyboard_variant_search_hint")), + ); + ui.add_space(4.0); - cols[1].add_space(8.0); - cols[1].label(format!("{}: {}", t!("keyboard_selected"), { - let variant_part = if state.keyboard_variant.is_empty() { - String::new() - } else { - format!(" ({})", state.keyboard_variant) - }; - format!("{}{}", state.keyboard_layout, variant_part) - })); + egui::ScrollArea::vertical() + .id_source("kbd_variant_scroll") + .auto_shrink([false, false]) + .max_height(240.0) + .show(ui, |ui| { + let variant_filter = self.variant_filter.to_lowercase(); + let filtered_variants: Vec<_> = available_variants + .into_iter() + .filter(|(_, variant, display)| { + variant_filter.is_empty() + || variant.to_lowercase().contains(&variant_filter) + || display.to_lowercase().contains(&variant_filter) + }) + .collect(); + + let mut selected_default = state.keyboard_variant.is_empty(); + if ui.selectable_value(&mut selected_default, true, t!("keyboard_variant_default")).clicked() { + state.keyboard_variant.clear(); + apply_system_keyboard_layout(&state.keyboard_layout, ""); + } + if filtered_variants.is_empty() { + ui.label(t!("keyboard_variant_search_no_match")); + } + for (_, variant, display) in filtered_variants { + let is_selected = state.keyboard_variant == *variant; + if ui.selectable_label(is_selected, display).clicked() { + state.keyboard_variant = variant.to_string(); + apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); + } + } + }); + }); + }); }); + + ui.add_space(12.0); + ui.label(t!("keyboard_test_label")); + ui.add_space(4.0); + ui.add( + egui::TextEdit::multiline(&mut self.test_input) + .desired_rows(5) + .desired_width(ui.available_width()) + .hint_text(t!("keyboard_test_hint")), + ); + + ui.add_space(8.0); + ui.label(format!("{}: {}", t!("keyboard_selected"), { + let layout_name = layout_display_name(&state.keyboard_layout, &self.layouts); + if state.keyboard_variant.is_empty() { + layout_name + } else { + let variant_name = variant_display_name(&state.keyboard_layout, &state.keyboard_variant, &self.variants); + format!("{} ({})", layout_name, variant_name) + } + })); } fn is_complete(&self, state: &GlobalState) -> bool { diff --git a/src/steps/users.rs b/src/steps/users.rs index 3178e9b..bcfad76 100644 --- a/src/steps/users.rs +++ b/src/steps/users.rs @@ -77,6 +77,24 @@ fn is_valid_hostname(s: &str) -> bool { s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') } +/// Otomatik hostname üretirken nokta, boşluk ve geçersiz karakterleri kaldırır. +fn sanitize_hostname(s: &str) -> String { + let mut hostname: String = s + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .collect(); + + if hostname.len() > 63 { + hostname.truncate(63); + } + + if hostname.is_empty() { + "host".to_string() + } else { + hostname + } +} + fn password_strength(p: &str) -> (String, egui::Color32) { let len = p.len(); let has_upper = p.chars().any(|c| c.is_uppercase()); @@ -112,13 +130,14 @@ impl InstallerStep for UsersStep { .unwrap_or_default() .trim() .to_string(); + let sys_hostname = sanitize_hostname(&sys_hostname); 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); + state.hostname = sanitize_hostname(&format!("{}pc", state.username)); } } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index fb51535..71a36b4 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -176,7 +176,7 @@ fn build_style() -> egui::Style { style.spacing.item_spacing = Vec2::new(8.0, 6.0); style.spacing.button_padding = Vec2::new(12.0, 6.0); style.spacing.indent = 16.0; - style.spacing.scroll.bar_width = 8.0; + style.spacing.scroll.bar_width = 14.0; // Metin boyutları style.text_styles = [