mod installer; mod steps; mod jobs; mod ui; mod autoinstall; mod branding; mod funct; use installer::{GlobalState, InstallerStep, WelcomeStep}; use steps::{LocationStep, KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep}; use eframe::egui; use rust_i18n::t; rust_i18n::i18n!("locales"); fn main() -> eframe::Result<()> { // ── CLI: otomatik kurulum modu ─────────────────────── let args: Vec = std::env::args().collect(); let demo_mode = args.contains(&"--demo".to_string()); if let Some(pos) = args.iter().position(|a| a == "--auto-install") { if let Some(path) = args.get(pos + 1) { return run_auto_install(path, demo_mode); } else { eprintln!("Hata: --auto-install şeklinde kullanın."); std::process::exit(1); } } run_gui(demo_mode) } /// Grafik arayüzü başlatır (normal mod). fn run_gui(demo_mode: bool) -> eframe::Result<()> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([1200.0, 900.0]) .with_min_inner_size([1080.0, 720.0]) .with_title(t!("installer")), ..Default::default() }; eframe::run_native( t!("installer").as_ref(), options, 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(true); ui::theme::apply(&cc.egui_ctx, true); let mut app = YaliApp::default(); app.state.demo_mode = demo_mode; Box::new(app) }), ) } /// Answer file ile tam otomatik, UI-siz kurulum. fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> { use autoinstall::{load, apply_to_state}; use std::path::Path; println!("=== Yali-RS Otomatik Kurulum ==="); println!("{}: {}", t!("answer_file"), path); // Dosyayı yükle ve doğrula let af = match load(Path::new(path)) { Ok(af) => af, Err(e) => { eprintln!("{}: {}", t!("error"), e); std::process::exit(1); } }; println!("{}: {}", t!("disk"), af.install.disk); println!("{}: {}", t!("timezone"), af.install.timezone); println!("{}: {}", t!("username"), af.user.username); println!("{}: {}", t!("hostname"), af.user.hostname); // Sistem kontrollerini çalıştır println!("\n--- {} ---", t!("system_checks")); let checks = autoinstall::checker::run_all(); for c in &checks { let sym = match c.status { autoinstall::checker::CheckStatus::Pass => "✓", autoinstall::checker::CheckStatus::Warn => "⚠", autoinstall::checker::CheckStatus::Fail => "✗", }; println!("[{}] {}: {}", sym, c.name, c.message); } if !autoinstall::checker::all_passed(&checks) { eprintln!("{}: Critical check error. Installation stopped.", t!("error")); std::process::exit(2); } // State'i hazırla let mut state = GlobalState { demo_mode, ..Default::default() }; apply_to_state(&af, &mut state); if state.partition_plan.is_none() { eprintln!("{}: {}: {}", t!("error"), t!("partition_plan_could_not_be_created"), af.install.disk); std::process::exit(3); } println!("\n--- {} ---", t!("installation_started")); // JobQueue'yu oluştur ve tokio'da çalıştır let locale = af.install.locale.clone(); let timezone = af.install.timezone.clone(); let username = af.user.username.clone(); let password = af.user.password.clone(); let hostname = af.user.hostname.clone(); let kb_layout = af.install.keyboard.clone(); let kb_variant = af.install.keyboard_variant.clone(); let source = af.install.source.clone(); let mount = af.install.mount.clone(); let plan = state.partition_plan.unwrap(); let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); rt.block_on(async { use std::sync::mpsc; use jobs::{InstallMessage, UiSender}; // CLI modunda egui Context yok; basit bir mpsc kanal kullanıyoruz. let (tx, rx) = mpsc::channel::(); // Arka plan iş parçacığı — gerçek job kuyruğunu çalıştırır. let cli_ctx = eframe::egui::Context::default(); let ui_sender = UiSender::new(tx, cli_ctx); let plan_c = plan.clone(); let locale_c = locale.clone(); let timezone_c = timezone.clone(); let username_c = username.clone(); let password_c = password.clone(); let hostname_c = hostname.clone(); let kb_layout_c = kb_layout.clone(); let kb_variant_c= kb_variant.clone(); let source_c = source.clone(); let mount_c = mount.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); let mut queue = jobs::build_full_job_queue( plan_c, jobs::JobQueueConfig { mount: mount_c, source: source_c, locale: locale_c, timezone: timezone_c, username: username_c, password: password_c, hostname: hostname_c, kb_layout: kb_layout_c, kb_variant: kb_variant_c, demo_mode, boot_device: String::new(), bootloader_timeout: 5, bootloader_password: String::new(), kernel_options: String::new(), use_ntp: true, root_password: String::new(), display_manager: "sddm".to_string(), autologin: true, desktop_environment: "plasma".to_string(), selected_package_groups: Vec::new(), manual_year: 2026, manual_month: 5, manual_day: 26, manual_hour: 12, manual_minute: 0, }, ); rt.block_on(queue.run_all(ui_sender)); }); // Kanalı oku ve terminale yaz loop { match rx.recv() { Ok(InstallMessage::Log(line)) => println!(" {}", line), Ok(InstallMessage::Progress(p)) => { 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; } Ok(InstallMessage::Error(e)) => { eprintln!("\n{}: {}", t!("error"), e); std::process::exit(4); } Err(_) => break, } } }); Ok(()) } struct YaliApp { state: GlobalState, steps: Vec>, rescue_step: Option>, last_step: usize, branding: branding::BrandingConfig, loaded_language: String, #[allow(dead_code)] show_cancel_dialog: bool, } impl Default for YaliApp { fn default() -> Self { let default_lang = "tr".to_string(); let branding = branding::BrandingConfig::load_for_lang(&default_lang); let mut app = Self { state: GlobalState::default(), steps: vec![ Box::new(WelcomeStep::default()), Box::new(LocationStep::default()), Box::new(KeyboardStep::default()), Box::new(NetworkStep::default()), Box::new(PartitionStep::default()), Box::new(UsersStep::default()), Box::new(BootloaderStep::default()), Box::new(SummaryStep), Box::new(ExecutionStep::default()), Box::new(FinishStep), ], rescue_step: Some(Box::new(steps::RescueStep::default())), last_step: usize::MAX, branding: branding.clone(), loaded_language: String::new(), show_cancel_dialog: false, }; // OEM modu: varsayılan değerleri ata ve step engelleme if branding.oem.enabled { app.state.oem_mode = true; if !branding.oem.default_username.is_empty() { app.state.username = branding.oem.default_username.clone(); app.state.hostname = branding.oem.default_hostname.clone(); } if !branding.oem.default_password.is_empty() { app.state.password = branding.oem.default_password.clone(); app.state.password_confirm = branding.oem.default_password.clone(); // OEM varsayılan şifresi yüklendi; kullanıcı değiştirene kadar // ilerleme engellenir. app.state.oem_password_changed = false; } // OEM ön tanımlı disk planı varsa? } // Branding görsel verilerini GlobalState'e enjekte et app.state.branding_yali_dark = branding.yali_logo_uri(); app.state.branding_yali_light = branding.yali_logo_uri(); app.state.branding_pisi_dark = branding.pisi_logo_dark_uri(); app.state.branding_pisi_light = branding.pisi_logo_light_uri(); app.state.branding_icons = branding.slides.icons .iter() .map(|(k, v)| (k.clone(), branding::path_to_file_uri(v))) .collect(); app.state.branding_slides = branding.slides.items.clone(); app.steps[0].on_enter(&mut app.state); app.last_step = 0; app } } impl eframe::App for YaliApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { rust_i18n::set_locale(&self.state.language); // Dil değişimi kontrolü -> branding dosyasını dinamik olarak yeniden yükle if self.loaded_language != self.state.language { self.loaded_language = self.state.language.clone(); let branding = branding::BrandingConfig::load_for_lang(&self.loaded_language); self.branding = branding.clone(); self.state.branding_yali_dark = branding.yali_logo_uri(); self.state.branding_yali_light = branding.yali_logo_uri(); self.state.branding_pisi_dark = branding.pisi_logo_dark_uri(); self.state.branding_pisi_light = branding.pisi_logo_light_uri(); self.state.branding_icons = branding.slides.icons .iter() .map(|(k, v)| (k.clone(), branding::path_to_file_uri(v))) .collect(); self.state.branding_slides = branding.slides.items.clone(); // Pencere başlığını dinamik olarak güncelle ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.branding.general.title.clone())); } // Rescue modu: tam ekran, sidebar yok, navigasyon yok if self.state.rescue_mode { if let Some(rescue) = &mut self.rescue_step { egui::CentralPanel::default() .frame(egui::Frame::none() .fill(ui::theme::c_bg_dark()) .inner_margin(egui::Margin::same(20.0))) .show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { rescue.show(ui, &mut self.state); }); ui.add_space(12.0); if ui.button(t!("rescue_return")).clicked() { self.state.rescue_mode = false; } }); } return; } let cur = self.state.current_step; let n_steps = self.steps.len(); let is_exec = cur == n_steps - 2; let is_finish = cur == n_steps - 1; // Adım değişmişse on_enter çağır if cur != self.last_step { if let Some(step) = self.steps.get_mut(cur) { step.on_enter(&mut self.state); } self.last_step = cur; } // ── Sol panel: adım listesi ────────────────────────── egui::SidePanel::left("sidebar") .exact_width(195.0) .frame(egui::Frame::none().fill(ui::theme::c_sidebar())) .show(ctx, |ui| { ui.add_space(20.0); // Dağıtım logosu — branding.toml'dan dinamik yüklenir ui.vertical_centered(|ui| { let logo_uri = if self.state.is_dark { &self.state.branding_yali_dark } else { &self.state.branding_yali_light }; if !logo_uri.is_empty() { ui.add( egui::Image::from_uri(logo_uri.as_str()) .max_width(120.0) .rounding(egui::Rounding::same(8.0)), ); } ui.add_space(8.0); // ui.label( // egui::RichText::new(t!("app_name")) // .strong() // .size(16.0) // .color(ui::theme::c_text_dim()), // ); // ui.colored_label( // egui::Color32::from_rgb(60, 60, 90), // format!("{}: {}", t!("version"), env!("CARGO_PKG_VERSION")), // ); }); ui.add_space(16.0); ui.separator(); ui.add_space(12.0); // Adım listesi for (i, step) in self.steps.iter().enumerate() { //let label = format!("{}. {}", i + 1, step.name()); let label = step.name(); egui::Frame::none() .fill(if i == cur { egui::Color32::from_rgba_unmultiplied(0x21, 0x96, 0xF3, 30) } else { egui::Color32::TRANSPARENT }) //.rounding(egui::Rounding::same(6.0)) .inner_margin(egui::Margin::symmetric(10.0, 5.0)) .show(ui, |ui| { ui.set_width(175.0); if i == cur { ui.horizontal(|ui| { ui.colored_label(ui::theme::c_accent(), "▶"); ui.colored_label(ui::theme::c_sidebar_text(), label); }); } else if i < cur { ui.horizontal(|ui| { ui.colored_label(ui::theme::c_success(), "✅"); ui.colored_label(ui::theme::c_sidebar_text(), label); }); } else { ui.colored_label( egui::Color32::from_rgb(255, 255, 255), format!(" {}", label), ); } }); ui.add_space(2.0); } // Alt kısım: tema değiştirme 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); } }); }); // ── Alt panel: navigasyon ──────────────────────────── egui::TopBottomPanel::bottom("footer") .frame(egui::Frame::none() .fill(ui::theme::c_footer_bg()) .stroke(egui::Stroke::new(1.0, ui::theme::c_footer_bg()))) .show(ctx, |ui| { ui.add_space(10.0); ui.horizontal(|ui| { ui.add_space(8.0); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_space(8.0); let is_valid = self.steps .get(cur) .map(|s| s.is_complete(&self.state)) .unwrap_or(false); if !is_finish { if !is_exec || is_valid { let next_label = if is_exec { t!("finish_next") } else { t!("next") }; // if !is_exec && !is_finish && ui::theme::danger_button(ui, &t!("cancel")).clicked() { // self.show_cancel_dialog = true; // }; let next_clicked = ui::theme::primary_button_enabled(ui, &next_label, is_valid).clicked(); if next_clicked { self.state.current_step += 1; } } if cur > 0 && !is_exec { ui.add_space(8.0); if ui::theme::secondary_button(ui, &t!("back")).clicked() { self.state.current_step -= 1; } } } }); }); ui.add_space(10.0); }); // // ── İptal onay diyaloğu ─────────────────────────────── // if self.show_cancel_dialog { // egui::Window::new(t!("cancel_confirm_title")) // .collapsible(false) // .resizable(false) // .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) // .show(ctx, |ui| { // ui.label(t!("cancel_confirm_msg")); // ui.add_space(12.0); // ui.horizontal(|ui| { // if ui.button(t!("cancel_confirm_yes")).clicked() { // ctx.send_viewport_cmd(egui::ViewportCommand::Close); // } // if ui.button(t!("cancel_confirm_no")).clicked() { // self.show_cancel_dialog = false; // } // }); // }); // } // ── Merkez panel ───────────────────────────────────── egui::CentralPanel::default() .frame(egui::Frame::none() .fill(ui::theme::c_bg_dark()) .inner_margin(egui::Margin::same(20.0))) .show(ctx, |ui| { egui::ScrollArea::vertical() .id_source("main_scroll") .show(ui, |ui| { if let Some(step) = self.steps.get_mut(cur) { step.show(ui, &mut self.state); } }); }); } }