Files
yali-rs/src/main.rs
T

489 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
mod installer;
mod steps;
mod jobs;
mod ui;
mod autoinstall;
mod branding;
use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep};
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep};
use eframe::egui;
use rust_i18n::t;
rust_i18n::i18n!("locales");
fn main() -> eframe::Result<()> {
// ── CLI: otomatik kurulum modu ───────────────────────
let args: Vec<String> = 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 <dosya.toml> ş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::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::<InstallMessage>();
// 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::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<Box<dyn InstallerStep>>,
rescue_step: Option<Box<dyn InstallerStep>>,
last_step: usize,
branding: branding::BrandingConfig,
loaded_language: String,
}
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(DisplayManagerStep::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(),
};
// 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());
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_text(), label);
});
} else if i < cur {
ui.horizontal(|ui| {
ui.colored_label(ui::theme::c_success(), "");
ui.colored_label(ui::theme::c_text_dim(), label);
});
} else {
ui.colored_label(
egui::Color32::from_rgb(80, 80, 100),
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_bg_panel())
.stroke(egui::Stroke::new(1.0, ui::theme::c_border())))
.show(ctx, |ui| {
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.add_space(8.0);
if !is_exec && !is_finish && ui.add(ui::theme::secondary_button(&t!("cancel"))).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
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")
};
let btn = if is_valid {
ui::theme::primary_button(&next_label)
} else {
ui::theme::secondary_button(&next_label)
};
if ui.add_enabled(is_valid, btn).clicked() {
self.state.current_step += 1;
}
}
if cur > 0 && !is_exec {
ui.add_space(8.0);
if ui.add(ui::theme::secondary_button(&t!("back"))).clicked() {
self.state.current_step -= 1;
}
}
}
});
});
ui.add_space(10.0);
});
// ── 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);
}
});
});
}
}