From 635c844710b3178be945c6a086cdf29c53d004ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 07:09:39 +0300 Subject: [PATCH 1/8] feat: keyboard layout/variant search with custom combobox, hex color constants, various UI fixes - Custom combobox implementation with TextEdit + Area popup for keyboard layout/variant search - Added color_from_hex() helper, layout_buf/variant_buf fields - Removed egui-dropdown dependency - Popup items 28px -> 32px with gradient background - Dropdown text color changed to pure white - All color constants converted to hex format - KLE SVG rendering improvements - CHANGELOG.md updated --- CHANGELOG.md | 18 +- assets/keyboard-layout.svg | 2040 ++++++++++++++++++++++++++++++++++++ src/installer.rs | 2 +- src/main.rs | 38 +- src/steps/keyboard.rs | 185 +++- src/ui/combobox.rs | 15 +- src/ui/kle_svg.rs | 288 ++--- src/ui/theme.rs | 10 +- 8 files changed, 2348 insertions(+), 248 deletions(-) create mode 100644 assets/keyboard-layout.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f29625..a849229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # Changelog -## [4.0.2] — 2026-06-21 +## [4.0.3] — 2026-06-23 + +### Added +- Aramalı klavye düzeni/varyant seçici: egui `TextEdit` + `Area` popup ile özel implementasyon +- `color_from_hex(hex: u32)` yardımcısı (`combobox_stil.rs`) +- `layout_buf`, `variant_buf` alanları (`KeyboardStep`) + +### Changed +- `egui-dropdown` bağımlılığı kaldırıldı (artık kullanılmıyor) +- Düzen/Varyant seçiciler yan yana → alt alta, etiket genişliği en uzun etikete göre eşitlendi +- Popup öğe yüksekliği 28px → 32px, gradient arkaplan (`draw_gradient_bg`) ile `combobox.rs` stilinde +- `DROPDOWN_TEXT_COLOR` kırık beyaz → tam beyaz (`color_from_hex(0xFFFFFF)`) +- Tüm renk sabitleri hex formatına çevrildi (`color_from_hex`) + +### Fixed +- Popup ilk açılışta eski seçim filtresiyle tüm öğelerin görünmemesi — odaklanınca tampon temizleniyor +- `t!()` dönüş tipi `Cow` → `String` uyumsuzluğu (`keyboard.rs`) ### Changed - Sürgü maksimum değeri artık diskteki kullanılabilir alan kadar (diğer bölümler hariç) diff --git a/assets/keyboard-layout.svg b/assets/keyboard-layout.svg new file mode 100644 index 0000000..edb04a9 --- /dev/null +++ b/assets/keyboard-layout.svg @@ -0,0 +1,2040 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/installer.rs b/src/installer.rs index 60e59ff..e8d1536 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -277,7 +277,7 @@ impl Default for GlobalState { install_log: Vec::new(), install_current_file: None, demo_mode: false, - is_dark: false, + is_dark: true, rescue_mode: false, bootloader_device: String::new(), bootloader_timeout: 5, diff --git a/src/main.rs b/src/main.rs index 250a631..a2d2d34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,8 +48,8 @@ fn run_gui(demo_mode: bool) -> eframe::Result<()> { Box::new(move |cc| { // Görsel yükleyicileri etkinleştir (PNG desteği) egui_extras::install_image_loaders(&cc.egui_ctx); - ui::theme::set_theme_mode(false); - ui::theme::apply(&cc.egui_ctx, false); + ui::theme::set_theme_mode(true); + ui::theme::apply(&cc.egui_ctx, true); let mut app = YaliApp::default(); app.state.demo_mode = demo_mode; Box::new(app) @@ -407,24 +407,24 @@ impl eframe::App for YaliApp { } // Alt kısım: tema değiştirme - // ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { - // ui.add_space(6.0); + ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { + ui.add_space(6.0); - // let theme_icon = if self.state.is_dark { "☀" } else { "🌙" }; - // let theme_label = if self.state.is_dark { "Light" } else { "Dark" }; - // if ui.add( - // egui::Button::new( - // egui::RichText::new(format!("{} {}", theme_icon, theme_label)) - // .size(12.0) - // ) - // .rounding(egui::Rounding::same(6.0)) - // .min_size(egui::Vec2::new(100.0, 28.0)) - // ).clicked() { - // self.state.is_dark = !self.state.is_dark; - // ui::theme::set_theme_mode(self.state.is_dark); - // ui::theme::apply(ctx, self.state.is_dark); - // } - // }); + let theme_icon = if self.state.is_dark { "☀" } else { "🌙" }; + let theme_label = if self.state.is_dark { "Light" } else { "Dark" }; + if ui.add( + egui::Button::new( + egui::RichText::new(format!("{} {}", theme_icon, theme_label)) + .size(12.0) + ) + .rounding(egui::Rounding::same(6.0)) + .min_size(egui::Vec2::new(100.0, 28.0)) + ).clicked() { + self.state.is_dark = !self.state.is_dark; + ui::theme::set_theme_mode(self.state.is_dark); + ui::theme::apply(ctx, self.state.is_dark); + } + }); }); // ── Alt panel: navigasyon ──────────────────────────── diff --git a/src/steps/keyboard.rs b/src/steps/keyboard.rs index b9fb274..1bedb3f 100644 --- a/src/steps/keyboard.rs +++ b/src/steps/keyboard.rs @@ -1,4 +1,4 @@ -use eframe::egui::{self, Color32}; +use eframe::egui; use rust_i18n::t; use crate::installer::{GlobalState, InstallerStep}; use crate::ui::theme; @@ -92,6 +92,7 @@ fn split_code_desc(line: &str) -> Option<(&str, &str)> { Some((code, desc)) } } + fn layout_display_name(layout: &str, layouts: &[(String, String)]) -> String { layouts .iter() @@ -112,7 +113,6 @@ fn variant_display_name( .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"); @@ -141,7 +141,6 @@ impl InstallerStep for KeyboardStep { } fn on_enter(&mut self, state: &mut GlobalState) { - // Varsayılan düzen sistemi diline göre seçilir if state.keyboard_layout.is_empty() { state.keyboard_layout = if state.language == "tr" { "tr".to_string() @@ -162,64 +161,166 @@ impl InstallerStep for KeyboardStep { ui.add_space(12.0); - // ── Klavye görseli (xkbcommon ile çizilir, temaya uyumlu) ─ - if let Some(kle) = crate::ui::kle_svg::compile_layout(&state.keyboard_layout, &state.keyboard_variant) { - let max = egui::Vec2::new(ui.available_width(), ui.available_height().max(100.0)); - crate::ui::kle_svg::draw_layout(ui, &kle, max); - } + // ── Klavye görseli ──────────────────────────────────── + let kle = crate::ui::kle_svg::compile_layout(&state.keyboard_layout, &state.keyboard_variant) + .unwrap_or_else(|| crate::ui::kle_svg::empty_layout()); + let max = egui::Vec2::new(ui.available_width(), ui.available_height().max(100.0)); + crate::ui::kle_svg::draw_layout(ui, &kle, max); ui.add_space(12.0); - // ── Layout ve Varyant ComboBox'ları yan yana ────────────── - let available_variants: Vec<_> = self - .variants - .iter() - .filter(|(layout, _, _)| layout == &state.keyboard_layout) - .collect(); - - let current_variant_name = if state.keyboard_variant.is_empty() { - t!("keyboard_variant_default").to_string() - } else { - let vn = variant_display_name(&state.keyboard_layout, &state.keyboard_variant, &self.variants); - if vn.is_empty() { state.keyboard_variant.clone() } else { vn } - }; - let current_layout_name = layout_display_name(&state.keyboard_layout, &self.layouts); + // ── Label genişlik hesaplama ────────────────────────── + let layout_label = t!("keyboard_layout_label").to_string(); + let variant_label = t!("keyboard_variant_label").to_string(); + let label_w = ui.fonts(|f| { + let measure = |s: &str| -> f32 { + f.layout_no_wrap(s.to_string(), egui::FontId::proportional(14.0), egui::Color32::WHITE) + .size().x + }; + measure(&layout_label).max(measure(&variant_label)).ceil() + }) + 16.0; + // ── Düzen (Layout) ──────────────────────────────────── ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.add_space(6.0); - ui.label(egui::RichText::new(t!("keyboard_layout_label")).strong().color(theme::c_text())); - }); + ui.add_space(6.0); + ui.allocate_ui_with_layout( + egui::Vec2::new(label_w, ui.available_height()), + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + ui.label(egui::RichText::new(&layout_label).strong().color(theme::c_text())); + }, + ); ui.add_space(8.0); - let lw = ui.available_width().min(200.0); - theme::show_combo_box(ui, "kbd_layout", current_layout_name.as_str(), lw, |ui, w| { + + let lw = ui.available_width(); + let current_layout = layout_display_name(&state.keyboard_layout, &self.layouts); + + theme::show_combo_box(ui, "kbd_layout", ¤t_layout, lw, |ui, w| { + let mut pressed_char = None; + ui.input(|i| { + for event in &i.events { + if let egui::Event::Text(text) = event { + pressed_char = Some(text.to_lowercase()); + } + } + }); + for (code, display) in &self.layouts { - if theme::selectable_value(ui, &mut state.keyboard_layout, code.clone(), display.as_str(), w).clicked() { + let is_selected = state.keyboard_layout == *code; + let is_target = if let Some(ref c) = pressed_char { + display.to_lowercase().starts_with(c) + } else { + false + }; + + if is_target { + state.keyboard_layout = code.clone(); if !self.variants.iter().any(|(layout, _, _)| layout == code) { state.keyboard_variant.clear(); } apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); } + + let response = theme::selectable_value(ui, &mut state.keyboard_layout, code.clone(), display.as_str(), w); + + if response.clicked() { + if !self.variants.iter().any(|(layout, _, _)| layout == code) { + state.keyboard_variant.clear(); + } + apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); + } + + if is_target || (is_selected && pressed_char.is_none()) { + response.scroll_to_me(None); + } } }); + }); - ui.add_space(16.0); + ui.add_space(8.0); - ui.vertical(|ui| { - ui.add_space(6.0); - ui.label(egui::RichText::new(t!("keyboard_variant_label")).strong().color(theme::c_text())); - }); + // ── Varyant ─────────────────────────────────────────── + ui.horizontal(|ui| { + ui.add_space(6.0); + ui.allocate_ui_with_layout( + egui::Vec2::new(label_w, ui.available_height()), + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + ui.label(egui::RichText::new(&variant_label).strong().color(theme::c_text())); + }, + ); ui.add_space(8.0); - let vw = ui.available_width().min(200.0); - theme::show_combo_box(ui, "kbd_variant", current_variant_name.as_str(), vw, |ui, w| { - let default_variant = String::new(); - if theme::selectable_value(ui, &mut state.keyboard_variant, default_variant, &t!("keyboard_variant_default"), w).clicked() { + + let vw = ui.available_width(); + let available_variants: Vec<(String, String)> = self + .variants + .iter() + .filter(|(layout, _, _)| layout == &state.keyboard_layout) + .map(|(_, v_code, display)| (v_code.clone(), display.clone())) + .collect(); + + let current_variant = if state.keyboard_variant.is_empty() { + t!("keyboard_variant_default").to_string() + } else { + let vn = variant_display_name(&state.keyboard_layout, &state.keyboard_variant, &self.variants); + if vn.is_empty() { state.keyboard_variant.clone() } else { vn } + }; + let default_label = t!("keyboard_variant_default").to_string(); + + theme::show_combo_box(ui, "kbd_variant", ¤t_variant, vw, |ui, w| { + let mut pressed_char = None; + ui.input(|i| { + for event in &i.events { + if let egui::Event::Text(text) = event { + pressed_char = Some(text.to_lowercase()); + } + } + }); + + // Varsayılan Varyant Elemanı + let is_default_selected = state.keyboard_variant.is_empty(); + let is_default_target = if let Some(ref c) = pressed_char { + default_label.to_lowercase().starts_with(c) + } else { + false + }; + + if is_default_target { + state.keyboard_variant.clear(); apply_system_keyboard_layout(&state.keyboard_layout, ""); } - for (_, variant, display) in &available_variants { - if theme::selectable_value(ui, &mut state.keyboard_variant, variant.clone(), display.as_str(), w).clicked() { + + let def_response = theme::selectable_value(ui, &mut state.keyboard_variant, String::new(), &default_label, w); + if def_response.clicked() { + apply_system_keyboard_layout(&state.keyboard_layout, ""); + } + + if is_default_target || (is_default_selected && pressed_char.is_none()) { + def_response.scroll_to_me(None); + } + + // Diğer Varyantlar + for (v_code, display) in &available_variants { + let is_selected = state.keyboard_variant == *v_code; + let is_target = if let Some(ref c) = pressed_char { + display.to_lowercase().starts_with(c) + } else { + false + }; + + if is_target { + state.keyboard_variant = v_code.clone(); apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); } + + let response = theme::selectable_value(ui, &mut state.keyboard_variant, v_code.clone(), display.as_str(), w); + if response.clicked() { + apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); + } + + if is_target || (is_selected && pressed_char.is_none()) { + response.scroll_to_me(None); + } } }); }); @@ -228,7 +329,7 @@ impl InstallerStep for KeyboardStep { ui.label(t!("keyboard_test_label")); ui.add_space(4.0); egui::Frame::group(ui.style()) - .fill(Color32::from_rgb(255, 255, 255)) + .fill(theme::c_keyboard_bg()) .rounding(6.0) .show(ui, |ui| { ui.add( @@ -254,4 +355,4 @@ impl InstallerStep for KeyboardStep { fn is_complete(&self, state: &GlobalState) -> bool { !state.keyboard_layout.is_empty() } -} +} \ No newline at end of file diff --git a/src/ui/combobox.rs b/src/ui/combobox.rs index 3d240ce..f5b16b9 100644 --- a/src/ui/combobox.rs +++ b/src/ui/combobox.rs @@ -48,13 +48,14 @@ pub fn selectable_value( draw_gradient_bg(ui, rect); - ui.put(rect, |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - ui.add_space(6.0); - ui.colored_label(DROPDOWN_TEXT_COLOR, label); - }) - .response - }); + let painter = ui.painter(); + painter.text( + egui::pos2(rect.left() + 6.0, rect.center().y), + egui::Align2::LEFT_CENTER, + label, + egui::FontId::proportional(14.0), + DROPDOWN_TEXT_COLOR, + ); if response.clicked() { *current = value; diff --git a/src/ui/kle_svg.rs b/src/ui/kle_svg.rs index e9d751c..0f1f586 100644 --- a/src/ui/kle_svg.rs +++ b/src/ui/kle_svg.rs @@ -1,4 +1,4 @@ -use eframe::egui::{self, Rect, Rounding, Stroke, Vec2}; +use eframe::egui::{self, Vec2}; use xkbcommon::xkb; #[derive(Debug, Clone)] @@ -10,36 +10,16 @@ pub struct XkbKey { #[derive(Debug)] pub struct XkbLayout { pub keys: Vec, - pub width: f32, pub height: f32, } -/// ISO‑105 tuşlu klavye için XKB adı → (x, y, w, h) birim cinsinden. -/// x,y ondalıklı olabilir (geniş tuşlar için). +pub fn empty_layout() -> XkbLayout { + XkbLayout { keys: vec![] } +} + fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { - // Satır y offset'leri (birim) let r = |row| row as f32; - // Standart ISO‑105 düzeni. x değerleri kümülatif. match name { - // ── Function row ──────────────── - "ESC" => Some((0.0, r(0), 1.0, 1.0)), - "FK01" => Some((2.0, r(0), 1.0, 1.0)), - "FK02" => Some((3.0, r(0), 1.0, 1.0)), - "FK03" => Some((4.0, r(0), 1.0, 1.0)), - "FK04" => Some((5.0, r(0), 1.0, 1.0)), - "FK05" => Some((6.0, r(0), 1.0, 1.0)), - "FK06" => Some((7.0, r(0), 1.0, 1.0)), - "FK07" => Some((8.0, r(0), 1.0, 1.0)), - "FK08" => Some((9.0, r(0), 1.0, 1.0)), - "FK09" => Some((10.0, r(0), 1.0, 1.0)), - "FK10" => Some((11.0, r(0), 1.0, 1.0)), - "FK11" => Some((12.0, r(0), 1.0, 1.0)), - "FK12" => Some((13.0, r(0), 1.0, 1.0)), - "PRSC" => Some((15.0, r(0), 1.0, 1.0)), - "SCLK" => Some((16.0, r(0), 1.0, 1.0)), - "PAUS" => Some((17.0, r(0), 1.0, 1.0)), - - // ── Number row ───────────────── "TLDE" => Some((0.0, r(1), 1.0, 1.0)), "AE01" => Some((1.0, r(1), 1.0, 1.0)), "AE02" => Some((2.0, r(1), 1.0, 1.0)), @@ -53,9 +33,8 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { "AE10" => Some((10.0, r(1), 1.0, 1.0)), "AE11" => Some((11.0, r(1), 1.0, 1.0)), "AE12" => Some((12.0, r(1), 1.0, 1.0)), - "BKSP" => Some((13.0, r(1), 2.0, 1.0)), + "BKSP" => Some((13.0, r(1), 2.25, 1.0)), - // ── Top alpha row (Q–P) ──────── "TAB" => Some((0.0, r(2), 1.5, 1.0)), "AD01" => Some((1.5, r(2), 1.0, 1.0)), "AD02" => Some((2.5, r(2), 1.0, 1.0)), @@ -69,10 +48,7 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { "AD10" => Some((10.5, r(2), 1.0, 1.0)), "AD11" => Some((11.5, r(2), 1.0, 1.0)), "AD12" => Some((12.5, r(2), 1.0, 1.0)), - "BKSL" => Some((14.0, r(2), 1.5, 1.0)), // ISO: solundaki tuş (\|) - "RTRN" => Some((14.0, r(2), 1.5, 2.0)), // ISO Enter (2 satır) - // ── Home row (A–L) ──────────── "CAPS" => Some((0.0, r(3), 1.75, 1.0)), "AC01" => Some((1.75, r(3), 1.0, 1.0)), "AC02" => Some((2.75, r(3), 1.0, 1.0)), @@ -86,10 +62,10 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { "AC10" => Some((10.75,r(3), 1.0, 1.0)), "AC11" => Some((11.75,r(3), 1.0, 1.0)), "AC12" => Some((12.75,r(3), 1.0, 1.0)), + "BKSL" => Some((12.50,r(3), 1.0, 1.0)), - // ── Bottom row (Z–M) ────────── "LFSH" => Some((0.0, r(4), 1.25, 1.0)), - "LSGT" => Some((1.25, r(4), 1.0, 1.0)), // ISO extra (< > |) + "LSGT" => Some((1.25, r(4), 1.0, 1.0)), "AB01" => Some((2.25, r(4), 1.0, 1.0)), "AB02" => Some((3.25, r(4), 1.0, 1.0)), "AB03" => Some((4.25, r(4), 1.0, 1.0)), @@ -100,61 +76,15 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { "AB08" => Some((9.25, r(4), 1.0, 1.0)), "AB09" => Some((10.25,r(4), 1.0, 1.0)), "AB10" => Some((11.25,r(4), 1.0, 1.0)), - "AB11" => Some((12.25,r(4), 1.0, 1.0)), - "RTSH" => Some((13.25,r(4), 2.75, 1.0)), + "RTSH" => Some((12.25,r(4), 3.0, 1.0)), - // ── Space bar row ───────────── "LCTL" => Some((0.0, r(5), 1.25, 1.0)), "LWIN" => Some((1.25, r(5), 1.25, 1.0)), "LALT" => Some((2.5, r(5), 1.5, 1.0)), "SPCE" => Some((4.0, r(5), 6.25, 1.0)), - "RALT" | "LVL3" => Some((10.25,r(5), 1.5, 1.0)), - "RWIN" => Some((11.75,r(5), 1.25, 1.0)), - "COMP" => Some((13.0, r(5), 1.25, 1.0)), - "RCTL" => Some((14.25,r(5), 1.25, 1.0)), - - // ── Nav cluster ─────────────── - "INS" => Some((15.5, r(2), 1.0, 1.0)), - "HOME" => Some((16.5, r(2), 1.0, 1.0)), - "PGUP" => Some((17.5, r(2), 1.0, 1.0)), - "DELE" => Some((15.5, r(3), 1.0, 1.0)), - "END" => Some((16.5, r(3), 1.0, 1.0)), - "PGDN" => Some((17.5, r(3), 1.0, 1.0)), - "UP" => Some((16.5, r(4), 1.0, 1.0)), - "LEFT" => Some((15.5, r(5), 1.0, 1.0)), - "DOWN" => Some((16.5, r(5), 1.0, 1.0)), - "RGHT" => Some((17.5, r(5), 1.0, 1.0)), - - // ── Numpad ──────────────────── - "NMLK" => Some((19.0, r(2), 1.0, 1.0)), - "KSLU" => Some((20.0, r(2), 1.0, 1.0)), // / - "KAST" => Some((21.0, r(2), 1.0, 1.0)), // * - "KMIN" => Some((22.0, r(2), 1.0, 1.0)), // - - "KP7" => Some((19.0, r(3), 1.0, 1.0)), - "KP8" => Some((20.0, r(3), 1.0, 1.0)), - "KP9" => Some((21.0, r(3), 1.0, 1.0)), - "KPLU" => Some((22.0, r(3), 1.0, 2.0)), // + - "KP4" => Some((19.0, r(4), 1.0, 1.0)), - "KP5" => Some((20.0, r(4), 1.0, 1.0)), - "KP6" => Some((21.0, r(4), 1.0, 1.0)), - "KP1" => Some((19.0, r(5), 1.0, 1.0)), - "KP2" => Some((20.0, r(5), 1.0, 1.0)), - "KP3" => Some((21.0, r(5), 1.0, 1.0)), - "KP0" => Some((19.0, r(6), 2.0, 1.0)), - "KPDL" => Some((21.0, r(6), 1.0, 1.0)), // . - "KPEN" => Some((22.0, r(5), 1.0, 2.0)), // Enter - "K7" => Some((19.0, r(3), 1.0, 1.0)), // old alias - "K8" => Some((20.0, r(3), 1.0, 1.0)), - "K9" => Some((21.0, r(3), 1.0, 1.0)), - "K4" => Some((19.0, r(4), 1.0, 1.0)), - "K5" => Some((20.0, r(4), 1.0, 1.0)), - "K6" => Some((21.0, r(4), 1.0, 1.0)), - "K1" => Some((19.0, r(5), 1.0, 1.0)), - "K2" => Some((20.0, r(5), 1.0, 1.0)), - "K3" => Some((21.0, r(5), 1.0, 1.0)), - "K0" => Some((19.0, r(6), 2.0, 1.0)), - "KDOT" => Some((21.0, r(6), 1.0, 1.0)), - "KENT" => Some((22.0, r(5), 1.0, 2.0)), + "RALT" | "LVL3" => Some((10.25, r(5), 1.5, 1.0)), + "COMP" => Some((11.75, r(5), 1.25, 1.0)), + "RCTL" => Some((13.0, r(5), 2.25, 1.0)), _ => None, } @@ -162,94 +92,76 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { fn keysym_to_label(sym: xkb::Keysym) -> String { let utf = xkb::keysym_to_utf8(sym); - if !utf.is_empty() { return utf; } - - let name = xkb::keysym_get_name(sym); - if name == "VoidSymbol" || name == "NoSymbol" { - return String::new(); + if utf.chars().any(|c| !c.is_control()) { + let trimmed = utf.trim(); + if !trimmed.is_empty() { + return trimmed.to_uppercase(); + } } - - // Yaygın XKB sembol adlarını kısa etiketlere çevir + let name = xkb::keysym_get_name(sym); + if name == "VoidSymbol" || name == "NoSymbol" { return String::new(); } match name.as_str() { - "BackSpace" => "⌫".into(), - "Tab" => "⇥".into(), - "Return" => "↵".into(), - "Caps_Lock" => "⇪".into(), - "Shift_L" => "⇧".into(), - "Shift_R" => "⇧".into(), - "Control_L" => "Ctrl".into(), - "Control_R" => "Ctrl".into(), + "BackSpace" => "Bksp".into(), + "Tab" => "Tab".into(), + "Return" => "Enter".into(), + "Caps_Lock" => "Caps".into(), + "Shift_L" | "Shift_R" => "Shift".into(), + "Control_L" | "Control_R" => "Ctrl".into(), "Alt_L" => "Alt".into(), - "Alt_R" => "AltGr".into(), - "ISO_Level3_Shift" => "AltGr".into(), - "Super_L" => "Win".into(), - "Super_R" => "Win".into(), + "Alt_R" | "ISO_Level3_Shift" => "AltGr".into(), + "Super_L" | "Super_R" => "Win".into(), "Menu" => "Menü".into(), "Escape" => "Esc".into(), - "Delete" => "Del".into(), - "Insert" => "Ins".into(), - "Prior" => "PgUp".into(), - "Next" => "PgDn".into(), "space" => "".into(), - "Multi_key" => "Compose".into(), - _ if name.starts_with("XF86") => String::new(), _ => name, } } -/// XKB keymap'inden klavye düzenini oluşturur. pub fn compile_layout(layout: &str, variant: &str) -> Option { - let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); - let model = if std::path::Path::new("/usr/share/X11/xkb/symbols/").exists() { - "pc105" - } else { - "pc105" - }; + let mut context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + context.set_log_level(xkb::LogLevel::Critical); let keymap = xkb::Keymap::new_from_names::( - &context, - "", // rules - model, // model - layout, // layout - if variant.is_empty() { "" } else { variant }, // variant - None::, // options - xkb::COMPILE_NO_FLAGS, + &context, "", "pc105", layout, + if variant.is_empty() { "" } else { variant }, + None::, xkb::COMPILE_NO_FLAGS, )?; let min = keymap.min_keycode(); let max = keymap.max_keycode(); let mut keys = Vec::new(); - let mut min_x = f32::MAX; + let mut min_y = f32::MAX; - let mut max_x = f32::MIN; - let mut max_y = f32::MIN; + + let mut enter_label = "Enter".to_string(); for code_u32 in (min.raw() as u32)..=(max.raw() as u32) { let code = xkb::Keycode::new(code_u32); let Some(name) = keymap.key_get_name(code) else { continue }; + + if name == "RTRN" { + let syms = keymap.key_get_syms_by_level(code, 0, 0); + let label = syms.iter().map(|&s| keysym_to_label(s)).collect::>().join(""); + if !label.is_empty() { enter_label = label; } + continue; + } + let Some((x, y, w, h)) = key_position(name) else { continue }; - // Sembolleri al (base, shift, altgr, shift+altgr), boş olanları atla let mut raw_labels = Vec::new(); for level in 0..4 { let syms = keymap.key_get_syms_by_level(code, 0, level); - let label = syms.iter() - .map(|&s| keysym_to_label(s)) - .collect::>() - .join(""); + let label = syms.iter().map(|&s| keysym_to_label(s)).collect::>().join(""); if !label.is_empty() && !raw_labels.contains(&label) { raw_labels.push(label); } } - // Her tuşta sadece 1 karakter göster (büyük harf tercihli) let labels: Vec = { let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect(); if single.is_empty() { - // Hiç tek karakter yoksa ilk label'ı göster (F1, Esc, …) raw_labels.into_iter().take(1).collect() } else { - // Büyük harf varsa onu göster, yoksa ilk tek karakteri let uc = single.iter().find(|l| l.chars().next().map_or(false, |c| c.is_uppercase())); vec![if let Some(s) = uc { (*s).clone() } else { (*single[0]).clone() }] } @@ -257,78 +169,102 @@ pub fn compile_layout(layout: &str, variant: &str) -> Option { keys.push(XkbKey { x, y, w, h, labels }); - if x < min_x { min_x = x; } if y < min_y { min_y = y; } - if x + w > max_x { max_x = x + w; } - if y + h > max_y { max_y = y + h; } } - // normalize x,y — tüm tuşlar (0,0) bazlı olsun + keys.push(XkbKey { x: 13.5, y: 2.0, w: 1.75, h: 1.0, labels: vec![enter_label.clone()] }); + keys.push(XkbKey { x: 14.75, y: 3.0, w: 0.5, h: 1.0, labels: vec!["_RTRN_BOT".to_string()] }); + for key in &mut keys { - key.x -= min_x; key.y -= min_y; } Some(XkbLayout { keys, - width: max_x - min_x, - height: max_y - min_y, }) } -/// Klavyeyi egui ile çizer. pub fn draw_layout(ui: &mut egui::Ui, layout: &XkbLayout, max_size: Vec2) { let margin = 6.0; - let spacing = 0.08; - let corner = 3.0; - let uw = (max_size.x - margin * 2.0) / layout.width; - let uh = (max_size.y - margin * 2.0) / layout.height; - let unit = uw.min(uh).max(12.0); + let svg_size = Vec2::new(840.0, 300.0); + let img_scale = ((max_size.x - margin * 2.0) / svg_size.x) + .min((max_size.y - margin * 2.0) / svg_size.y) + .max(0.1); + let draw_size = svg_size * img_scale; + let total_size = Vec2::new(draw_size.x + margin * 2.0, draw_size.y + margin * 2.0); - let draw_w = layout.width * unit; - let draw_h = layout.height * unit; - - let (_id, resp) = ui.allocate_exact_size(Vec2::new(draw_w + margin * 2.0, draw_h + margin * 2.0), egui::Sense::hover()); + let (_id, resp) = ui.allocate_exact_size(total_size, egui::Sense::hover()); let painter = ui.painter_at(resp.rect); - let origin = resp.rect.min; + let origin = egui::pos2(resp.rect.min.x + margin, resp.rect.min.y + margin); let visuals = ui.visuals(); - let key_fill = visuals.widgets.inactive.bg_fill; - let key_stroke = visuals.widgets.noninteractive.bg_stroke; - let text_color = visuals.text_color(); let bg = visuals.window_fill(); + let is_dark = visuals.dark_mode; - painter.rect_filled(resp.rect, Rounding::same(6.0), bg); + painter.rect_filled(resp.rect, egui::Rounding::same(6.0), bg); - let font_size = (unit * 0.28).max(6.0); - let small_size = (unit * 0.22).max(5.0); + let img_rect = egui::Rect::from_min_size(origin, draw_size); + egui::Image::from_bytes( + "keyboard.svg", + &include_bytes!("../../assets/keyboard-layout.svg")[..], + ).paint_at(ui, img_rect); - let sp = unit * spacing; + // Dark mode: SVG'yi karart + if is_dark { + painter.rect_filled(img_rect, egui::Rounding::same(4.0), egui::Color32::from_black_alpha(170)); + } + + let font_size = (draw_size.x * 0.015).max(6.0); + + let text_color = if is_dark { egui::Color32::from_gray(230) } else { egui::Color32::from_gray(30) }; + + const PITCH: f32 = 54.0; + const KEY_PX: f32 = 52.0; + const CX0: f32 = 42.0; + const CY0: f32 = 42.0; + + const ENTER_VBX: f32 = 791.0; + const ENTER_VBY: f32 = 123.0; + + let mut enter_text = String::new(); for key in &layout.keys { - let kx = origin.x + margin + key.x * unit + sp; - let ky = origin.y + margin + key.y * unit + sp; - let kw = key.w * unit - sp * 2.0; - let kh = key.h * unit - sp * 2.0; + if let Some(lbl) = key.labels.first() { + if lbl == "_RTRN_BOT" { continue; } + if key.y == 1.0 && (key.x - 13.5).abs() < 0.1 { + enter_text = lbl.clone(); + continue; + } + } - if kw <= 0.0 || kh <= 0.0 { continue; } + let svg_cx = CX0 + key.x * PITCH; + let svg_cy = CY0 + key.y * PITCH; + let svg_w = KEY_PX + (key.w - 1.0) * PITCH; + let svg_h = KEY_PX + (key.h - 1.0) * PITCH; - let key_rect = Rect::from_min_size(egui::pos2(kx, ky), Vec2::new(kw, kh)); - painter.rect(key_rect, Rounding::same(corner), key_fill, Stroke::new(key_stroke.width.max(0.5), key_stroke.color)); + let ex = origin.x + (svg_cx - svg_w / 2.0) * img_scale; + let ey = origin.y + (svg_cy - svg_h / 2.0) * img_scale; + let ew = svg_w * img_scale; + let eh = svg_h * img_scale; - for (i, label) in key.labels.iter().enumerate() { - let lbl = label.trim(); - if lbl.is_empty() { continue; } - let (lx, ly, sz) = match i { - 0 => (kx + kw * 0.5, ky + kh * 0.3, font_size), - 1 => (kx + kw * 0.5, ky + kh * 0.65, small_size), - _ => (kx + kw * 0.5, ky + kh * 0.9, small_size), - }; - painter.text( - egui::pos2(lx, ly), egui::Align2::CENTER_CENTER, - lbl, egui::FontId::proportional(sz), text_color, - ); + if let Some(lbl) = key.labels.first() { + let trimmed = lbl.trim(); + if trimmed.is_empty() { continue; } + + let text_pos = egui::pos2(ex + ew / 2.0, ey + eh / 2.0); + + painter.text(text_pos, egui::Align2::CENTER_CENTER, trimmed, egui::FontId::proportional(font_size), text_color); } } + + if !enter_text.is_empty() { + painter.text( + egui::pos2(origin.x + ENTER_VBX * img_scale, origin.y + ENTER_VBY * img_scale), + egui::Align2::CENTER_CENTER, + &enter_text, + egui::FontId::proportional(font_size), + text_color, + ); + } } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index b52802e..a99ff35 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -15,6 +15,7 @@ use std::sync::atomic::{AtomicU32, Ordering}; static BG_DARK: AtomicU32 = AtomicU32::new(0x121214); static BG_PANEL: AtomicU32 = AtomicU32::new(0x1A1A1E); static BG_WIDGET: AtomicU32 = AtomicU32::new(0x242428); +static BG_FOOTER: AtomicU32 = AtomicU32::new(0x121214); static BORDER: AtomicU32 = AtomicU32::new(0x323238); @@ -26,6 +27,7 @@ static ERROR: AtomicU32 = AtomicU32::new(0xF44336); static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D); static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b) static SIDEBAR_TEXT: AtomicU32 = AtomicU32::new(0x000000); +static KEYBOARD_BG: AtomicU32 = AtomicU32::new(0xFFFFFF); @@ -46,11 +48,13 @@ pub fn c_warning() -> Color32 { color_from_hex(WARNING.load(Ordering::Relaxed) pub fn c_error() -> Color32 { color_from_hex(ERROR.load(Ordering::Relaxed)) } pub fn c_sidebar() -> Color32 { color_from_hex(SIDEBAR.load(Ordering::Relaxed)) } pub fn c_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering::Relaxed)) } -pub fn c_footer_bg() -> Color32 { color_from_hex(0xf7f7f7) } +pub fn c_footer_bg() -> Color32 { color_from_hex(BG_FOOTER.load(Ordering::Relaxed)) } pub fn c_root_bg() -> Color32 { color_from_hex(0xf7f7f7) } +pub fn c_keyboard_bg() -> Color32 { color_from_hex(KEYBOARD_BG.load(Ordering::Relaxed)) } pub fn set_theme_mode(is_dark: bool) { if is_dark { + BG_FOOTER.store(0x121214, Ordering::Relaxed); BG_DARK.store(0x121214, Ordering::Relaxed); BG_PANEL.store(0x1A1A1E, Ordering::Relaxed); BG_WIDGET.store(0x242428, Ordering::Relaxed); @@ -58,13 +62,15 @@ pub fn set_theme_mode(is_dark: bool) { TEXT.store(0xF3F3F5, Ordering::Relaxed); TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed); SIDEBAR.store(0x0B0B0D, Ordering::Relaxed); + KEYBOARD_BG.store(0x000000, Ordering::Relaxed); } else { + BG_FOOTER.store(0xf7f7f7, Ordering::Relaxed); BG_DARK.store(0xf7f7f7, Ordering::Relaxed); SIDEBAR.store(0x292f34, Ordering::Relaxed); TEXT.store(0x000000, Ordering::Relaxed); TEXT_DIM.store(0x000000, Ordering::Relaxed); SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed); - + KEYBOARD_BG.store(0xffffff, Ordering::Relaxed); // BG_DARK.store(0xF0F0F5, Ordering::Relaxed); // BG_PANEL.store(0xFFFFFF, Ordering::Relaxed); From 5e66e151bd6dcb030cb4ca3782c317db15b6a7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 07:18:41 +0300 Subject: [PATCH 2/8] fix: make user form and theme colors dark/light theme aware - users.rs: hardcoded form fill replaced with c_bg_widget() - theme.rs: c_root_bg() now uses theme-aware BG_ROOT atomic - theme.rs: set_theme_mode() now sets BG_PANEL, BG_WIDGET, BORDER for light mode - theme.rs: build_visuals() inactive/hovered/active fills are now theme-dependent --- src/steps/users.rs | 2 +- src/ui/kle_svg copy.rs | 302 +++++++++++++++++++++++++++++++++++++++++ src/ui/theme.rs | 25 ++-- 3 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 src/ui/kle_svg copy.rs diff --git a/src/steps/users.rs b/src/steps/users.rs index 83550b0..dc6ded2 100644 --- a/src/steps/users.rs +++ b/src/steps/users.rs @@ -155,7 +155,7 @@ impl InstallerStep for UsersStep { // ─── Kullanıcı Bilgileri Formu ────────────────────────────────── egui::Frame::group(ui.style()) - .fill(egui::Color32::from_rgb(240, 243, 198)) + .fill(theme::c_bg_widget()) .rounding(6.0) .inner_margin(egui::Margin::symmetric(10.0, 10.0)) .show(ui, |ui| { diff --git a/src/ui/kle_svg copy.rs b/src/ui/kle_svg copy.rs new file mode 100644 index 0000000..d2d904e --- /dev/null +++ b/src/ui/kle_svg copy.rs @@ -0,0 +1,302 @@ +use eframe::egui::{self, Rect, Rounding, Stroke, Vec2}; +use xkbcommon::xkb; + +#[derive(Debug, Clone)] +pub struct XkbKey { + pub x: f32, pub y: f32, pub w: f32, pub h: f32, + pub labels: Vec, +} + +#[derive(Debug)] +pub struct XkbLayout { + pub keys: Vec, + pub width: f32, pub height: f32, +} + +/// Boş bir klavye düzeni (layout derlenemediğinde gösterilir). +pub fn empty_layout() -> XkbLayout { + XkbLayout { keys: vec![], width: 1.0, height: 1.0 } +} + +/// ISO‑105 tuşlu klavye için XKB adı → (x, y, w, h) birim cinsinden. +/// x,y ondalıklı olabilir (geniş tuşlar için). +fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { + // Satır y offset'leri (birim) + let r = |row| row as f32; + + // Standart ISO‑105 düzeni. x değerleri kümülatif. + match name { + // ── (function row tamamen kaldırıldı) ── + + + // ── Number row ───────────────── + "TLDE" => Some((0.0, r(1), 1.0, 1.0)), + "AE01" => Some((1.0, r(1), 1.0, 1.0)), + "AE02" => Some((2.0, r(1), 1.0, 1.0)), + "AE03" => Some((3.0, r(1), 1.0, 1.0)), + "AE04" => Some((4.0, r(1), 1.0, 1.0)), + "AE05" => Some((5.0, r(1), 1.0, 1.0)), + "AE06" => Some((6.0, r(1), 1.0, 1.0)), + "AE07" => Some((7.0, r(1), 1.0, 1.0)), + "AE08" => Some((8.0, r(1), 1.0, 1.0)), + "AE09" => Some((9.0, r(1), 1.0, 1.0)), + "AE10" => Some((10.0, r(1), 1.0, 1.0)), + "AE11" => Some((11.0, r(1), 1.0, 1.0)), + "AE12" => Some((12.0, r(1), 1.0, 1.0)), + "BKSP" => Some((13.0, r(1), 2.0, 1.0)), + + // ── Top alpha row (Q–P) ──────── + "TAB" => Some((0.0, r(2), 1.5, 1.0)), + "AD01" => Some((1.5, r(2), 1.0, 1.0)), + "AD02" => Some((2.5, r(2), 1.0, 1.0)), + "AD03" => Some((3.5, r(2), 1.0, 1.0)), + "AD04" => Some((4.5, r(2), 1.0, 1.0)), + "AD05" => Some((5.5, r(2), 1.0, 1.0)), + "AD06" => Some((6.5, r(2), 1.0, 1.0)), + "AD07" => Some((7.5, r(2), 1.0, 1.0)), + "AD08" => Some((8.5, r(2), 1.0, 1.0)), + "AD09" => Some((9.5, r(2), 1.0, 1.0)), + "AD10" => Some((10.5, r(2), 1.0, 1.0)), + "AD11" => Some((11.5, r(2), 1.0, 1.0)), + "AD12" => Some((12.5, r(2), 1.0, 1.0)), + "BKSL" => Some((13.5, r(2), 1.0, 1.0)), // ISO: solundaki tuş (\|) + "RTRN" => Some((14.5, r(2), 1.25, 2.0)), // ISO Enter (2 satır) + + // ── Home row (A–L) ──────────── + "CAPS" => Some((0.0, r(3), 1.75, 1.0)), + "AC01" => Some((1.75, r(3), 1.0, 1.0)), + "AC02" => Some((2.75, r(3), 1.0, 1.0)), + "AC03" => Some((3.75, r(3), 1.0, 1.0)), + "AC04" => Some((4.75, r(3), 1.0, 1.0)), + "AC05" => Some((5.75, r(3), 1.0, 1.0)), + "AC06" => Some((6.75, r(3), 1.0, 1.0)), + "AC07" => Some((7.75, r(3), 1.0, 1.0)), + "AC08" => Some((8.75, r(3), 1.0, 1.0)), + "AC09" => Some((9.75, r(3), 1.0, 1.0)), + "AC10" => Some((10.75,r(3), 1.0, 1.0)), + "AC11" => Some((11.75,r(3), 1.0, 1.0)), + "AC12" => Some((12.75,r(3), 1.0, 1.0)), + + // ── Bottom row (Z–M) ────────── + "LFSH" => Some((0.0, r(4), 1.25, 1.0)), + "LSGT" => Some((1.25, r(4), 1.0, 1.0)), // ISO extra (< > |) + "AB01" => Some((2.25, r(4), 1.0, 1.0)), + "AB02" => Some((3.25, r(4), 1.0, 1.0)), + "AB03" => Some((4.25, r(4), 1.0, 1.0)), + "AB04" => Some((5.25, r(4), 1.0, 1.0)), + "AB05" => Some((6.25, r(4), 1.0, 1.0)), + "AB06" => Some((7.25, r(4), 1.0, 1.0)), + "AB07" => Some((8.25, r(4), 1.0, 1.0)), + "AB08" => Some((9.25, r(4), 1.0, 1.0)), + "AB09" => Some((10.25,r(4), 1.0, 1.0)), + "AB10" => Some((11.25,r(4), 1.0, 1.0)), + "RTSH" => Some((12.25,r(4), 3.75, 1.0)), + + // ── Space bar row ───────────── + "LCTL" => Some((0.0, r(5), 1.25, 1.0)), + "LWIN" => Some((1.25, r(5), 1.25, 1.0)), + "LALT" => Some((2.5, r(5), 1.5, 1.0)), + "SPCE" => Some((4.0, r(5), 6.25, 1.0)), + "RALT" | "LVL3" => Some((10.25,r(5), 1.5, 1.0)), + "COMP" => Some((11.75,r(5), 1.25, 1.0)), + "RCTL" => Some((13.0, r(5), 1.25, 1.0)), + + // ── (nav cluster tamamen kaldırıldı) ── + + _ => None, + } +} + +fn keysym_to_label(sym: xkb::Keysym) -> String { + let utf = xkb::keysym_to_utf8(sym); + if utf.chars().any(|c| !c.is_control()) { + let trimmed = utf.trim(); + if !trimmed.is_empty() { + return trimmed.to_string(); + } + } + + let name = xkb::keysym_get_name(sym); + if name == "VoidSymbol" || name == "NoSymbol" { + return String::new(); + } + + // Yaygın XKB sembol adlarını kısa etiketlere çevir + match name.as_str() { + "BackSpace" => "Bksp".into(), + "Tab" => "Tab".into(), + "Return" => "Enter".into(), + "Caps_Lock" => "Caps".into(), + "Shift_L" => "Shift".into(), + "Shift_R" => "Shift".into(), + "Control_L" => "Ctrl".into(), + "Control_R" => "Ctrl".into(), + "Alt_L" => "Alt".into(), + "Alt_R" => "AltGr".into(), + "ISO_Level3_Shift" => "AltGr".into(), + "Super_L" => "Win".into(), + "Super_R" => "Win".into(), + "Menu" => "Menü".into(), + "Escape" => "Esc".into(), + "Delete" => "Del".into(), + "Insert" => "Ins".into(), + "Prior" => "PgUp".into(), + "Next" => "PgDn".into(), + "space" => "".into(), + "Multi_key" => "Compose".into(), + _ if name.starts_with("XF86") => String::new(), + _ => name, + } +} + +/// XKB keymap'inden klavye düzenini oluşturur. +pub fn compile_layout(layout: &str, variant: &str) -> Option { + let mut context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + context.set_log_level(xkb::LogLevel::Critical); + let model = if std::path::Path::new("/usr/share/X11/xkb/symbols/").exists() { + "pc105" + } else { + "pc105" + }; + + let keymap = xkb::Keymap::new_from_names::( + &context, + "", // rules + model, // model + layout, // layout + if variant.is_empty() { "" } else { variant }, // variant + None::, // options + xkb::COMPILE_NO_FLAGS, + )?; + + let min = keymap.min_keycode(); + let max = keymap.max_keycode(); + let mut keys = Vec::new(); + let mut min_x = f32::MAX; + let mut min_y = f32::MAX; + let mut max_x = f32::MIN; + let mut max_y = f32::MIN; + let mut moved_comma = false; + + for code_u32 in (min.raw() as u32)..=(max.raw() as u32) { + let code = xkb::Keycode::new(code_u32); + let Some(name) = keymap.key_get_name(code) else { continue }; + let Some((x, y, w, h)) = key_position(name) else { continue }; + + // Sembolleri al (base, shift, altgr, shift+altgr), boş olanları atla + let mut raw_labels = Vec::new(); + for level in 0..4 { + let syms = keymap.key_get_syms_by_level(code, 0, level); + let label = syms.iter() + .map(|&s| keysym_to_label(s)) + .collect::>() + .join(""); + if !label.is_empty() && !raw_labels.contains(&label) { + raw_labels.push(label); + } + } + + // Her tuşta sadece 1 karakter göster (büyük harf tercihli) + let mut labels: Vec = { + let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect(); + if single.is_empty() { + // Hiç tek karakter yoksa ilk label'ı göster (F1, Esc, …) + raw_labels.into_iter().take(1).collect() + } else { + // Büyük harf varsa onu göster, yoksa ilk tek karakteri + let uc = single.iter().find(|l| l.chars().next().map_or(false, |c| c.is_uppercase())); + vec![if let Some(s) = uc { (*s).clone() } else { (*single[0]).clone() }] + } + }; + + // , tuşu BKSL'deyse AC12'ye taşı (Türkçe Q düzeni için) + if name == "BKSL" && labels.contains(&",".to_string()) { + moved_comma = true; + continue; + } + if name == "AC12" { + labels = vec![",".to_string()]; + } + + keys.push(XkbKey { x, y, w, h, labels }); + + if x < min_x { min_x = x; } + if y < min_y { min_y = y; } + if x + w > max_x { max_x = x + w; } + if y + h > max_y { max_y = y + h; } + } + + // AC12 keymap'te yoksa manuel ekle + if moved_comma && !keys.iter().any(|k| (k.x - 12.75).abs() < 0.1 && (k.y - 3.0).abs() < 0.1) { + keys.push(XkbKey { x: 12.75, y: 3.0, w: 1.0, h: 1.0, labels: vec![",".to_string()] }); + } + + // normalize x,y — tüm tuşlar (0,0) bazlı olsun + for key in &mut keys { + key.x -= min_x; + key.y -= min_y; + } + + Some(XkbLayout { + keys, + width: max_x - min_x, + height: max_y - min_y, + }) +} + +/// Klavyeyi egui ile çizer. +pub fn draw_layout(ui: &mut egui::Ui, layout: &XkbLayout, max_size: Vec2) { + let margin = 6.0; + let spacing = 0.08; + let corner = 3.0; + + let uw = (max_size.x - margin * 2.0) / layout.width; + let uh = (max_size.y - margin * 2.0) / layout.height; + let unit = uw.min(uh).max(12.0); + + let draw_w = layout.width * unit; + let draw_h = layout.height * unit; + + let (_id, resp) = ui.allocate_exact_size(Vec2::new(draw_w + margin * 2.0, draw_h + margin * 2.0), egui::Sense::hover()); + let painter = ui.painter_at(resp.rect); + let origin = resp.rect.min; + + let visuals = ui.visuals(); + let key_fill = visuals.widgets.inactive.bg_fill; + let key_stroke = visuals.widgets.noninteractive.bg_stroke; + let text_color = egui::Color32::BLACK; + let bg = visuals.window_fill(); + + painter.rect_filled(resp.rect, Rounding::same(6.0), bg); + + let font_size = (unit * 0.28).max(6.0); + let small_size = (unit * 0.22).max(5.0); + + let sp = unit * spacing; + + for key in &layout.keys { + let kx = origin.x + margin + key.x * unit + sp; + let ky = origin.y + margin + key.y * unit + sp; + let kw = key.w * unit - sp * 2.0; + let kh = key.h * unit - sp * 2.0; + + if kw <= 0.0 || kh <= 0.0 { continue; } + + let key_rect = Rect::from_min_size(egui::pos2(kx, ky), Vec2::new(kw, kh)); + painter.rect(key_rect, Rounding::same(corner), key_fill, Stroke::new(key_stroke.width.max(0.5), key_stroke.color)); + + for (i, label) in key.labels.iter().enumerate() { + let lbl = label.trim(); + if lbl.is_empty() { continue; } + let (lx, ly, sz) = match i { + 0 => (kx + kw * 0.5, ky + kh * 0.5, font_size), + 1 => (kx + kw * 0.5, ky + kh * 0.75, small_size), + _ => (kx + kw * 0.5, ky + kh * 0.9, small_size), + }; + painter.text( + egui::pos2(lx, ly), egui::Align2::CENTER_CENTER, + lbl, egui::FontId::proportional(sz), text_color, + ); + } + } +} diff --git a/src/ui/theme.rs b/src/ui/theme.rs index a99ff35..6d6a13d 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -28,6 +28,7 @@ static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D); static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b) static SIDEBAR_TEXT: AtomicU32 = AtomicU32::new(0x000000); static KEYBOARD_BG: AtomicU32 = AtomicU32::new(0xFFFFFF); +static BG_ROOT: AtomicU32 = AtomicU32::new(0x1A1A1E); @@ -49,7 +50,7 @@ pub fn c_error() -> Color32 { color_from_hex(ERROR.load(Ordering::Relaxed)) pub fn c_sidebar() -> Color32 { color_from_hex(SIDEBAR.load(Ordering::Relaxed)) } pub fn c_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering::Relaxed)) } pub fn c_footer_bg() -> Color32 { color_from_hex(BG_FOOTER.load(Ordering::Relaxed)) } -pub fn c_root_bg() -> Color32 { color_from_hex(0xf7f7f7) } +pub fn c_root_bg() -> Color32 { color_from_hex(BG_ROOT.load(Ordering::Relaxed)) } pub fn c_keyboard_bg() -> Color32 { color_from_hex(KEYBOARD_BG.load(Ordering::Relaxed)) } pub fn set_theme_mode(is_dark: bool) { @@ -63,6 +64,7 @@ pub fn set_theme_mode(is_dark: bool) { TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed); SIDEBAR.store(0x0B0B0D, Ordering::Relaxed); KEYBOARD_BG.store(0x000000, Ordering::Relaxed); + BG_ROOT.store(0x1A1A1E, Ordering::Relaxed); } else { BG_FOOTER.store(0xf7f7f7, Ordering::Relaxed); BG_DARK.store(0xf7f7f7, Ordering::Relaxed); @@ -71,14 +73,15 @@ pub fn set_theme_mode(is_dark: bool) { TEXT_DIM.store(0x000000, Ordering::Relaxed); SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed); KEYBOARD_BG.store(0xffffff, Ordering::Relaxed); + BG_ROOT.store(0xf7f7f7, Ordering::Relaxed); - // BG_DARK.store(0xF0F0F5, Ordering::Relaxed); - // BG_PANEL.store(0xFFFFFF, Ordering::Relaxed); - // BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed); - // BORDER.store(0xD0D0DA, Ordering::Relaxed); - // TEXT.store(0x20202A, Ordering::Relaxed); - // TEXT_DIM.store(0x60606A, Ordering::Relaxed); - // SIDEBAR.store(0xE4E4EA, Ordering::Relaxed); + BG_DARK.store(0xF0F0F5, Ordering::Relaxed); + BG_PANEL.store(0xFFFFFF, Ordering::Relaxed); + BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed); + BORDER.store(0xD0D0DA, Ordering::Relaxed); + TEXT.store(0x20202A, Ordering::Relaxed); + TEXT_DIM.store(0x60606A, Ordering::Relaxed); + SIDEBAR.store(0xE4E4EA, Ordering::Relaxed); } } @@ -189,13 +192,13 @@ fn build_visuals(is_dark: bool) -> Visuals { v.widgets.noninteractive.bg_fill = c_bg_widget(); // Pasif Hali: Widget arka planı (TextEdit, ComboBox giriş alanı) - v.widgets.inactive.bg_fill = Color32::from_rgb(255, 255, 255); + v.widgets.inactive.bg_fill = if is_dark { Color32::from_rgb(255, 255, 255) } else { Color32::from_rgb(245, 245, 245) }; // Üzerine Gelme Hali: Hafif açık - v.widgets.hovered.bg_fill = Color32::from_rgb(55, 58, 68); + v.widgets.hovered.bg_fill = if is_dark { Color32::from_rgb(55, 58, 68) } else { Color32::from_rgb(235, 235, 235) }; // Tıklanma/Açık Kalma Hali: Daha açık - v.widgets.active.bg_fill = Color32::WHITE; + v.widgets.active.bg_fill = if is_dark { Color32::WHITE } else { Color32::from_rgb(230, 230, 230) }; // ComboBox stilleri (combobox_stil.rs) crate::ui::combobox_stil::apply_visuals(&mut v); From 35ddd0e9337d3c3c2ae2713a0478f0bb377a794b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 07:44:28 +0300 Subject: [PATCH 3/8] fix: next button disabled when user form is incomplete - gradient_button now accepts enabled parameter; uses Sense::hover() and muted colors when disabled - Added primary_button_enabled, secondary_button_enabled, danger_button_enabled variants - main.rs: next button uses primary_button_enabled(..., is_valid) so it's grayed out and non-clickable when form incomplete --- src/main.rs | 6 +---- src/ui/buttons.rs | 69 ++++++++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index a2d2d34..0bdbcf9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -455,11 +455,7 @@ impl eframe::App for YaliApp { // if !is_exec && !is_finish && ui::theme::danger_button(ui, &t!("cancel")).clicked() { // self.show_cancel_dialog = true; // }; - let next_clicked = if is_valid { - ui::theme::primary_button(ui, &next_label).clicked() - } else { - ui::theme::secondary_button(ui, &next_label).clicked() - }; + let next_clicked = ui::theme::primary_button_enabled(ui, &next_label, is_valid).clicked(); if next_clicked { self.state.current_step += 1; } diff --git a/src/ui/buttons.rs b/src/ui/buttons.rs index 4d299e9..88fc354 100644 --- a/src/ui/buttons.rs +++ b/src/ui/buttons.rs @@ -9,7 +9,7 @@ fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 { ) } -fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_color: Color32, text_color: Color32, hover_top: Color32, hover_bottom: Color32, border_color: Color32) -> egui::Response { +fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_color: Color32, text_color: Color32, hover_top: Color32, hover_bottom: Color32, border_color: Color32, enabled: bool) -> egui::Response { let rounding = Rounding::same(6.0); let min_size = Vec2::new(100.0, 32.0); let padding = Vec2::new(12.0, 6.0); @@ -27,13 +27,26 @@ fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_col min_size.y.max(galley.size().y + 2.0 * padding.y), ); - let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click()); + let sense = if enabled { egui::Sense::click() } else { egui::Sense::hover() }; + let (rect, response) = ui.allocate_exact_size(size, sense); + + let (actual_top, actual_bottom, actual_text, actual_border) = if enabled { + let hover_gradient = if response.hovered() { + (hover_top, hover_bottom) + } else { + (top_color, bottom_color) + }; + (hover_gradient.0, hover_gradient.1, text_color, border_color) + } else { + let disabled = lerp_color(top_color, Color32::from_rgb(80, 80, 80), 0.5); + (disabled, disabled, Color32::from_gray(160), Color32::from_gray(100)) + }; let n_strips = 16; for i in 0..n_strips { let t = i as f32 / n_strips as f32; let t2 = (i + 1) as f32 / n_strips as f32; - let c = lerp_color(top_color, bottom_color, (t + t2) * 0.5); + let c = lerp_color(actual_top, actual_bottom, (t + t2) * 0.5); let y0 = rect.top() + rect.height() * t; let y1 = rect.top() + rect.height() * t2; let strip_rect = egui::Rect::from_min_max( @@ -43,27 +56,12 @@ fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_col ui.painter().rect(strip_rect, Rounding::ZERO, c, Stroke::NONE); } - if response.hovered() { - for i in 0..n_strips { - let t = i as f32 / n_strips as f32; - let t2 = (i + 1) as f32 / n_strips as f32; - let c = lerp_color(hover_top, hover_bottom, (t + t2) * 0.5); - let y0 = rect.top() + rect.height() * t; - let y1 = rect.top() + rect.height() * t2; - let strip_rect = egui::Rect::from_min_max( - egui::pos2(rect.left(), y0), - egui::pos2(rect.right(), y1), - ); - ui.painter().rect(strip_rect, Rounding::ZERO, c, Stroke::NONE); - } - } - - ui.painter().rect(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0, border_color)); + ui.painter().rect(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0, actual_border)); ui.painter().galley( egui::pos2(rect.center().x - galley.size().x / 2.0, rect.center().y - galley.size().y / 2.0), galley, - text_color, + actual_text, ); if let Some(cursor) = ui.visuals().interact_cursor { @@ -76,34 +74,48 @@ fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_col } pub fn primary_button(ui: &mut egui::Ui, text: &str) -> egui::Response { + primary_button_enabled(ui, text, true) +} + +pub fn primary_button_enabled(ui: &mut egui::Ui, text: &str, enabled: bool) -> egui::Response { gradient_button(ui, text, Color32::from_rgb(50, 130, 255), Color32::from_rgb(13, 110, 253), Color32::WHITE, Color32::from_rgb(70, 150, 255), Color32::from_rgb(30, 130, 255), - Color32::from_rgb(13, 110, 253)) + Color32::from_rgb(13, 110, 253), + enabled) } pub fn secondary_button(ui: &mut egui::Ui, text: &str) -> egui::Response { + secondary_button_enabled(ui, text, true) +} + +pub fn secondary_button_enabled(ui: &mut egui::Ui, text: &str, enabled: bool) -> egui::Response { gradient_button(ui, text, Color32::from_rgb(130, 140, 150), // top Color32::from_rgb(100, 110, 120), // bottom Color32::from_rgb(255, 255, 255), // text color32 Color32::from_rgb(150, 160, 170), // hover top Color32::from_rgb(120, 130, 140), // hover bottom - Color32::from_rgb(100, 110, 120) // border - ) + Color32::from_rgb(100, 110, 120), // border + enabled) } pub fn danger_button(ui: &mut egui::Ui, text: &str) -> egui::Response { + danger_button_enabled(ui, text, true) +} + +pub fn danger_button_enabled(ui: &mut egui::Ui, text: &str, enabled: bool) -> egui::Response { gradient_button(ui, text, Color32::from_rgb(240, 83, 99), Color32::from_rgb(200, 40, 60), Color32::WHITE, Color32::from_rgb(255, 100, 120), Color32::from_rgb(220, 60, 80), - Color32::from_rgb(200, 40, 60)) + Color32::from_rgb(200, 40, 60), + enabled) } #[allow(dead_code)] @@ -114,7 +126,8 @@ pub fn white_grd_btn(ui: &mut egui::Ui, text: &str) -> egui::Response { Color32::from_rgb(102, 102, 102), Color32::from_rgb(246, 246, 246), Color32::from_rgb(255, 255, 255), - Color32::from_rgb(177, 177, 177)) + Color32::from_rgb(177, 177, 177), + true) } #[allow(dead_code)] pub fn btn_dark_blue(ui: &mut egui::Ui, text: &str) -> egui::Response { @@ -124,7 +137,8 @@ pub fn btn_dark_blue(ui: &mut egui::Ui, text: &str) -> egui::Response { Color32::WHITE, Color32::from_rgb(70, 140, 207), Color32::from_rgb(99, 184, 238), - Color32::from_rgb(56, 102, 163)) + Color32::from_rgb(56, 102, 163), + true) } #[allow(dead_code)] @@ -135,5 +149,6 @@ pub fn light_gray_btn(ui: &mut egui::Ui, text: &str) -> egui::Response { Color32::from_rgb(102, 102, 102), // metin: #666666 Color32::from_rgb(233, 233, 233), // hover üst (ters) Color32::from_rgb(249, 249, 249), // hover alt (ters) - Color32::from_rgb(227, 195, 227)) // border: #e3c7e3 + Color32::from_rgb(227, 195, 227), // border: #e3c7e3 + true) } From 1161296d2742a42215d23964eb61d4137e3a397d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 08:09:46 +0300 Subject: [PATCH 4/8] feat: add sub-progress bar for rsync file copy progress - InstallMessage: added SubProgress(f32) and CurrentFile(String) variants - GlobalState: added install_sub_progress field - UiSender: added sub_progress() and current_file() helpers - CopyFilesJob: now streams rsync stderr, parses xfr#/to-chk for progress, and sends SubProgress/CurrentFile messages to UI - execution.rs: displays secondary progress bar below the main one during rsync file copy, updates install_current_file in real-time - main.rs: handles new InstallMessage variants in auto-install mode --- src/installer.rs | 2 ++ src/jobs/mod.rs | 76 ++++++++++++++++++++++++++++++++++++++++-- src/main.rs | 2 ++ src/steps/execution.rs | 17 ++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/installer.rs b/src/installer.rs index e8d1536..ef1e25b 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -207,6 +207,7 @@ pub struct GlobalState { pub current_step: usize, pub available_disks: Vec, pub install_progress: f32, + pub install_sub_progress: f32, pub install_log: Vec, pub install_current_file: Option, pub demo_mode: bool, @@ -274,6 +275,7 @@ impl Default for GlobalState { current_step: 0, available_disks: Vec::new(), install_progress: 0.0, + install_sub_progress: 0.0, install_log: Vec::new(), install_current_file: None, demo_mode: false, diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index a50deb3..7a9c5de 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -13,6 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; +use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; use rust_i18n::t; @@ -27,6 +28,10 @@ pub enum InstallMessage { Log(String), /// Genel ilerleme; 0.0–1.0 arasında. Progress(f32), + /// Alt ilerleme (ör: rsync içindeki dosya kopyalama); 0.0–1.0 arasında. + SubProgress(f32), + /// Kopyalanmakta olan dosya adı. + CurrentFile(String), /// Tüm işler başarıyla tamamlandı. Done, /// Bir iş başarısız oldu; kurulum durdu. @@ -54,6 +59,14 @@ impl UiSender { pub fn log(&self, s: impl Into) { self.send(InstallMessage::Log(s.into())); } + + pub fn sub_progress(&self, p: f32) { + self.send(InstallMessage::SubProgress(p)); + } + + pub fn current_file(&self, f: impl Into) { + self.send(InstallMessage::CurrentFile(f.into())); + } } // ───────────────────────────────────────────── @@ -179,8 +192,67 @@ impl Job for CopyFilesJob { 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?; + if DEMO_MODE.load(Ordering::Relaxed) { + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + return Ok(()); + } + + let mut child = Command::new("rsync") + .args(&args) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .map_err(|e| format!("rsync çalıştırılamadı: {}", e))?; + + let stderr = child.stderr.take() + .ok_or_else(|| "rsync stderr alınamadı".to_string())?; + let reader = BufReader::new(stderr); + let mut lines = reader.lines(); + + while let Some(line) = lines.next_line().await + .map_err(|e| format!("rsync çıktısı okunamadı: {}", e))? + { + let trimmed = line.trim(); + // rsync --info=progress2 format (newlines when piped): + // 1,024 100% 512.00kB/s 0:00:00 (xfr#5, to-chk=99/105) + // Extract xfr# and to-chk for sub-progress + if let Some(xfr_pos) = trimmed.find("xfr#") { + let after_xfr = &trimmed[xfr_pos + 4..]; + let xfr_end = after_xfr.find(|c: char| !c.is_ascii_digit()).unwrap_or(after_xfr.len()); + let xfr: u64 = after_xfr[..xfr_end].parse().unwrap_or(0); + + if let Some(to_chk_pos) = trimmed.find("to-chk=") { + let after_to = &trimmed[to_chk_pos + 7..]; + let slash_pos = after_to.find('/').unwrap_or(after_to.len()); + let total_end = after_to[slash_pos + 1..] + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(after_to[slash_pos + 1..].len()); + let total: u64 = after_to[slash_pos + 1..][..total_end].parse().unwrap_or(1); + + let p = if total > 0 { + (xfr as f32) / (total as f32) + } else { + 0.0 + }; + ui.sub_progress(p.min(1.0)); + } + } + + // Extract filename: lines starting with a path (not a number/space) + let first_char = trimmed.chars().next(); + if let Some('/') = first_char { + ui.current_file(trimmed.to_string()); + } else if trimmed.starts_with("./") || trimmed.starts_with("..") { + ui.current_file(trimmed.to_string()); + } + } + + let status = child.wait().await + .map_err(|e| format!("rsync beklenemedi: {}", e))?; + + if !status.success() { + return Err(format!("rsync çıkış kodu: {:?}", status.code())); + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 0bdbcf9..d7a4b7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -188,6 +188,8 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> { print!("\r {}: [{:50}] {:.0}%", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0); let _ = std::io::Write::flush(&mut std::io::stdout()); } + Ok(InstallMessage::SubProgress(_)) => {} + Ok(InstallMessage::CurrentFile(_)) => {} Ok(InstallMessage::Done) => { println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description")); break; diff --git a/src/steps/execution.rs b/src/steps/execution.rs index b328f68..ad8d11d 100644 --- a/src/steps/execution.rs +++ b/src/steps/execution.rs @@ -40,6 +40,7 @@ impl InstallerStep for ExecutionStep { self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone())); state.install_progress = 0.0; + state.install_sub_progress = 0.0; state.install_log.clear(); state.install_current_file = None; state.install_log.push(t!("install_starting").to_string()); @@ -182,6 +183,12 @@ impl InstallerStep for ExecutionStep { Ok(InstallMessage::Progress(p)) => { state.install_progress = p; } + Ok(InstallMessage::SubProgress(p)) => { + state.install_sub_progress = p; + } + Ok(InstallMessage::CurrentFile(f)) => { + state.install_current_file = Some(f); + } Ok(InstallMessage::Done) => { state.install_progress = 1.0; self.finished = true; @@ -305,6 +312,16 @@ impl InstallerStep for ExecutionStep { .animate(running) .desired_height(10.0), ); + + if state.install_sub_progress > 0.0 { + ui.add_space(4.0); + ui.add( + egui::ProgressBar::new(state.install_sub_progress) + .animate(running) + .desired_height(6.0) + .fill(theme::c_accent_dim()), + ); + } }); // Durum etiketi From 89d397aa7d6b6f4e58476b60c06b49c7148bf7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 08:26:32 +0300 Subject: [PATCH 5/8] fix: simulate sub-progress in demo mode for CopyFilesJob --- src/jobs/mod.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index 7a9c5de..d80a50d 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -193,7 +193,14 @@ impl Job for CopyFilesJob { args.push(self.target.clone()); if DEMO_MODE.load(Ordering::Relaxed) { - tokio::time::sleep(std::time::Duration::from_secs(3)).await; + ui.log("rsync /run/livecd/squashfs-root/ → /mnt (DEMO)"); + let steps = 20; + for i in 0..steps { + let p = (i + 1) as f32 / steps as f32; + ui.current_file(format!("/usr/share/doc/{}/README.md", (i * 137) % 50)); + ui.sub_progress(p); + tokio::time::sleep(std::time::Duration::from_millis(150)).await; + } return Ok(()); } From 6d0e8fa9bfc8bd6805fb6f61e402e1a80e1fbd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 08:37:18 +0300 Subject: [PATCH 6/8] fix: language selector SVG icon now uses theme text color (tint) --- src/funct.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/funct.rs b/src/funct.rs index 7e60b90..f336711 100644 --- a/src/funct.rs +++ b/src/funct.rs @@ -127,6 +127,7 @@ pub fn language_selector_ui(ui: &mut eframe::egui::Ui, state: &mut crate::instal ui.add( eframe::egui::Image::new(eframe::egui::include_image!("../assets/icons/language-icon.svg")) .max_width(40.0) + .tint(crate::ui::theme::c_text()) ); }); From 7f94c8c0cc9eae6c8edd0b85dbcb245ade7bf53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Wed, 24 Jun 2026 08:42:20 +0300 Subject: [PATCH 7/8] fix: make language selector combobox and dropdown theme-aware (dark/light) --- src/ui/combobox.rs | 14 +++++++-- src/ui/combobox_stil.rs | 67 +++++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/src/ui/combobox.rs b/src/ui/combobox.rs index f5b16b9..6225937 100644 --- a/src/ui/combobox.rs +++ b/src/ui/combobox.rs @@ -1,5 +1,5 @@ use eframe::egui::{self, Rect, Vec2}; -use super::combobox_stil::{draw_gradient_bg, DROPDOWN_TEXT_COLOR}; +use super::combobox_stil::draw_gradient_bg; /// Stil uygulanmış ComboBox oluşturur (genişlik ayarı yapılmaz, Grid içinde kullanım için). pub fn combo_box( @@ -48,13 +48,14 @@ pub fn selectable_value( draw_gradient_bg(ui, rect); + let text_color = combobox_stil_text_color(ui); let painter = ui.painter(); painter.text( egui::pos2(rect.left() + 6.0, rect.center().y), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(14.0), - DROPDOWN_TEXT_COLOR, + text_color, ); if response.clicked() { @@ -63,6 +64,15 @@ pub fn selectable_value( response.on_hover_cursor(egui::CursorIcon::PointingHand) } +fn combobox_stil_text_color(ui: &egui::Ui) -> egui::Color32 { + let bg = ui.visuals().panel_fill; + if bg.r() < 128 && bg.g() < 128 && bg.b() < 128 { + egui::Color32::from_rgb(230, 230, 240) + } else { + egui::Color32::from_rgb(50, 50, 50) + } +} + /// ComboBox dropdown öğesi — özel içerik çizmek için (ör. bayrak + metin). /// `add_contents` callback'i verilen `Rect` içine içerik ekler. pub fn selectable_value_custom( diff --git a/src/ui/combobox_stil.rs b/src/ui/combobox_stil.rs index 15eafb3..96f8480 100644 --- a/src/ui/combobox_stil.rs +++ b/src/ui/combobox_stil.rs @@ -1,64 +1,67 @@ use eframe::egui::{self, Color32, Rect, Stroke}; use egui::epaint::Mesh; -// ─── ComboBox Popup Renk Sabitleri ────────────────────────── -pub const WEAK_BG_INACTIVE: Color32 = Color32::from_rgb(224, 224, 224); -pub const WEAK_BG_HOVERED: Color32 = Color32::from_rgb(200, 200, 200); -pub const WEAK_BG_ACTIVE: Color32 = Color32::from_rgb(180, 180, 180); - -// ─── Gradient Dropdown Öğesi Renkleri ─────────────────────── -pub const GRADIENT_TOP: Color32 = Color32::from_rgb(249, 249, 249); -pub const GRADIENT_BOT: Color32 = Color32::from_rgb(233, 233, 233); - -// ─── ComboBox Metin Renkleri ──────────────────────────────── -/// Seçili metin rengi (ComboBox kapalıyken widget üzerinde görünen metin) -pub const COMBO_TEXT_COLOR: Color32 = Color32::from_rgb(50, 50, 50); -/// Açılır listedeki öğelerin metin rengi -pub const DROPDOWN_TEXT_COLOR: Color32 = Color32::from_rgb(50, 50, 50); - -// ─── ComboBox Kenarlık (border) Renkleri ──────────────────── -/// Üzerine gelindiğinde / tıklanınca kenarlık rengi -pub const COMBO_BORDER_ACCENT: Color32 = Color32::from_rgb(155, 155, 155); - /// Verilen `Visuals` yapısına ComboBox stillerini uygular. pub fn apply_visuals(v: &mut egui::Visuals) { - v.widgets.inactive.weak_bg_fill = WEAK_BG_INACTIVE; - v.widgets.hovered.weak_bg_fill = WEAK_BG_HOVERED; - v.widgets.active.weak_bg_fill = WEAK_BG_ACTIVE; + v.widgets.inactive.weak_bg_fill = Color32::from_rgb(224, 224, 224); + v.widgets.hovered.weak_bg_fill = Color32::from_rgb(200, 200, 200); + v.widgets.active.weak_bg_fill = Color32::from_rgb(180, 180, 180); - v.widgets.inactive.fg_stroke = Stroke::new(1.0, COMBO_TEXT_COLOR); + v.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(50, 50, 50)); v.widgets.hovered.fg_stroke = Stroke::new(1.5, Color32::WHITE); v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE); - // Kenarlıklar - v.widgets.inactive.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT); - v.widgets.hovered.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT); - v.widgets.active.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT); + v.widgets.inactive.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155)); + v.widgets.hovered.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155)); + v.widgets.active.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155)); } -/// Açık gri gradyan mesh arkaplanı çizer (dropdown öğeleri için). +fn is_dark_ui(ui: &egui::Ui) -> bool { + let bg = ui.visuals().panel_fill; + bg.r() < 128 && bg.g() < 128 && bg.b() < 128 +} + +fn gradient_top(ui: &egui::Ui) -> Color32 { + if is_dark_ui(ui) { + Color32::from_rgb(40, 40, 45) + } else { + Color32::from_rgb(249, 249, 249) + } +} + +fn gradient_bot(ui: &egui::Ui) -> Color32 { + if is_dark_ui(ui) { + Color32::from_rgb(30, 30, 35) + } else { + Color32::from_rgb(233, 233, 233) + } +} + +/// Açık/koyu temaya uygun gradyan mesh arkaplanı çizer (dropdown öğeleri için). pub fn draw_gradient_bg(ui: &mut egui::Ui, rect: Rect) { + let top = gradient_top(ui); + let bot = gradient_bot(ui); let mut mesh = Mesh::default(); let idx = mesh.vertices.len() as u32; mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.left(), rect.top()), uv: egui::pos2(0.0, 0.0), - color: GRADIENT_TOP, + color: top, }); mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.right(), rect.top()), uv: egui::pos2(0.0, 0.0), - color: GRADIENT_TOP, + color: top, }); mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.right(), rect.bottom()), uv: egui::pos2(0.0, 0.0), - color: GRADIENT_BOT, + color: bot, }); mesh.vertices.push(egui::epaint::Vertex { pos: egui::pos2(rect.left(), rect.bottom()), uv: egui::pos2(0.0, 0.0), - color: GRADIENT_BOT, + color: bot, }); mesh.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx + 2, idx + 3, idx]); ui.painter().add(egui::Shape::mesh(mesh)); From 71e2ab382917167344945e30456ae6b333cf2980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Thu, 25 Jun 2026 01:33:51 +0300 Subject: [PATCH 8/8] chore: add English/Turkish changelog cross-links --- CHANGELOG.md | 30 ++++++- CHANGELOG_en.md | 176 ++++++++++++++++++++++++++++++++++++ README.md | 15 ++-- README1.md | 114 ++++++++++++++++++++++++ README_en.md | 112 +++++++++++++++++++++++ locales/en.toml | 4 + locales/tr.toml | 4 + src/installer.rs | 2 +- src/jobs/mod.rs | 32 ++++++- src/main.rs | 3 +- src/steps/bootloader.rs | 111 +++++++++++------------ src/steps/mod.rs | 3 +- src/steps/users.rs | 191 +++++++++++++++++++++++++++++++--------- src/ui/theme.rs | 17 ++-- 14 files changed, 694 insertions(+), 120 deletions(-) create mode 100644 CHANGELOG_en.md create mode 100644 README1.md create mode 100644 README_en.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a849229..a5f8ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,32 @@ -# Changelog +# Changelog — [English](CHANGELOG_en.md) + +## [4.0.4] — 2026-06-25 + +### Added +- Kullanıcı adı doğrulama fonksiyonu `validate_username` (detaylı hata mesajları döndürür) +- Yasaklı sistem kullanıcı adları listesi (root, daemon, bin, sys, vb.) +- Türkçe karakter dönüşümleri büyük harfleri de kapsayacak şekilde genişletildi (Ş, Ğ, İ, Ö, Ü, Ç) +- Locale dosyalarına yeni kullanıcı adı doğrulama anahtarları (username_too_long, username_forbidden, username_invalid_start, username_invalid_chars) +- UsersStep struct'ına `autologin` alanı eklendi + +### Changed +- Kullanıcı adı, şifre doğrulama ve hostname input'ları için check/fail iconları input'un yanına alındı +- Hata mesajları inputların altında gösteriliyor (username doğrulama, şifre eşleşme) +- Önyükleyici (bootloader) sayfası form düzeni users sayfasındaki gibi güncellendi (Frame, form_label, form_input_text/password) +- Display manager adımı adım listesinden kaldırıldı (ISO'lar tek masaüstü ile geliyor) +- Autologin özelliği display manager adımından users adımına taşındı +- Display manager yapılandırması otomatik hale getirildi (desktop_environment otomatik tespit ediliyor) +- SDDM/GDM yapılandırmasında masaüstü ortamı seçimi kullanılıyor (xfce.desktop, gnome.desktop vb.) +- Autologin varsayılan değeri false olarak değiştirildi + +### Fixed +- Dark modda sidebar metin renginin kaybolması sorunu (SIDEBAR_TEXT beyaz olarak ayarlandı) +- derive_username fonksiyonunda yanlış karakter (ā → ğ) düzeltildi +- unused_mut warning (name değişkeni) +- unused variable warning (err değişkeni) +- unused import warning (DisplayManagerStep) + +--- ## [4.0.3] — 2026-06-23 diff --git a/CHANGELOG_en.md b/CHANGELOG_en.md new file mode 100644 index 0000000..0fd10e0 --- /dev/null +++ b/CHANGELOG_en.md @@ -0,0 +1,176 @@ +# Changelog — [Türkçe](CHANGELOG.md) + +## [4.0.4] — 2026-06-25 + +### Added +- Username validation function `validate_username` (returns detailed error messages) +- Forbidden system usernames list (root, daemon, bin, sys, etc.) +- Turkish character conversions extended to include uppercase letters (Ş, Ğ, İ, Ö, Ü, Ç) +- New username validation keys in locale files (username_too_long, username_forbidden, username_invalid_start, username_invalid_chars) +- `autologin` field added to UsersStep struct + +### Changed +- Check/fail icons for username, password validation, and hostname inputs moved next to input fields +- Error messages shown below inputs (username validation, password mismatch) +- Bootloader page form layout updated to match users page (Frame, form_label, form_input_text/password) +- Display manager step removed from step list (ISOs ship with single desktop environment) +- Autologin feature moved from display manager step to users step +- Display manager configuration made automatic (desktop_environment auto-detected) +- Desktop environment selection used in SDDM/GDM configuration (xfce.desktop, gnome.desktop, etc.) +- Autologin default value changed to false + +### Fixed +- Sidebar text color disappearing in dark mode (SIDEBAR_TEXT set to white) +- Incorrect character in derive_username function (ā → ğ) +- unused_mut warning (name variable) +- unused variable warning (err variable) +- unused import warning (DisplayManagerStep) + +--- + +## [4.0.3] — 2026-06-23 + +### Added +- Searchable keyboard layout/variant selector: egui `TextEdit` + `Area` popup custom implementation +- `color_from_hex(hex: u32)` helper (`combobox_stil.rs`) +- `layout_buf`, `variant_buf` fields (`KeyboardStep`) + +### Changed +- `egui-dropdown` dependency removed (no longer used) +- Layout/Variant selectors side-by-side → stacked, label width equalized to longest label +- Popup item height 28px → 32px, gradient background (`draw_gradient_bg`) in `combobox.rs` style +- `DROPDOWN_TEXT_COLOR` off-white → pure white (`color_from_hex(0xFFFFFF)`) +- All color constants converted to hex format (`color_from_hex`) + +### Fixed +- Popup not showing all items on first open due to old selection filter — buffer cleared on focus +- `t!()` return type `Cow` → `String` mismatch (`keyboard.rs`) + +### Changed +- Slider maximum value now equals available disk space (excluding other partitions) +- Selected partition capacity included in slider range in edit mode +- `form_input_text` size field directly connected to `add_size_str`, formatted MB display added +- Size label now in `"123.456 MB"` format instead of `"123 MB"` (for parseable values) + +### Fixed +- Value written to size input field not written back to `add_size_str` — text edit remained read-only + +--- + +## [4.0.1] — 2026-06-19 + +### Added +- `c_root_bg()` theme function +- `show_root_password`, `show_root_password_confirm` fields (`UsersStep`) +- `form_input_text` / `form_input_password` wrapped with `Frame` and `.frame(false)` + +### Changed +- `form_input_text`: Wrapped in Frame, added stroke/fill/rounding, height 32→10 +- `form_input_password`: SVG eye icons (instead of emoji), Frame wrapped, `override_text_color` +- Disk grid selection colors `theme::c_*()` → constant `(237,237,224,1)` +- Disk list header and column headers: `Align::Center` → `Align::LEFT`, `vertical_centered` removed +- Root password fields raw `TextEdit` → `form_input_password` (with eye icon + frame) +- User form Frame color `theme::c_bg_widget()` → `(240,243,198)` +- Manual disk list brand/model label ordering changed + +### Fixed +- `users.rs:154` — `Color32` import error (missing `egui::` prefix) + +--- + +## [4.0.0] — 2026-06-18 + +### Added +- `CHANGELOG.md` +- `src/ui/combobox.rs` — ComboBox helpers (`show_combo_box`, `selectable_value`, `selectable_value_custom`, `draw_gradient_bg`) +- `src/ui/combobox_stil.rs` — ComboBox visual constants and `apply_visuals()` +- `src/steps/location.rs` — Region/timezone selection step +- `src/steps/network.rs` — Network configuration step +- `src/steps/rescue.rs` — Rescue mode +- `xkbcommon` dependency +- Keyboard visual: XKB keymap → egui drawing (`compile_layout`, `draw_layout`, `keysym_to_label`) +- ISO‑105 key position mapping (function keys, numpad, navigation included) +- Single character display per key (uppercase preferred) + +### Changed +- Keyboard visual: KLE JSON download/parse → direct keymap compilation with xkbcommon +- `Cargo.toml`: `ureq`, `serde_json` removed; `xkbcommon` added +- ComboBox styles moved from `theme.rs` → `combobox_stil.rs` +- Comment lines added to `secondary_button` gradient colors +- `draw_gradient_bg` moved to `combobox_stil.rs` +- All ComboBox usages updated with `theme::show_combo_box` / `theme::selectable_value` +- Keyboard step (`keyboard.rs`): `ScrollArea`+`selectable_label` → ComboBox +- `TextEdit::multiline` background made with per-widget `Frame::group` +- `locale/` files updated +- Font sizes increased (label 9→12, secondary 7→9) + +### Removed +- `ureq`, `serde_json` dependencies +- `assets/keyboards/KLE_*.json` cache files +- `assets/keyboards/KB_*.svg` files +- Old keyboard SVG loading code +- KLE JSON download/cache logic +- Old button examples in demo page + +### Fixed +- `set_style`/`set_visuals` order: `set_style` called first +- Region/timezone labels vertically aligned +- Keyboard step `on_enter` not called on first render — layout also initialized in `show()` +- `xkbcommon::Keycode` iteration (conversion with `raw()`) +- `None::<&str>` → `None::` type mismatch + +--- + +## [4.0.0] — 2026-06-06 + +### Added +- Rescue mode (rescue step) +- Network configuration step (NetworkManager/Wicd) +- Netinstall step +- LVM and LUKS encryption support +- Demo mode (`--demo`) +- Automatic installation (`--auto-install answer.toml`) +- Multi-language support with `rust-i18n` (Turkish, English) +- Gradient buttons (`gradient_button`, `secondary_button`) +- Language selector (flag + label) +- SVG loading support (`egui_extras` + `image`) +- Slideshow (configurable with branding.toml) +- Step list in sidebar (processed steps green ✔️) +- Real-time installation file tracking +- Advanced partitioning UI (usage bar, LUKS indicator, mount-point selector) + +### Changed +- `pspec.xml`/`actions.py` → Cargo-based build +- `comar`/`mudur` calls → `zbus` D-Bus and direct command execution +- Shell-based password management → secure stdin piping (LUKS/GRUB) +- Automatic username derivation (real name → system username) +- Hostname validation/sanitization +- Cleanup operations: user deletion, pool management, boot directory cleanup +- Rsync command with dynamic exclusion +- Version 4.0.1 → 4.0.0 (Cargo.toml compatibility) + +### Fixed +- GRUB password writing security +- Dracut/Mkinitcpio LVM module addition +- EFI partition detection and mounting +- Password strength indicator colors +- Automatic partitioning (LVM/LUKS ordering) + +--- + +## [0.1.0] — 2026-05-20 + +### Added +- First stable release +- Project skeleton (egui + wgpu) +- Step-based installation wizard +- Automatic disk partitioning (GPT/MBR) +- User creation (with password strength check) +- Bootloader configuration (GRUB) +- Display manager selection +- Summary screen +- Basic color theme (light/dark) +- Customizable branding (branding.toml) +- Error screen for rescue purposes +- Job queue (JobQueue / async task management) +- YALI serialization (installer state save/load) diff --git a/README.md b/README.md index ee26dbd..4dcc883 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,12 @@ Rust ve egui ile yeniden yazılmış **Pisi GNU/Linux** kurulum aracı. YALI, canlı ortamdan sistemi kurar, paketleri kurar ve temel yapılandırmayı gerçekleştirir. Orijinal Python/Qt sürümünün modern, hızlı ve bakımı kolay bir yeniden yazımıdır. +- [Changelog](CHANGELOG.md) +- [English README](README_en.md) + ## Özellikler -- **11 adımlı kurulum sihirbazı**: Hoş Geldiniz → Konum → Klavye → Ağ → Disk Bölümleme → Kullanıcılar → Bootloader → Görüntü Yöneticisi → Özet → Kurulum → Bitir +- **10 adımlı kurulum sihirbazı**: Hoş Geldiniz → Konum → Klavye → Ağ → Disk Bölümleme → Kullanıcılar → Bootloader → Özet → Kurulum → Bitir - **İki mod**: Grafik arayüz (varsayılan) ve cevap dosyasıyla otomatik kurulum (`--auto-install answer.toml`) - **Demo modu**: Sistem kontrollerini atlar, kurulumu simüle eder (`--demo`) - **Disk bölümleme**: Otomatik (GPT/MBR) veya manuel, LVM ve LUKS şifreleme desteği @@ -56,7 +59,6 @@ sudo cargo run --release -- --auto-install answer.toml │ │ ├── partition.rs # Disk bölümleme (otomatik + manuel + LVM) │ │ ├── users.rs # Kullanıcı hesabı oluşturma │ │ ├── bootloader.rs # GRUB yapılandırması -│ │ ├── display_manager.rs # Görüntü yöneticisi ve masaüstü seçimi │ │ ├── summary.rs # Kurulum özeti │ │ ├── execution.rs # Kurulum yürütme (ilerleme çubuğu, log) │ │ ├── finish.rs # Tamamlanma ekranı @@ -86,12 +88,11 @@ sudo cargo run --release -- --auto-install answer.toml 3. **Klavye** — Düzen/varyant seçimi, arama, canlı önizleme (setxkbmap) 4. **Ağ** — WiFi tarama, bağlantı, proxy 5. **Disk Bölümleme** — Otomatik (rehberli) veya manuel, LVM, LUKS -6. **Kullanıcılar** — Hesap oluşturma, parola gücü göstergesi, root parolası +6. **Kullanıcılar** — Hesap oluşturma, parola gücü göstergesi, root parolası, otomatik oturum açma 7. **Bootloader** — GRUB aygıtı, zaman aşımı, parola, çekirdek parametreleri -8. **Görüntü Yöneticisi** — SDDM/LightDM/GDM, masaüstü ortamı, otomatik oturum -9. **Özet** — Tüm seçimlerin gözden geçirilmesi -10. **Kurulum** — Slayt + ilerleme çubuğu + canlı log -11. **Bitir** — Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön +8. **Özet** — Tüm seçimlerin gözden geçirilmesi +9. **Kurulum** — Slayt + ilerleme çubuğu + canlı log +10. **Bitir** — Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön ## Bağımlılıklar diff --git a/README1.md b/README1.md new file mode 100644 index 0000000..ee26dbd --- /dev/null +++ b/README1.md @@ -0,0 +1,114 @@ +# YALI (Yet Another Linux Installer) — Rust / egui + +Rust ve egui ile yeniden yazılmış **Pisi GNU/Linux** kurulum aracı. YALI, canlı ortamdan sistemi kurar, paketleri kurar ve temel yapılandırmayı gerçekleştirir. Orijinal Python/Qt sürümünün modern, hızlı ve bakımı kolay bir yeniden yazımıdır. + +## Özellikler + +- **11 adımlı kurulum sihirbazı**: Hoş Geldiniz → Konum → Klavye → Ağ → Disk Bölümleme → Kullanıcılar → Bootloader → Görüntü Yöneticisi → Özet → Kurulum → Bitir +- **İki mod**: Grafik arayüz (varsayılan) ve cevap dosyasıyla otomatik kurulum (`--auto-install answer.toml`) +- **Demo modu**: Sistem kontrollerini atlar, kurulumu simüle eder (`--demo`) +- **Disk bölümleme**: Otomatik (GPT/MBR) veya manuel, LVM ve LUKS şifreleme desteği +- **Ağ yapılandırması**: WiFi tarama (nmcli), proxy ayarları +- **Önyükleyici**: GRUB aygıt/parola/zaman aşımı ayarları, çekirdek parametreleri +- **Kurtarma modu**: Chroot, GRUB yeniden kurulumu, Pisi geçmişe dönme +- **OEM modu**: Üretici ön kurulumu için ön tanımlı ayarlar +- **Uluslararasılaştırma**: Türkçe ve İngilizce (rust-i18n ile TOML locale dosyaları) +- **Markalama sistemi**: Çalışma zamanında yüklenebilir `branding.toml` ile logo, slayt, renk ayarları +- **Tema**: Koyu/açık tema, atomik renk yönetimi, özel gradient butonlar +- **Asenkron iş kuyruğu**: Tüm kurulum adımları ayrı `tokio` runtime'ında çalışır, `mpsc` kanalı ile UI güncellenir +- **Sistem kontrolleri**: RAM, disk alanı, internet, UEFI, CPU, NVMe gibi 8 ön kontrol +- **Slayt gösterisi**: 9 masaüstü ortamı için özel markalı slaytlar +- **Demo oturum temizliği**: Canlı ortam artifact'lerini kaldırır, üretim deposu ekler + +## Kullanım + +```bash +# Normal kurulum (root yetkisi gerekir) +sudo cargo run --release + +# Demo modu (root gerekmez) +cargo run --release -- --demo + +# Otomatik kurulum (cevap dosyası ile) +sudo cargo run --release -- --auto-install answer.toml +``` + +## Proje Yapısı + +``` +├── src/ +│ ├── main.rs # Giriş noktası, eframe App, GUI düzeni +│ ├── installer.rs # InstallerStep trait'i, GlobalState, WelcomeStep +│ ├── funct.rs # Paylaşılan UI yardımcıları (form alanları, dil seçici) +│ ├── branding.rs # Branding.toml yükleyici (logo, slayt, tema, OEM) +│ ├── ui/ +│ │ ├── theme.rs # Renkler, fontlar, Visuals, section_heading, error/warning/success kutuları +│ │ ├── buttons.rs # Gradient butonlar (primary, secondary, danger, vb.) +│ │ ├── combobox.rs # ComboBox yardımcıları (show_combo_box, selectable_value) +│ │ ├── combobox_stil.rs # ComboBox görsel sabitleri (weak_bg, border, text renkleri) +│ │ ├── slideshow.rs # Otomatik ilerleyen slayt gösterisi +│ │ └── error_screen.rs # Hata ekranı (log görüntüleyici, kopyala, yeniden dene) +│ ├── steps/ +│ │ ├── location.rs # Konum/saat dilimi + tarih/saat +│ │ ├── license.rs # Lisans sözleşmesi +│ │ ├── keyboard.rs # Klavye düzeni/varyant seçimi +│ │ ├── network.rs # Ağ yapılandırması (WiFi, proxy) +│ │ ├── partition.rs # Disk bölümleme (otomatik + manuel + LVM) +│ │ ├── users.rs # Kullanıcı hesabı oluşturma +│ │ ├── bootloader.rs # GRUB yapılandırması +│ │ ├── display_manager.rs # Görüntü yöneticisi ve masaüstü seçimi +│ │ ├── summary.rs # Kurulum özeti +│ │ ├── execution.rs # Kurulum yürütme (ilerleme çubuğu, log) +│ │ ├── finish.rs # Tamamlanma ekranı +│ │ ├── netinstall.rs # Ek paket grubu seçimi +│ │ └── rescue.rs # Kurtarma modu (chroot, GRUB, Pisi takeback) +│ ├── jobs/mod.rs # Asenkron iş kuyruğu ve tüm kurulum işleri +│ └── autoinstall/ +│ ├── mod.rs # Cevap dosyası yükleme ve state uygulama +│ └── checker.rs # Sistem ön kontrolleri +├── locales/ +│ ├── tr.toml # Türkçe çeviriler +│ └── en.toml # İngilizce çeviriler +├── assets/ +│ ├── icons/ # UI ikonları (YALI_icon_LIGHT.svg, language-icon.svg, vb.) +│ ├── flags/ # Ülke bayrakları (~250 SVG) +│ └── *.png # Slayt ve logo görselleri +├── branding.toml # Varsayılan markalama yapılandırması +├── branding_en.toml # İngilizce markalama +├── branding_tr.toml # Türkçe markalama +└── answer-example.toml # Örnek cevap dosyası +``` + +## Kurulum İş Akışı + +1. **Hoş Geldiniz** — Dil seçimi, sistem kontrolleri, kurtarma modu +2. **Konum / Zaman** — Saat dilimi seçimi (bölge/alan çift ComboBox), GeoIP, NTP veya manuel tarih/saat +3. **Klavye** — Düzen/varyant seçimi, arama, canlı önizleme (setxkbmap) +4. **Ağ** — WiFi tarama, bağlantı, proxy +5. **Disk Bölümleme** — Otomatik (rehberli) veya manuel, LVM, LUKS +6. **Kullanıcılar** — Hesap oluşturma, parola gücü göstergesi, root parolası +7. **Bootloader** — GRUB aygıtı, zaman aşımı, parola, çekirdek parametreleri +8. **Görüntü Yöneticisi** — SDDM/LightDM/GDM, masaüstü ortamı, otomatik oturum +9. **Özet** — Tüm seçimlerin gözden geçirilmesi +10. **Kurulum** — Slayt + ilerleme çubuğu + canlı log +11. **Bitir** — Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön + +## Bağımlılıklar + +| Bağımlılık | Sürüm | Amaç | +|---|---|---| +| `eframe` | 0.27 (wgpu) | egui çatısı, WebGPU çıktısı | +| `egui_extras` | 0.27 (image, file, svg) | Görsel yükleme, dosya diyalogları, SVG | +| `image` | 0.24 | PNG kod çözme | +| `serde` / `toml` | 1.0 / 0.8 | Branding ve cevap dosyası serileştirme | +| `zbus` | 4.0 | D-Bus iletişimi (COMAR) | +| `tokio` | 1.0 (full) | Asenkron iş kuyruğu | +| `sysinfo` | 0.30 | Sistem bilgisi (RAM, disk, OS) | +| `rust-i18n` | 3.1 | Uluslararasılaştırma | +| `chrono` | 0.4 | Tarih/saat işlemleri | + +## Lisans + +GNU General Public License v3.0 + +Bu proje Pisi GNU/Linux ekibi tarafından geliştirilmektedir. diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..5c385ee --- /dev/null +++ b/README_en.md @@ -0,0 +1,112 @@ +# YALI (Yet Another Linux Installer) — Rust / egui + +A rewritten **Pisi GNU/Linux** installation tool in Rust and egui. YALI installs the system from the live environment, installs packages, and performs basic configuration. It is a modern, fast, and maintainable rewrite of the original Python/Qt version. + +## Features + +- **10-step installation wizard**: Welcome → Location → Keyboard → Network → Disk Partitioning → Users → Bootloader → Summary → Installation → Finish +- **Two modes**: Graphical interface (default) and automatic installation with answer file (`--auto-install answer.toml`) +- **Demo mode**: Skips system checks, simulates installation (`--demo`) +- **Disk partitioning**: Automatic (GPT/MBR) or manual, LVM and LUKS encryption support +- **Network configuration**: WiFi scanning (nmcli), proxy settings +- **Bootloader**: GRUB device/password/timeout settings, kernel parameters +- **Rescue mode**: Chroot, GRUB reinstallation, Pisi rollback +- **OEM mode**: Predefined settings for manufacturer pre-installation +- **Internationalization**: Turkish and English (with rust-i18n via TOML locale files) +- **Branding system**: Runtime-loadable `branding.toml` for logo, slides, color settings +- **Theme**: Dark/light theme, atomic color management, custom gradient buttons +- **Async job queue**: All installation steps run in separate `tokio` runtime, UI updated via `mpsc` channel +- **System checks**: 8 pre-checks including RAM, disk space, internet, UEFI, CPU, NVMe +- **Slideshow**: Custom branded slides for 9 desktop environments +- **Demo session cleanup**: Removes live environment artifacts, adds production repository + +## Usage + +```bash +# Normal installation (requires root) +sudo cargo run --release + +# Demo mode (no root required) +cargo run --release -- --demo + +# Automatic installation (with answer file) +sudo cargo run --release -- --auto-install answer.toml +``` + +## Project Structure + +``` +├── src/ +│ ├── main.rs # Entry point, eframe App, GUI layout +│ ├── installer.rs # InstallerStep trait, GlobalState, WelcomeStep +│ ├── funct.rs # Shared UI helpers (form fields, language selector) +│ ├── branding.rs # Branding.toml loader (logo, slides, theme, OEM) +│ ├── ui/ +│ │ ├── theme.rs # Colors, fonts, Visuals, section_heading, error/warning/success boxes +│ │ ├── buttons.rs # Gradient buttons (primary, secondary, danger, etc.) +│ │ ├── combobox.rs # ComboBox helpers (show_combo_box, selectable_value) +│ │ ├── combobox_stil.rs # ComboBox visual constants (weak_bg, border, text colors) +│ │ ├── slideshow.rs # Auto-advancing slideshow +│ │ └── error_screen.rs # Error screen (log viewer, copy, retry) +│ ├── steps/ +│ │ ├── location.rs # Location/timezone + date/time +│ │ ├── license.rs # License agreement +│ │ ├── keyboard.rs # Keyboard layout/variant selection +│ │ ├── network.rs # Network configuration (WiFi, proxy) +│ │ ├── partition.rs # Disk partitioning (automatic + manual + LVM) +│ │ ├── users.rs # User account creation +│ │ ├── bootloader.rs # GRUB configuration +│ │ ├── summary.rs # Installation summary +│ │ ├── execution.rs # Installation execution (progress bar, log) +│ │ ├── finish.rs # Completion screen +│ │ ├── netinstall.rs # Extra package group selection +│ │ └── rescue.rs # Rescue mode (chroot, GRUB, Pisi takeback) +│ ├── jobs/mod.rs # Async job queue and all installation jobs +│ └── autoinstall/ +│ ├── mod.rs # Answer file loading and state application +│ └── checker.rs # System pre-checks +├── locales/ +│ ├── tr.toml # Turkish translations +│ └── en.toml # English translations +├── assets/ +│ ├── icons/ # UI icons (YALI_icon_LIGHT.svg, language-icon.svg, etc.) +│ ├── flags/ # Country flags (~250 SVG) +│ └── *.png # Slide and logo images +├── branding.toml # Default branding configuration +├── branding_en.toml # English branding +├── branding_tr.toml # Turkish branding +└── answer-example.toml # Example answer file +``` + +## Installation Workflow + +1. **Welcome** — Language selection, system checks, rescue mode +2. **Location / Time** — Timezone selection (region/area dual ComboBox), GeoIP, NTP or manual date/time +3. **Keyboard** — Layout/variant selection, search, live preview (setxkbmap) +4. **Network** — WiFi scanning, connection, proxy +5. **Disk Partitioning** — Automatic (guided) or manual, LVM, LUKS +6. **Users** — Account creation, password strength indicator, root password, autologin +7. **Bootloader** — GRUB device, timeout, password, kernel parameters +8. **Summary** — Review all selections +9. **Installation** — Slideshow + progress bar + live log +10. **Finish** — Successful installation, reboot or return to live desktop + +## Dependencies + +| Dependency | Version | Purpose | +|---|---|---| +| `eframe` | 0.27 (wgpu) | egui framework, WebGPU backend | +| `egui_extras` | 0.27 (image, file, svg) | Image loading, file dialogs, SVG | +| `image` | 0.24 | PNG decoding | +| `serde` / `toml` | 1.0 / 0.8 | Branding and answer file serialization | +| `zbus` | 4.0 | D-Bus communication (COMAR) | +| `tokio` | 1.0 (full) | Async job queue | +| `sysinfo` | 0.30 | System information (RAM, disk, OS) | +| `rust-i18n` | 3.1 | Internationalization | +| `chrono` | 0.4 | Date/time operations | + +## License + +GNU General Public License v3.0 + +This project is developed by the Pisi GNU/Linux team. diff --git a/locales/en.toml b/locales/en.toml index a10fc3c..bb05cd3 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -61,6 +61,10 @@ password_label = "Password (at least 8 characters...)" password_confirm_label = "Confirm password" hostname_label = "Computer name" username_invalid = "Invalid username. Must start with a lowercase letter or underscore, max 32 characters." +username_too_long = "Username too long (maximum 32 characters)" +username_forbidden = "This username is reserved by the system and is forbidden" +username_invalid_start = "Username must start with a lowercase letter or underscore" +username_invalid_chars = "Only lowercase letters, digits, underscore and hyphen are allowed" hostname_invalid = "Invalid hostname. Only letters, digits, and hyphens are allowed." passwords_no_match = "Passwords do not match" diff --git a/locales/tr.toml b/locales/tr.toml index 140a547..c4e97b3 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -61,6 +61,10 @@ password_label = "Parola (en az 8 karakter olmalıdır...)" password_confirm_label = "Parola (tekrar)" hostname_label = "Bilgisayar adı" username_invalid = "Geçersiz kullanıcı adı. Küçük harf veya alt çizgi ile başlamalı, en fazla 32 karakter." +username_too_long = "Kullanıcı adı çok uzun (maksimum 32 karakter)" +username_forbidden = "Bu kullanıcı adı sistem tarafından kullanılıyor ve yasaklı" +username_invalid_start = "Kullanıcı adı küçük harf veya alt çizgi ile başlamalı" +username_invalid_chars = "Sadece küçük harf, rakam, alt çizgi ve tire kullanılabilir" hostname_invalid = "Geçersiz bilgisayar adı. Yalnızca harf, rakam ve tire içerebilir." passwords_no_match = "Parolalar eşleşmiyor" diff --git a/src/installer.rs b/src/installer.rs index ef1e25b..f81360e 100644 --- a/src/installer.rs +++ b/src/installer.rs @@ -303,7 +303,7 @@ impl Default for GlobalState { oem_password_changed: false, selected_package_groups: Vec::new(), display_manager: "sddm".to_string(), - autologin: true, + autologin: false, desktop_environment: detect_desktop_environment(), branding_icons: std::collections::HashMap::new(), branding_pisi_dark: String::new(), diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index d80a50d..d3d74d1 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -1265,10 +1265,22 @@ impl Job for ConfigureDisplayManagerJob { match dm.as_str() { "sddm" => { let conf = format!("{}/usr/lib/sddm/sddm.conf.d/sddm.conf", self.mount); + let session = match self.desktop_environment.as_str() { + "plasma" => "plasmax11.desktop", + "xfce" => "xfce.desktop", + "gnome" => "gnome.desktop", + "cinnamon" => "cinnamon.desktop", + "budgie" => "budgie-desktop.desktop", + "lxde" => "LXDE.desktop", + "lxqt" => "lxqt.desktop", + "mate" => "mate.desktop", + "lumina" => "lumina-desktop.desktop", + _ => "plasmax11.desktop", + }; let content = if self.autologin { format!( - "[Autologin]\nUser={}\nSession=plasmax11.desktop\n\n[Theme]\nCurrent=pisilinux24\n", - self.username + "[Autologin]\nUser={}\nSession={}\n\n[Theme]\nCurrent=pisilinux24\n", + self.username, session ) } else { "[Autologin]\nUser=\nSession=\n\n[Theme]\nCurrent=pisilinux24\n".to_string() @@ -1299,10 +1311,22 @@ impl Job for ConfigureDisplayManagerJob { let dir = format!("{}/etc/gdm", self.mount); let _ = tokio::fs::create_dir_all(&dir).await; let conf = format!("{}/etc/gdm/custom.conf", self.mount); + let session = match self.desktop_environment.as_str() { + "plasma" => "plasmax11.desktop", + "xfce" => "xfce.desktop", + "gnome" => "gnome.desktop", + "cinnamon" => "cinnamon.desktop", + "budgie" => "budgie-desktop.desktop", + "lxde" => "LXDE.desktop", + "lxqt" => "lxqt.desktop", + "mate" => "mate.desktop", + "lumina" => "lumina-desktop.desktop", + _ => "gnome.desktop", + }; let content = if self.autologin { format!( - "[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\n", - self.username + "[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\nDefaultSession={}\n", + self.username, session ) } else { "[daemon]\n".to_string() diff --git a/src/main.rs b/src/main.rs index d7a4b7e..ef62488 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ mod branding; mod funct; use installer::{GlobalState, InstallerStep, WelcomeStep}; -use steps::{LocationStep, KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep}; +use steps::{LocationStep, KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep}; use eframe::egui; use rust_i18n::t; @@ -231,7 +231,6 @@ impl Default for YaliApp { Box::new(PartitionStep::default()), Box::new(UsersStep::default()), Box::new(BootloaderStep::default()), - Box::new(DisplayManagerStep::default()), Box::new(SummaryStep), Box::new(ExecutionStep::default()), Box::new(FinishStep), diff --git a/src/steps/bootloader.rs b/src/steps/bootloader.rs index 1d06933..a23a953 100644 --- a/src/steps/bootloader.rs +++ b/src/steps/bootloader.rs @@ -2,6 +2,7 @@ use eframe::egui; use rust_i18n::t; use crate::installer::{GlobalState, InstallerStep}; use crate::ui::theme; +use crate::funct::{form_label, form_input_text, form_input_password}; #[derive(Default)] pub struct BootloaderStep { @@ -26,77 +27,79 @@ impl InstallerStep for BootloaderStep { } fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { - ui.heading(t!("bootloader_title")); - ui.label(t!("bootloader_description")); + theme::section_heading(ui, &t!("bootloader_title")); + ui.label( + egui::RichText::new(t!("bootloader_description")) + .color(theme::c_text_dim()) + .size(13.0), + ); ui.add_space(12.0); - egui::Grid::new("bootloader_grid") - .num_columns(2) - .spacing([12.0, 10.0]) + // ─── Önyükleyici Formu ────────────────────────────────── + egui::Frame::group(ui.style()) + .fill(theme::c_bg_widget()) + .rounding(6.0) + .inner_margin(egui::Margin::symmetric(10.0, 10.0)) .show(ui, |ui| { - // Hedef aygıt - ui.label(t!("bootloader_device_label")); - ui.horizontal(|ui| { - let mut device_list: Vec = state.available_disks.iter() - .map(|d| d.name.clone()) - .collect(); - if device_list.is_empty() { - device_list.push(state.bootloader_device.clone()); - } - let current = if state.bootloader_device.is_empty() { - device_list[0].clone() - } else { - state.bootloader_device.clone() - }; - theme::show_combo_box(ui, "boot_device_select", current.as_str(), ui.available_width(), |ui, w| { - for dev in &device_list { - theme::selectable_value(ui, &mut state.bootloader_device, dev.clone(), dev.as_str(), w); + ui.set_width(ui.available_width()); + ui.vertical(|ui| { + ui.add_space(10.0); + + // Hedef aygıt + form_label(ui, &t!("bootloader_device_label")); + ui.horizontal(|ui| { + let mut device_list: Vec = state.available_disks.iter() + .map(|d| d.name.clone()) + .collect(); + if device_list.is_empty() { + device_list.push(state.bootloader_device.clone()); } + let current = if state.bootloader_device.is_empty() { + device_list[0].clone() + } else { + state.bootloader_device.clone() + }; + theme::show_combo_box(ui, "boot_device_select", current.as_str(), ui.available_width(), |ui, w| { + for dev in &device_list { + theme::selectable_value(ui, &mut state.bootloader_device, dev.clone(), dev.as_str(), w); + } + }); }); ui.label( egui::RichText::new(t!("bootloader_device_hint")) .size(11.0) - .color(egui::Color32::from_rgb(120, 120, 140)), + .color(theme::c_text_dim()), ); - }); - ui.end_row(); + ui.add_space(6.0); - // Timeout - ui.label(t!("bootloader_timeout_label")); - ui.horizontal(|ui| { - ui.add(egui::Slider::new(&mut state.bootloader_timeout, 0..=60) - .text(t!("bootloader_timeout_suffix"))); - }); - ui.end_row(); + // Timeout + form_label(ui, &t!("bootloader_timeout_label")); + ui.horizontal(|ui| { + ui.add(egui::Slider::new(&mut state.bootloader_timeout, 0..=60) + .text(t!("bootloader_timeout_suffix"))); + }); + ui.add_space(6.0); - // GRUB şifresi - ui.label(t!("bootloader_password_label")); - ui.horizontal(|ui| { - if self.show_password { - ui.text_edit_singleline(&mut state.bootloader_password); - } else { - ui.add(egui::TextEdit::singleline(&mut state.bootloader_password).password(true)); - } - if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() { - self.show_password = !self.show_password; - } - if !state.bootloader_password.is_empty() { - ui.colored_label(egui::Color32::from_rgb(200, 180, 40), "⚠"); - } - }); - ui.end_row(); + // GRUB şifresi + form_label(ui, &t!("bootloader_password_label")); + ui.horizontal(|ui| { + let show_pwd = &mut self.show_password; + form_input_password(ui, &mut state.bootloader_password, show_pwd); + }); + ui.add_space(6.0); - // Kernel parametreleri - ui.label(t!("bootloader_kernel_label")); - ui.horizontal(|ui| { - ui.text_edit_singleline(&mut state.kernel_options); + // Kernel parametreleri + form_label(ui, &t!("bootloader_kernel_label")); + ui.horizontal(|ui| { + form_input_text(ui, &mut state.kernel_options); + }); ui.label( egui::RichText::new(t!("bootloader_kernel_hint")) .size(11.0) - .color(egui::Color32::from_rgb(120, 120, 140)), + .color(theme::c_text_dim()), ); + }); - ui.end_row(); }); } diff --git a/src/steps/mod.rs b/src/steps/mod.rs index 92986d0..ace9757 100644 --- a/src/steps/mod.rs +++ b/src/steps/mod.rs @@ -7,7 +7,7 @@ pub mod partition; pub mod bootloader; pub mod network; pub mod rescue; -pub mod display_manager; +//pub mod display_manager; pub mod netinstall; pub mod location; @@ -20,5 +20,4 @@ pub use partition::PartitionStep; pub use bootloader::BootloaderStep; pub use network::NetworkStep; pub use rescue::RescueStep; -pub use display_manager::DisplayManagerStep; pub use location::LocationStep; diff --git a/src/steps/users.rs b/src/steps/users.rs index dc6ded2..a5654d9 100644 --- a/src/steps/users.rs +++ b/src/steps/users.rs @@ -6,16 +6,18 @@ use crate::funct::{ form_input_text, form_input_password, form_label, form_check /// Kullanıcı adı, şifre, hostname ve yönetici (root) şifresi ayarları adımı. pub struct UsersStep { use_same_for_admin: bool, + autologin: bool, show_password: bool, show_password_confirm: bool, - show_root_password: bool, // <-- Eklendi - show_root_password_confirm: bool, // <-- Eklendi + show_root_password: bool, + show_root_password_confirm: bool, } impl Default for UsersStep { fn default() -> Self { Self { use_same_for_admin: true, + autologin: false, show_password: false, show_password_confirm: false, show_root_password: false, @@ -30,16 +32,22 @@ impl Default for UsersStep { /// - Boşlukları `-`'ye çevirir /// - Geçersiz karakterleri temizler fn derive_username(real_name: &str) -> String { - let mut name = real_name.to_lowercase(); - name = name + let name = real_name .replace('ş', "s") + .replace('Ş', "s") .replace('ğ', "g") + .replace('Ğ', "g") .replace('ı', "i") + .replace('İ', "i") .replace('ö', "o") + .replace('Ö', "o") .replace('ü', "u") + .replace('Ü', "u") .replace('ç', "c") + .replace('Ç', "c") .replace(' ', "-") - .replace('_', "-"); + .replace('_', "-") + .to_lowercase(); 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() { @@ -54,17 +62,44 @@ fn derive_username(real_name: &str) -> String { /// 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. -fn is_valid_username(s: &str) -> bool { - if s.is_empty() || s.len() > 32 { - return false; +/// Ayrıca sistem kullanıcı adları yasaklıdır. +/// Hata varsa hata mesajını döndürür, yoksa None döndürür. +fn validate_username(s: &str) -> Option { + if s.is_empty() { + return None; } + if s.len() > 32 { + return Some(t!("username_too_long").to_string()); + } + + // Yasaklı sistem kullanıcı adları + let forbidden = [ + "root", "daemon", "bin", "sys", "sync", "games", "man", "lp", + "mail", "news", "uucp", "proxy", "www-data", "backup", "list", + "irc", "gnats", "nobody", "systemd-timesync", "systemd-network", + "systemd-resolve", "systemd-bus-proxy", "messagebus", "sshd", + "ftp", "apache", "nginx", "mysql", "postgres", "docker", + ]; + if forbidden.contains(&s) { + return Some(t!("username_forbidden").to_string()); + } + let mut chars = s.chars(); // İlk karakter küçük harf veya alt çizgi olmalı match chars.next() { Some(c) if c.is_ascii_lowercase() || c == '_' => {} - _ => return false, + _ => return Some(t!("username_invalid_start").to_string()), } - chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-') + if !chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-') { + return Some(t!("username_invalid_chars").to_string()); + } + + None +} + +/// Kullanıcı adının geçerliliğini kontrol eder (basit bool versiyonu). +fn is_valid_username(s: &str) -> bool { + validate_username(s).is_none() } /// Hostname kuralları: 1-63 karakter, yalnızca [a-zA-Z0-9-], tire ile başlayamaz/bitemez. @@ -121,6 +156,9 @@ impl InstallerStep for UsersStep { } fn on_enter(&mut self, state: &mut GlobalState) { + // Autologin senkronizasyonu + self.autologin = state.autologin; + // 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); @@ -168,27 +206,54 @@ impl InstallerStep for UsersStep { form_label(ui, &t!("real_name_label")); { let prev = state.real_name.clone(); - form_input_text(ui, &mut state.real_name); + ui.horizontal(|ui| { + form_input_text(ui, &mut state.real_name); + + + if !state.real_name.is_empty() { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/check.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_success()) + ); + }else{ + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_error()) + ); + } + }); + if state.real_name != prev && !state.real_name.is_empty() { state.username = derive_username(&state.real_name); } } - if !state.real_name.is_empty() { - ui.colored_label(theme::c_success(), "✅"); - } else { - ui.colored_label(theme::c_error(), "❌"); - } ui.add_space(6.0); // Kullanıcı adı (otomatik türetilir, düzenlenebilir) form_label(ui, &t!("username_label")); - form_input_text(ui, &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_warning(), t!("username_invalid")); + + ui.horizontal(|ui| { + form_input_text(ui, &mut state.username); + if !state.username.is_empty() { + if let Some(_err) = validate_username(&state.username) { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_error()) + ); + } else { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/check.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_success()) + ); + } } + }); + if let Some(err) = validate_username(&state.username) { + ui.colored_label(theme::c_error(), err); } ui.add_space(6.0); @@ -221,27 +286,50 @@ impl InstallerStep for UsersStep { form_label(ui, &t!("password_confirm_label")); { let show_pwd = &mut self.show_password_confirm; - form_input_password(ui, &mut state.password_confirm, show_pwd); + ui.horizontal(|ui| { + form_input_password(ui, &mut state.password_confirm, show_pwd); + if !state.password_confirm.is_empty() { + if state.password == state.password_confirm { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/check.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_success()) + ); + } else { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_error()) + ); + } + } + }); } - if !state.password_confirm.is_empty() { - if state.password == state.password_confirm { - ui.colored_label(theme::c_success(), "✅"); - } else { - ui.colored_label(theme::c_warning(), t!("passwords_no_match")); - } + if !state.password_confirm.is_empty() && state.password != state.password_confirm { + ui.colored_label(theme::c_warning(), t!("passwords_no_match")); } ui.add_space(6.0); // Hostname form_label(ui, &t!("hostname_label")); - form_input_text(ui, &mut state.hostname); - if !state.hostname.is_empty() { - if is_valid_hostname(&state.hostname) { - ui.colored_label(theme::c_success(), "✅"); - } else { - ui.colored_label(theme::c_warning(), t!("hostname_invalid")); + ui.horizontal(|ui| { + form_input_text(ui, &mut state.hostname); + if !state.hostname.is_empty() { + if is_valid_hostname(&state.hostname) { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/check.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_success()) + ); + } else { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_error()) + ); + } } - } + }); }); ui.add_space(10.0); @@ -250,9 +338,13 @@ impl InstallerStep for UsersStep { ui.add_space(14.0); + // ─── Otomatik Oturum Açma ─────────────────────────────────────── + form_checkbox(ui, &mut self.autologin, &t!("display_manager_autologin")); + ui.add_space(8.0); + // ─── Yönetici (root) Ayarları ───────────────────────────────────── form_checkbox(ui, &mut self.use_same_for_admin, &t!("use_same_for_admin")); - + if self.use_same_for_admin { state.root_password = state.password.clone(); state.root_password_confirm = state.password_confirm.clone(); @@ -281,16 +373,33 @@ impl InstallerStep for UsersStep { // DÜZELTME: 3. parametre olarak show_root_password_confirm eklendi let show_root_pwd_conf = &mut self.show_root_password_confirm; - form_input_password(ui, &mut state.root_password_confirm, show_root_pwd_conf); + ui.horizontal(|ui| { + form_input_password(ui, &mut state.root_password_confirm, show_root_pwd_conf); + if !state.root_password_confirm.is_empty() { + if state.root_password == state.root_password_confirm { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/check.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_success()) + ); + } else { + ui.add( + egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg")) + .max_width(18.0).max_height(18.0) + .tint(theme::c_error()) + ); + } + } + }); - if state.root_password == state.root_password_confirm { - ui.colored_label(theme::c_success(), "✅"); - } else { + if !state.root_password_confirm.is_empty() && state.root_password != state.root_password_confirm { ui.colored_label(theme::c_error(), t!("passwords_no_match")); } }); } + // Autologin değerini state'e kaydet + state.autologin = self.autologin; } fn is_complete(&self, state: &GlobalState) -> bool { diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 6d6a13d..5f0e873 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -60,28 +60,29 @@ pub fn set_theme_mode(is_dark: bool) { BG_PANEL.store(0x1A1A1E, Ordering::Relaxed); BG_WIDGET.store(0x242428, Ordering::Relaxed); BORDER.store(0x323238, Ordering::Relaxed); - TEXT.store(0xF3F3F5, Ordering::Relaxed); - TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed); + TEXT.store(0xF3F3F5, Ordering::Relaxed); // #F3F3F5 + TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed); // #9E9EAF SIDEBAR.store(0x0B0B0D, Ordering::Relaxed); + SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed); KEYBOARD_BG.store(0x000000, Ordering::Relaxed); BG_ROOT.store(0x1A1A1E, Ordering::Relaxed); } else { BG_FOOTER.store(0xf7f7f7, Ordering::Relaxed); BG_DARK.store(0xf7f7f7, Ordering::Relaxed); SIDEBAR.store(0x292f34, Ordering::Relaxed); - TEXT.store(0x000000, Ordering::Relaxed); - TEXT_DIM.store(0x000000, Ordering::Relaxed); + TEXT.store(0x000000, Ordering::Relaxed); // #000000 + TEXT_DIM.store(0x000000, Ordering::Relaxed); // #000000 SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed); KEYBOARD_BG.store(0xffffff, Ordering::Relaxed); BG_ROOT.store(0xf7f7f7, Ordering::Relaxed); - BG_DARK.store(0xF0F0F5, Ordering::Relaxed); + //BG_DARK.store(0xF0F0F5, Ordering::Relaxed); BG_PANEL.store(0xFFFFFF, Ordering::Relaxed); BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed); BORDER.store(0xD0D0DA, Ordering::Relaxed); - TEXT.store(0x20202A, Ordering::Relaxed); - TEXT_DIM.store(0x60606A, Ordering::Relaxed); - SIDEBAR.store(0xE4E4EA, Ordering::Relaxed); + //TEXT.store(0x20202A, Ordering::Relaxed); // #20202A + //TEXT_DIM.store(0x60606A, Ordering::Relaxed); // #60606A + //SIDEBAR.store(0xE4E4EA, Ordering::Relaxed); } }