forked from pisilinux-rs/yali-rs
489 lines
20 KiB
Rust
489 lines
20 KiB
Rust
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);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
} |