This commit is contained in:
2026-06-01 00:32:03 +03:00
parent 20e5062ecd
commit 64db88d12e
32 changed files with 6109 additions and 926 deletions
+86 -18
View File
@@ -81,12 +81,8 @@ fn check_ram() -> CheckResult {
/// En az 10 GB boş disk alanı kontrolü.
fn check_disk_space() -> CheckResult {
use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list();
let max_gb = disks.iter()
.map(|d| d.total_space() / 1024 / 1024 / 1024)
.max()
.unwrap_or(0);
// sysinfo yerine doğrudan kernel'in gördüğü blok aygıtı boyutunu alıyoruz.
let max_gb = get_max_block_device_gb();
if max_gb < 10 {
CheckResult::fail(
@@ -99,10 +95,82 @@ fn check_disk_space() -> CheckResult {
t!("check_disk_warn", size = max_gb),
)
} else {
CheckResult::pass(t!("check_disk_space"), t!("check_disk_pass", size = max_gb))
CheckResult::pass(
t!("check_disk_space"),
t!("check_disk_pass", size = max_gb)
)
}
}
use std::fs;
/// Sistemdeki en büyük fiziksel/sanal blok aygıtının boyutunu GB cinsinden döndürür.
///
/// Önce `lsblk -d` ile dener (yalnızca disk düzeyindeki aygıtlar, bölümler hariç).
/// lsblk yoksa veya başarısız olursa `/sys/block` ile fallback yapar;
/// `/sys/block` yalnızca tam diskleri içerdiğinden bölüm girişleri karışmaz.
fn get_max_block_device_gb() -> u64 {
let mut max_gb = 0u64;
// ── 1. Birincil yöntem: lsblk -d (partition.rs ile tutarlı) ──────────────
let lsblk_out = std::process::Command::new("lsblk")
.args(["-d", "-o", "NAME,SIZE", "-b", "--noheadings"])
.output();
if let Ok(o) = lsblk_out {
if o.status.success() {
let text = String::from_utf8_lossy(&o.stdout);
for line in text.lines() {
let mut tokens = line.split_whitespace();
let name = match tokens.next() { Some(n) => n, None => continue };
// loop, ram, zram, sr (CD-ROM) aygıtlarını atla
if name.starts_with("loop")
|| name.starts_with("ram")
|| name.starts_with("sr")
|| name.starts_with("zram")
{ continue; }
let size_str = match tokens.next() { Some(s) => s, None => continue };
let size_bytes: u64 = size_str.parse().unwrap_or(0);
if size_bytes == 0 { continue; }
let size_gb = size_bytes / 1_073_741_824;
if size_gb > max_gb { max_gb = size_gb; }
}
// lsblk başarılıysa ve en az bir disk bulduysa döndür
if max_gb > 0 { return max_gb; }
}
}
// ── 2. Fallback: /sys/block (yalnızca tam diskler — bölüm yok) ───────────
// /sys/class/block aksine /sys/block dizini YALNIZCA tam disk girişlerini
// içerir; sda1, nvme0n1p1 gibi bölüm sembolik bağlantıları burada bulunmaz.
if let Ok(entries) = fs::read_dir("/sys/block") {
for entry in entries.flatten() {
let name = entry.file_name().into_string().unwrap_or_default();
if name.starts_with("loop")
|| name.starts_with("ram")
|| name.starts_with("zram")
|| name.starts_with("sr")
{ continue; }
let size_path = entry.path().join("size");
if let Ok(size_str) = fs::read_to_string(&size_path) {
// sysfs 'size' dosyası: 512 byte'lık sektör sayısı
if let Ok(sectors) = size_str.trim().parse::<u64>() {
let size_gb = (sectors * 512) / 1_073_741_824;
if size_gb > max_gb { max_gb = size_gb; }
}
}
}
}
max_gb
}
/// İnternet bağlantısı kontrolü (isteğe bağlı).
fn check_internet() -> CheckResult {
// DNS çözümlemesi ile basit kontrol
@@ -150,19 +218,19 @@ fn check_cpu_arch() -> CheckResult {
}
}
/// Canlı sistem ortamı kontrolü.
/// Canlı sistem ortamı kontrolü — PyQt yali'deki gibi:
/// /bootmnt varsa kaynak orası (/bootmnt/pisi/pisi.sqfs),
/// yoksa /mnt/cdrom/pisi/pisi.sqfs kontrol edilir.
fn check_live_env() -> CheckResult {
let source = "/run/livecd/squashfs-root";
if !Path::new(source).exists() {
CheckResult::fail(
t!("check_live_sys"),
t!("check_live_fail"),
)
let sqfs = if Path::new("/bootmnt").exists() {
"/bootmnt/pisi/pisi.sqfs"
} else {
CheckResult::pass(
t!("check_live_sys"),
t!("check_live_pass"),
)
"/mnt/cdrom/pisi/pisi.sqfs"
};
if Path::new(sqfs).exists() {
CheckResult::pass(t!("check_live_sys"), t!("check_live_pass"))
} else {
CheckResult::fail(t!("check_live_sys"), t!("check_live_fail"))
}
}
+99 -183
View File
@@ -1,216 +1,132 @@
//! Otomatik kurulum (answer file) desteği.
//! Answer file yapıları, yükleme ve state'e uygulama.
//!
//! Kullanım:
//! yali-rs --auto-install /path/to/answer.toml
//!
//! Answer file formatı (TOML):
//!
//! [install]
//! disk = "/dev/sda"
//! timezone = "Europe/Istanbul"
//! locale = "tr_TR.UTF-8"
//! keyboard = "tr"
//!
//! [user]
//! username = "pisi"
//! password = "changeme"
//! hostname = "pisilinux"
use serde::Deserialize;
use std::path::Path;
//! Job kuyruğu ve Job struct'ları `src/jobs/` modülündedir.
pub mod checker;
// ─────────────────────────────────────────────
// ANSWER FILE YAPISI
// ANSWER FILE: VERİ YAPILARI
// ─────────────────────────────────────────────
#[derive(Debug, Deserialize)]
/// `[install]` bölümündeki alanlar.
#[derive(Debug, serde::Deserialize)]
pub struct InstallConfig {
pub disk: String,
#[serde(default = "default_timezone")]
pub timezone: String,
#[serde(default = "default_locale")]
pub locale: String,
#[serde(default = "default_keyboard")]
pub keyboard: String,
#[serde(default)]
pub keyboard_variant: String,
#[serde(default = "default_source")]
pub source: String,
#[serde(default = "default_mount")]
pub mount: String,
}
/// `[user]` bölümündeki alanlar.
#[derive(Debug, serde::Deserialize)]
pub struct UserConfig {
pub username: String,
pub password: String,
#[serde(default = "default_hostname")]
pub hostname: String,
}
/// Answer file'ın tamamı (`--auto-install <dosya.toml>`).
#[derive(Debug, serde::Deserialize)]
pub struct AnswerFile {
pub install: InstallConfig,
pub user: UserConfig,
}
#[derive(Debug, Deserialize)]
pub struct InstallConfig {
/// Hedef disk aygıtı. Örn: "/dev/sda", "/dev/nvme0n1"
pub disk: String,
/// Zaman dilimi. Örn: "Europe/Istanbul"
#[serde(default = "default_timezone")]
pub timezone: String,
/// Sistem locale. Örn: "tr_TR.UTF-8"
#[serde(default = "default_locale")]
pub locale: String,
/// Klavye düzeni. Örn: "tr"
#[serde(default = "default_keyboard")]
pub keyboard: String,
/// Klavye varyantı. Örn: "f" (Türkçe F için)
#[serde(default)]
pub keyboard_variant: String,
/// Kaynak dizin (canlı sistem). Varsayılan: /run/livecd/squashfs-root/
#[serde(default = "default_source")]
pub source: String,
/// Hedef bağlama noktası. Varsayılan: /mnt
#[serde(default = "default_mount")]
pub mount: String,
}
#[derive(Debug, Deserialize)]
pub struct UserConfig {
pub username: String,
pub password: String,
pub hostname: String,
}
fn default_timezone() -> String { "Europe/Istanbul".into() }
fn default_locale() -> String { "tr_TR.UTF-8".into() }
fn default_keyboard() -> String { "tr".into() }
fn default_source() -> String { "/run/livecd/squashfs-root/".into() }
fn default_mount() -> String { "/mnt".into() }
fn default_timezone() -> String { "Europe/Istanbul".to_string() }
fn default_locale() -> String { "tr_TR.UTF-8".to_string() }
fn default_keyboard() -> String { "tr".to_string() }
fn default_source() -> String { "/run/livecd/squashfs-root/".to_string() }
fn default_mount() -> String { "/mnt".to_string() }
fn default_hostname() -> String { "pisilinux".to_string() }
// ─────────────────────────────────────────────
// DOĞRULAMA
// ANSWER FILE: YÜKLE ve UYGULA
// ─────────────────────────────────────────────
#[derive(Debug)]
pub enum AnswerError {
Io(std::io::Error),
Parse(toml::de::Error),
Validation(Vec<String>),
}
impl std::fmt::Display for AnswerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AnswerError::Io(e) => write!(f, "Dosya okuma hatası: {}", e),
AnswerError::Parse(e) => write!(f, "TOML ayrıştırma hatası: {}", e),
AnswerError::Validation(v) => write!(f, "Doğrulama hataları:\n{}", v.join("\n")),
}
}
}
/// Answer dosyasını okuyup ayrıştırır ve doğrular.
pub fn load(path: &Path) -> Result<AnswerFile, AnswerError> {
/// TOML answer dosyasını diskten okur ve ayrıştırır.
pub fn load(path: &std::path::Path) -> Result<AnswerFile, String> {
let content = std::fs::read_to_string(path)
.map_err(AnswerError::Io)?;
let af: AnswerFile = toml::from_str(&content)
.map_err(AnswerError::Parse)?;
validate(&af)?;
Ok(af)
.map_err(|e| format!("Answer file okunamadı '{}': {}", path.display(), e))?;
toml::from_str(&content)
.map_err(|e| format!("Answer file ayrıştırılamadı: {}", e))
}
fn validate(af: &AnswerFile) -> Result<(), AnswerError> {
let mut errors: Vec<String> = Vec::new();
/// `AnswerFile` içeriğini `GlobalState`'e uygular.
/// Disk boyutunu `lsblk` ile sorgular ve `partition_plan` oluşturur.
pub fn apply_to_state(af: &AnswerFile, state: &mut crate::installer::GlobalState) {
use crate::installer::{DiskInfo, PartitionPlan, PartitionTableType};
// Disk kontrolü
if !af.install.disk.starts_with("/dev/") {
errors.push(format!(
"Geçersiz disk yolu '{}'. '/dev/' ile başlamalı.",
af.install.disk
));
}
if !Path::new(&af.install.disk).exists() {
errors.push(format!(
"Disk '{}' bulunamadı. Mevcut diskler için 'lsblk' komutunu çalıştırın.",
af.install.disk
));
}
// Kullanıcı adı doğrulaması (UsersStep ile aynı kural)
let un = &af.user.username;
if un.is_empty() || un.len() > 32 {
errors.push("Kullanıcı adı 1–32 karakter arasında olmalı.".into());
} else {
let valid = un.chars().next().map(|c| c.is_ascii_lowercase() || c == '_').unwrap_or(false)
&& un.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-');
if !valid {
errors.push(format!(
"Geçersiz kullanıcı adı '{}'. Yalnızca küçük harf, rakam, - ve _ kullanılabilir.",
un
));
}
}
// Şifre uzunluğu
if af.user.password.len() < 6 {
errors.push("Şifre en az 6 karakter olmalı.".into());
}
// Hostname
let hn = &af.user.hostname;
if hn.is_empty() || hn.len() > 63 || hn.starts_with('-') || hn.ends_with('-')
|| !hn.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
{
errors.push(format!("Geçersiz hostname '{}'.", hn));
}
if errors.is_empty() { Ok(()) } else { Err(AnswerError::Validation(errors)) }
}
// ─────────────────────────────────────────────
// GLOBALSTATE'E UYGULA
// ─────────────────────────────────────────────
use crate::installer::GlobalState;
use crate::steps::partition::PartitionStep;
use crate::installer::InstallerStep;
/// Answer file verilerini `GlobalState`'e yazar.
/// Bu işlemden sonra `PartitionStep::on_enter()` çağrılarak
/// disk listesi yenilenebilir ve plan oluşturulabilir.
pub fn apply_to_state(af: &AnswerFile, state: &mut GlobalState) {
// Temel alanlar
state.timezone = af.install.timezone.clone();
state.keyboard_layout = af.install.keyboard.clone();
state.keyboard_variant = af.install.keyboard_variant.clone();
state.selected_disk = Some(af.install.disk.clone());
state.erase_disk = true;
state.username = af.user.username.clone();
state.password = af.user.password.clone();
state.password_confirm = af.user.password.clone();
state.hostname = af.user.hostname.clone();
state.selected_disk = Some(af.install.disk.clone());
state.erase_disk = true;
state.language = if af.install.locale.starts_with("tr") { "tr" } else { "en" }.to_string();
// Locale'den dil kodunu çıkar: "tr_TR.UTF-8" → "tr"
state.language = af.install.locale
.split('_')
.next()
.unwrap_or("tr")
.to_string();
// Disk boyutunu lsblk ile sorgula
let disk_info: Option<DiskInfo> = (|| {
let out = std::process::Command::new("lsblk")
.args(["-d", "-b", "-o", "NAME,SIZE,MODEL", "--noheadings", &af.install.disk])
.output()
.ok()?;
if !out.status.success() { return None; }
let text = String::from_utf8_lossy(&out.stdout);
let line = text.lines().next()?;
let mut parts = line.split_whitespace();
let name = parts.next()?.to_string();
let bytes: u64 = parts.next()?.parse().ok()?;
let model = parts.collect::<Vec<_>>().join(" ");
Some(DiskInfo {
name: format!("/dev/{}", name),
model: if model.is_empty() { "Unknown".to_string() } else { model },
size_gb: bytes / 1_073_741_824,
size_bytes: bytes,
})
})();
// Bölümleme planını hesapla
let is_uefi = Path::new("/sys/firmware/efi").exists();
let mut ps = PartitionStep::default();
ps.on_enter(state);
// Partition planını hesapla
if let Some(info) = disk_info {
let is_uefi = std::path::Path::new("/sys/firmware/efi").exists();
let efi_mb = if is_uefi { 512u64 } else { 0 };
let total_mb = info.size_bytes / 1024 / 1024;
// Seçili diski bul ve plan oluştur
if let Some(disk) = state.available_disks.iter().find(|d| d.name == af.install.disk).cloned() {
use crate::installer::{PartitionPlan, PartitionTableType};
let total_mb = disk.size_bytes / 1024 / 1024;
let efi_mb = if is_uefi { 512 } else { 0 };
let swap_mb = recommended_swap_mb();
let root_mb = total_mb.saturating_sub(efi_mb + swap_mb + 512);
state.partition_plan = Some(PartitionPlan {
disk: disk.name.clone(),
table_type: if is_uefi { PartitionTableType::Gpt } else { PartitionTableType::Mbr },
efi_mb,
swap_mb,
root_mb,
});
// Tavsiye edilen swap: RAM * 1.5, min 512 MB, max 8192 MB
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let ram_mb = sys.total_memory() / 1024 / 1024;
let swap_mb = (ram_mb * 3 / 2).clamp(512, 8192);
let overhead = efi_mb + swap_mb + 512;
let root_mb = total_mb.saturating_sub(overhead);
if root_mb >= 6144 && info.size_gb >= 10 {
state.partition_plan = Some(PartitionPlan {
disk: info.name.clone(),
table_type: if is_uefi { PartitionTableType::Gpt } else { PartitionTableType::Mbr },
efi_mb,
swap_mb,
root_mb,
encrypt_root: false,
luks_password: String::new(),
});
}
state.available_disks.push(info);
}
}
fn recommended_swap_mb() -> u64 {
use sysinfo::System;
let mut sys = System::new();
sys.refresh_memory();
let ram_mb = sys.total_memory() / 1024 / 1024;
let swap = if ram_mb <= 2048 { ram_mb } else if ram_mb <= 8192 { ram_mb / 2 } else { 4096 };
swap.clamp(512, 8192)
}
+255
View File
@@ -0,0 +1,255 @@
//! Yali-RS görsel özelleştirme (branding) yapılandırması.
//!
//! `branding.toml` dosyasını okur ve tüm sabit kodlanmış görsel yollarını
//! çalışma zamanında çözümler. Geliştiriciler yali-rs koduna dokunmadan
//! yalnızca `branding.toml` ile logo, slayt görseli ve renk değiştirebilir.
#![allow(dead_code)]
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Clone, Debug, Default, Deserialize)]
pub struct BrandingConfig {
#[serde(default)]
pub general: GeneralConfig,
#[serde(default)]
pub slides: SlidesConfig,
#[serde(default)]
pub theme: ThemeConfig,
#[serde(default)]
pub oem: OemConfig,
}
// ── [general] ────────────────────────────────────────────────────────────────
#[derive(Clone, Debug, Deserialize)]
pub struct GeneralConfig {
/// Kurulum penceresinin başlığı.
#[serde(default = "default_title")]
pub title: String,
/// Sol kenar çubuğunda gösterilen dağıtım ismi.
#[serde(default = "default_subtitle")]
pub subtitle: String,
/// Sol kenar çubuğu için Yali logosu dosya yolu.
#[serde(default = "default_yali_logo")]
pub yali_logo: String,
/// Kurulum ana sayfasında gösterilen PisiLinux logosu (koyu tema).
#[serde(default = "default_pisi_logo_dark")]
pub pisi_logo_dark: String,
/// Kurulum ana sayfasında gösterilen PisiLinux logosu (açık tema).
#[serde(default = "default_pisi_logo_light")]
pub pisi_logo_light: String,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
title: default_title(),
subtitle: default_subtitle(),
yali_logo: default_yali_logo(),
pisi_logo_dark: default_pisi_logo_dark(),
pisi_logo_light: default_pisi_logo_light(),
}
}
}
// ── [slides] ─────────────────────────────────────────────────────────────────
/// Tek bir slaytın içeriği.
#[derive(Clone, Debug, Default, Deserialize)]
pub struct SlideItem {
/// [slides.icons] tablosundaki ikon anahtar adı.
pub icon: String,
/// Slayt başlığı.
pub title: String,
/// Slayt açıklama metni.
pub description: String,
/// Yalnızca bu masaüstü ortamında göster (boş = hepsinde).
/// Örnek: "xfce", "kde", "gnome"
#[serde(default)]
pub desktop: String,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct SlidesConfig {
/// Slayt geçiş süresi (saniye).
#[serde(default = "default_interval")]
pub interval_secs: u64,
/// Slayt ikonu anahtar sözcüğü → dosya yolu eşlemesi.
#[serde(default)]
pub icons: HashMap<String, String>,
/// branding.toml içinde [[slides.items]] ile tanımlanan slayt listesi.
/// Boşsa slideshow.rs içindeki sabit slaytlar kullanılır.
#[serde(default)]
pub items: Vec<SlideItem>,
}
// ── [theme] ──────────────────────────────────────────────────────────────────
#[derive(Clone, Debug, Deserialize)]
pub struct ThemeConfig {
#[serde(default = "default_primary")]
pub primary: String,
#[serde(default = "default_accent")]
pub accent: String,
#[serde(default = "default_background")]
pub background: String,
#[serde(default = "default_text")]
pub text: String,
}
impl Default for ThemeConfig {
fn default() -> Self {
Self {
primary: default_primary(),
accent: default_accent(),
background: default_background(),
text: default_text(),
}
}
}
// ── [oem] ────────────────────────────────────────────────────────────────────
#[derive(Clone, Debug, Default, Deserialize)]
pub struct OemConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
#[allow(dead_code)]
pub skip_steps: Vec<usize>,
#[serde(default)]
pub default_username: String,
#[serde(default = "default_hostname")]
pub default_hostname: String,
#[serde(default)]
pub default_password: String,
}
// ── Varsayılan değerler ───────────────────────────────────────────────────────
fn default_title() -> String { "YALI Installation Tool".to_string() }
fn default_subtitle() -> String { "PiSi GNU/LINUX".to_string() }
fn default_yali_logo() -> String { "assets/yali.png".to_string() }
fn default_pisi_logo_dark() -> String { "assets/pisi-logo-dark.png".to_string() }
fn default_pisi_logo_light() -> String { "assets/pisi-logo-light.png".to_string() }
fn default_interval() -> u64 { 6 }
fn default_primary() -> String { "#1a73e8".to_string() }
fn default_accent() -> String { "#34a853".to_string() }
fn default_background() -> String { "#181824".to_string() }
fn default_text() -> String { "#e2e8f0".to_string() }
fn default_hostname() -> String { "pisi-oem".to_string() }
// ── Yükleme ──────────────────────────────────────────────────────────────────
impl BrandingConfig {
/// `branding.toml` dosyasını arar ve yükler.
///
/// Arama sırası:
/// 1. `/etc/yali/branding.toml`
/// 2. `/usr/share/yali/branding.toml`
/// 3. Çalışma dizinindeki `branding.toml`
pub fn load() -> Self {
let paths = [
"/etc/yali/branding.toml",
"/usr/share/yali/branding.toml",
"branding.toml",
];
for path in &paths {
if let Ok(content) = std::fs::read_to_string(path) {
match toml::from_str(&content) {
Ok(cfg) => return cfg,
Err(e) => eprintln!("[branding] {}: {}", path, e),
}
}
}
BrandingConfig::default()
}
/// Belirtilen dile ait `branding_{lang}.toml` dosyasını arar ve yükler.
/// Bulunamazsa varsayılan `branding.toml` dosyasına geri döner.
pub fn load_for_lang(lang: &str) -> Self {
let lang_paths = [
format!("/etc/yali/branding_{}.toml", lang),
format!("/usr/share/yali/branding_{}.toml", lang),
format!("branding_{}.toml", lang),
];
for path in &lang_paths {
if let Ok(content) = std::fs::read_to_string(path) {
match toml::from_str(&content) {
Ok(cfg) => return cfg,
Err(e) => eprintln!("[branding] {}: {}", path, e),
}
}
}
Self::load()
}
/// İkon adına göre dosya yolunu döner.
///
/// Koyu/açık temaya duyarlı `pisilogo` ikonu için `is_dark` parametresi
/// kullanılır; diğer ikonlar için görmezden gelinir.
pub fn icon_path(&self, icon: &str, is_dark: bool) -> Option<String> {
// pisilogo özel durumu: tema bazlı iki ayrı anahtar
if icon == "pisilogo" {
let key = if is_dark { "pisi_logo_dark" } else { "pisi_logo_light" };
return self.slides.icons.get(key).cloned();
}
self.slides.icons.get(icon).cloned()
}
/// Sol kenar çubuğu (Yali) logosu için `file://` URI döner.
pub fn yali_logo_uri(&self) -> String {
path_to_file_uri(&self.general.yali_logo)
}
/// Ana sayfa PisiLinux logosu (koyu tema) için `file://` URI döner.
pub fn pisi_logo_dark_uri(&self) -> String {
path_to_file_uri(&self.general.pisi_logo_dark)
}
/// Ana sayfa PisiLinux logosu (açık tema) için `file://` URI döner.
pub fn pisi_logo_light_uri(&self) -> String {
path_to_file_uri(&self.general.pisi_logo_light)
}
}
/// Göreli veya mutlak dosya yolunu `egui::Image::from_uri` için uygun
/// `file://` URI formatına çevirir.
pub fn path_to_file_uri(path: &str) -> String {
if path.starts_with("file://") || path.starts_with("http") {
return path.to_string();
}
if std::path::Path::new(path).is_absolute() {
return format!("file://{}", path);
}
// Göreli yolları sırasıyla ara:
// 1. Çalışma dizini (CWD)
// 2. /usr/share/yali/
// 3. /etc/yali/
let cwd = std::env::current_dir().unwrap_or_default();
let cwd_path = cwd.join(path);
if cwd_path.exists() {
return format!("file://{}", cwd_path.display());
}
let usr_path = std::path::Path::new("/usr/share/yali").join(path);
if usr_path.exists() {
return format!("file://{}", usr_path.display());
}
let etc_path = std::path::Path::new("/etc/yali").join(path);
if etc_path.exists() {
return format!("file://{}", etc_path.display());
}
// Hiçbiri bulunamazsa varsayılan olarak CWD yolunu döner (hata/fallback takibi için)
format!("file://{}", cwd_path.display())
}
+391 -27
View File
@@ -1,4 +1,5 @@
use eframe::egui;
use sysinfo::System;
use rust_i18n::t;
/// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur.
@@ -45,6 +46,8 @@ pub struct PartitionPlan {
pub efi_mb: u64, // 0 → BIOS, >0 → UEFI
pub swap_mb: u64,
pub root_mb: u64, // kalan alan
pub encrypt_root: bool, // Kök bölümü LUKS ile şifrelensin mi?
pub luks_password: String, // LUKS şifresi
}
#[derive(Clone, Debug, Default, PartialEq)]
@@ -67,23 +70,31 @@ impl std::fmt::Display for PartitionTableType {
#[derive(Clone, Debug, PartialEq)]
pub enum FsType {
Ext4,
Ext3,
Btrfs,
Xfs,
Fat32, // EFI bölümü için
Ntfs,
Swap,
Lvm, // LVM fiziksel bölüm (PV)
#[allow(dead_code)]
LvmLv, // LVM mantıksal bölüm (LV)
}
impl FsType {
pub fn label(&self) -> &'static str {
match self {
FsType::Ext4 => "ext4",
FsType::Ext3 => "ext3",
FsType::Btrfs => "btrfs",
FsType::Xfs => "xfs",
FsType::Fat32 => "FAT32 (EFI)",
FsType::Ntfs => "NTFS",
FsType::Swap => "swap",
FsType::Lvm => "LVM PV",
FsType::LvmLv => "LVM LV",
}
}
}
impl std::fmt::Display for FsType {
@@ -92,6 +103,25 @@ impl std::fmt::Display for FsType {
}
}
/// LVM Volume Group yapılandırması.
#[derive(Clone, Debug, Default)]
pub struct VolumeGroup {
pub name: String,
pub pv_devices: Vec<String>, // PV olarak kullanılacak bölüm aygıtları
pub size_mb: u64, // VG toplam boyutu (hesaplanan)
}
/// LVM Logical Volume yapılandırması.
#[derive(Clone, Debug)]
pub struct LogicalVolume {
pub name: String,
pub vg_name: String,
pub size_mb: u64, // 0 = kalan alan
pub fstype: FsType,
#[allow(dead_code)]
pub mountpoint: String,
}
/// Kullanıcının manuel olarak tanımladığı tek bir bölüm.
#[derive(Clone, Debug)]
pub struct CustomPartition {
@@ -103,6 +133,12 @@ pub struct CustomPartition {
pub fstype: FsType,
/// Bağlama noktası; swap için "swap"
pub mountpoint: String,
/// LUKS şifreleme aktif mi?
pub encrypt: bool,
/// LUKS şifresi
pub luks_password: String,
/// LUKS harici adı (ör. "cryptroot")
pub luks_name: String,
}
/// Kurulum boyunca toplanan tüm veriler.
@@ -114,6 +150,8 @@ pub struct GlobalState {
pub partition_plan: Option<PartitionPlan>,
/// Manuel bölümleme modu için kullanıcının tanımladığı bölümler.
pub custom_partitions: Vec<CustomPartition>,
pub volume_groups: Vec<VolumeGroup>,
pub logical_volumes: Vec<LogicalVolume>,
pub keyboard_layout: String,
pub keyboard_variant: String,
pub username: String,
@@ -126,6 +164,44 @@ pub struct GlobalState {
pub install_log: Vec<String>,
pub demo_mode: bool,
pub is_dark: bool,
pub rescue_mode: bool,
pub bootloader_device: String,
pub bootloader_timeout: u32,
pub bootloader_password: String,
pub kernel_options: String,
pub license_accepted: bool,
pub root_password: String,
pub root_password_confirm: String,
pub display_manager: String,
pub autologin: bool,
pub desktop_environment: String,
pub network_wifi_password: String,
pub network_http_proxy: String,
pub network_https_proxy: String,
pub network_ftp_proxy: String,
pub use_ntp: bool,
pub manual_year: i32,
pub manual_month: u32,
pub manual_day: u32,
pub manual_hour: u32,
pub manual_minute: u32,
#[allow(dead_code)]
pub encrypt_root: bool,
#[allow(dead_code)]
pub luks_password: String,
pub oem_mode: bool,
pub selected_package_groups: Vec<String>,
/// branding.toml [slides.icons] tablosundan yüklenen ikon → dosya URI eşlemesi.
/// Slideshow bu map üzerinden çalışma zamanında görsel yollarını çözümler.
pub branding_icons: std::collections::HashMap<String, String>,
/// Sidebar için koyu tema logo URI'si.
pub branding_yali_dark: String,
pub branding_pisi_dark: String,
/// Sidebar için açık tema logo URI'si.
pub branding_yali_light: String,
pub branding_pisi_light: String,
/// branding.toml dosyasından yüklenen slaytlar.
pub branding_slides: Vec<crate::branding::SlideItem>,
}
impl Default for GlobalState {
@@ -137,6 +213,8 @@ impl Default for GlobalState {
erase_disk: true,
partition_plan: None,
custom_partitions: Vec::new(),
volume_groups: Vec::new(),
logical_volumes: Vec::new(),
keyboard_layout: "tr".to_string(),
keyboard_variant: String::new(),
username: String::new(),
@@ -149,11 +227,51 @@ impl Default for GlobalState {
install_log: Vec::new(),
demo_mode: false,
is_dark: true,
rescue_mode: false,
bootloader_device: String::new(),
bootloader_timeout: 5,
bootloader_password: String::new(),
kernel_options: String::new(),
license_accepted: false,
root_password: String::new(),
root_password_confirm: String::new(),
network_wifi_password: String::new(),
network_http_proxy: String::new(),
network_https_proxy: String::new(),
network_ftp_proxy: String::new(),
use_ntp: true,
manual_year: 2026,
manual_month: 5,
manual_day: 26,
manual_hour: 12,
manual_minute: 0,
encrypt_root: false,
luks_password: String::new(),
oem_mode: false,
selected_package_groups: Vec::new(),
display_manager: "sddm".to_string(),
autologin: true,
desktop_environment: {
let de = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.trim()
.to_lowercase();
if de.is_empty() {
"plasma".to_string()
} else {
de
}
},
branding_icons: std::collections::HashMap::new(),
branding_pisi_dark: String::new(),
branding_pisi_light: String::new(),
branding_yali_dark: String::new(),
branding_yali_light: String::new(),
branding_slides: Vec::new(),
}
}
}
// ─────────────────────────────────────────────
// WELCOME STEP (sistem gereksinimleri + dil seçimi)
// ─────────────────────────────────────────────
@@ -167,7 +285,11 @@ pub struct WelcomeStep {
impl Default for WelcomeStep {
fn default() -> Self {
Self { checks: Vec::new(), checked: false, last_language: String::new() }
Self {
checks: Vec::new(),
checked: false,
last_language: String::new(),
}
}
}
@@ -193,12 +315,54 @@ impl InstallerStep for WelcomeStep {
}
theme::section_heading(ui, &t!("welcome_title"));
ui.label(
ui.label(
egui::RichText::new(t!("welcome_description"))
.color(theme::c_text_dim())
.size(13.0),
.size(15.0),
);
ui.add_space(16.0);
ui.vertical_centered(|ui| {
let logo_uri = if state.is_dark {
&state.branding_pisi_dark
} else {
&state.branding_pisi_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);
let os_name = System::name().unwrap_or_else(|| "Bilinmiyor".to_string());
ui.label(
egui::RichText::new(format!("{} : {}", t!("os"), os_name))
.strong()
.size(16.0)
.color(theme::c_text_dim()),
);
ui.add_space(4.0);
let os_version = System::os_version().unwrap_or_else(|| "Bilinmiyor".to_string());
ui.label(
egui::RichText::new(format!("{} : {}", t!("version"), os_version))
.size(14.0)
.color(theme::c_text_dim()),
);
let desktop_name = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
ui.label(
egui::RichText::new(format!("{} : {}", t!("desktop"), desktop_name))
.size(14.0)
.color(theme::c_text_dim()),
);
});
ui.add_space(12.0);
// Dil seçimi (Açılır Liste / ComboBox)
@@ -232,7 +396,6 @@ impl InstallerStep for WelcomeStep {
.striped(true)
.show(ui, |ui| {
for check in &self.checks {
// Durum simgesi + renk
let (icon, color) = match check.status {
CheckStatus::Pass => ("✅", theme::c_success()),
CheckStatus::Warn => ("⚠️", theme::c_warning()),
@@ -249,6 +412,27 @@ impl InstallerStep for WelcomeStep {
}
});
// Rescue mode butonu
ui.add_space(10.0);
ui.horizontal(|ui| {
let rescue_available = detect_existing_linux();
if rescue_available {
ui.colored_label(
egui::Color32::from_rgb(80, 170, 80),
"🛟",
);
if ui.add(theme::secondary_button(&t!("rescue_button"))).clicked() {
state.rescue_mode = true;
}
} else {
ui.label(
egui::RichText::new(t!("rescue_not_available"))
.size(12.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
);
}
});
// Hata varsa uyarı
ui.add_space(10.0);
let has_fail = self.checks.iter().any(|c| c.status == CheckStatus::Fail);
@@ -269,11 +453,9 @@ impl InstallerStep for WelcomeStep {
}
fn is_complete(&self, state: &GlobalState) -> bool {
// Demo modunda her zaman ileri gitmeye izin ver
if state.demo_mode {
return true;
}
// Fail varsa ilerleyemez; kontroller bitmemişse de bekle
!self.checks.is_empty() && all_passed(&self.checks)
}
}
@@ -283,8 +465,9 @@ impl InstallerStep for WelcomeStep {
// ─────────────────────────────────────────────
pub struct LocationStep {
search_query: String,
timezones: Vec<String>,
geoip_done: bool,
datetime_init_done: bool,
}
impl Default for LocationStep {
@@ -296,29 +479,210 @@ impl Default for LocationStep {
"Europe/London", "Europe/Moscow", "Europe/Paris", "Europe/Rome",
"Pacific/Auckland",
].iter().map(|s| s.to_string()).collect();
Self { search_query: String::new(), timezones: tz_list }
Self {
timezones: tz_list,
geoip_done: false,
datetime_init_done: false,
}
}
}
impl LocationStep {
fn geoip_lookup(state: &mut GlobalState) {
let out = std::process::Command::new("curl")
.args(["-s", "--connect-timeout", "3", "http://ip-api.com/json"])
.output();
if let Ok(o) = out {
if let Ok(text) = String::from_utf8(o.stdout) {
let extract = |key: &str| -> Option<String> {
let pat = format!("\"{}\":\"", key);
text.find(&pat).and_then(|pos| {
let start = pos + pat.len();
text[start..].find('"').map(|end| text[start..start+end].to_string())
})
};
if let Some(country) = extract("countryCode") {
match country.as_str() {
"TR" => state.language = "tr".to_string(),
"DE" => state.language = "de".to_string(),
"FR" => state.language = "fr".to_string(),
"IT" => state.language = "it".to_string(),
"ES" => state.language = "es".to_string(),
"RU" => state.language = "ru".to_string(),
"NL" => state.language = "nl".to_string(),
"PL" => state.language = "pl".to_string(),
_ => {}
}
}
if let Some(tz) = extract("timezone") {
if !tz.is_empty() {
state.timezone = tz;
}
}
}
}
}
fn init_from_system(&mut self, state: &mut GlobalState) {
use chrono::{Datelike, Timelike};
let now_str = chrono::Local::now().format("%Y-%m-%d %H:%M").to_string();
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(&now_str, "%Y-%m-%d %H:%M") {
state.manual_year = dt.year();
state.manual_month = dt.month();
state.manual_day = dt.day();
state.manual_hour = dt.hour();
state.manual_minute = dt.minute();
}
self.datetime_init_done = true;
}
}
impl InstallerStep for LocationStep {
fn name(&self) -> String { t!("location").to_string() }
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("location_title"));
ui.label(t!("location_description"));
ui.add_space(10.0);
ui.label(t!("search_timezone"));
ui.text_edit_singleline(&mut self.search_query);
ui.add_space(5.0);
egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
for tz in &self.timezones {
if tz.to_lowercase().contains(&self.search_query.to_lowercase()) {
ui.selectable_value(&mut state.timezone, tz.clone(), tz);
}
}
});
fn name(&self) -> String {
format!("{} / {}", t!("location"), t!("datetime"))
}
fn is_complete(&self, state: &GlobalState) -> bool { !state.timezone.is_empty() }
fn on_enter(&mut self, state: &mut GlobalState) {
if !self.geoip_done {
Self::geoip_lookup(state);
self.geoip_done = true;
}
if !self.datetime_init_done {
self.init_from_system(state);
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
use crate::ui::theme;
// --- BÖLGE VE ZAMAN DİLİMİ (Region & Timezone) ---
theme::section_heading(ui, &t!("location_title"));
ui.label(
egui::RichText::new(t!("location_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(8.0);
ui.horizontal(|ui| {
ui.label(egui::RichText::new(t!("location_title")).strong().color(theme::c_text()));
let current_tz = if state.timezone.is_empty() {
"Europe/Istanbul".to_string()
} else {
state.timezone.clone()
};
if state.timezone.is_empty() {
state.timezone = current_tz.clone();
}
egui::ComboBox::from_id_source("timezone_select")
.selected_text(&current_tz)
.width(280.0)
.show_ui(ui, |ui| {
for tz in &self.timezones {
ui.selectable_value(&mut state.timezone, tz.clone(), tz);
}
});
});
ui.add_space(16.0);
ui.separator();
ui.add_space(16.0);
// --- TARİH VE SAAT AYARLARI (Date & Time) ---
theme::section_heading(ui, &t!("datetime_title"));
ui.label(
egui::RichText::new(t!("datetime_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(8.0);
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| {
ui.set_width(ui.available_width());
egui::Grid::new("datetime_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// NTP
ui.label(egui::RichText::new(t!("datetime_ntp_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(egui::Checkbox::new(&mut state.use_ntp, ""));
let status = if state.use_ntp { t!("datetime_ntp_on") } else { t!("datetime_ntp_off") };
ui.colored_label(
if state.use_ntp { theme::c_success() } else { theme::c_error() },
status,
);
});
ui.end_row();
if !state.use_ntp {
// Tarih
ui.label(egui::RichText::new(t!("datetime_date_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.label(t!("datetime_year"));
ui.add(egui::DragValue::new(&mut state.manual_year).clamp_range(2000..=2100).speed(1));
ui.label(t!("datetime_month"));
ui.add(egui::DragValue::new(&mut state.manual_month).clamp_range(1..=12).speed(1));
ui.label(t!("datetime_day"));
ui.add(egui::DragValue::new(&mut state.manual_day).clamp_range(1..=31).speed(1));
});
ui.end_row();
// Saat
ui.label(egui::RichText::new(t!("datetime_time_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.add(egui::DragValue::new(&mut state.manual_hour).clamp_range(0..=23).speed(1));
ui.label(":");
ui.add(egui::DragValue::new(&mut state.manual_minute).clamp_range(0..=59).speed(1));
});
ui.end_row();
}
});
});
ui.add_space(8.0);
if state.use_ntp {
ui.label(
egui::RichText::new(t!("datetime_ntp_info"))
.size(11.0)
.color(theme::c_text_dim()),
);
} else {
ui.label(
egui::RichText::new(t!("datetime_manual_info"))
.size(11.0)
.color(theme::c_text_dim()),
);
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
!state.timezone.is_empty()
}
}
/// Mevcut bir Linux kurulumu olup olmadığını kontrol eder.
/// Basitçe /mnt/etc/fstab varlığına veya blkid ile Linux bölümlerine bakar.
fn detect_existing_linux() -> bool {
// /etc/fstab veya /mnt/etc/fstab kontrolü
if std::path::Path::new("/mnt/etc/fstab").exists() {
return true;
}
// blkid ile Linux bölümlerini tara
if let Ok(out) = std::process::Command::new("blkid")
.args(["-o", "list"])
.output()
{
let text = String::from_utf8_lossy(&out.stdout);
text.lines().any(|l| l.contains("ext4") || l.contains("btrfs") || l.contains("xfs"))
} else {
false
}
}
+971 -205
View File
File diff suppressed because it is too large Load Diff
+140 -59
View File
@@ -3,9 +3,10 @@ mod steps;
mod jobs;
mod ui;
mod autoinstall;
mod branding;
use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep};
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep};
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, LicenseStep, NetworkStep, DisplayManagerStep};
use eframe::egui;
use rust_i18n::t;
@@ -32,14 +33,14 @@ fn main() -> eframe::Result<()> {
fn run_gui(demo_mode: bool) -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1100.0, 760.0])
.with_min_inner_size([900.0, 650.0])
.with_title(t!("installer").to_string()),
.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").to_string(),
t!("installer").as_ref(),
options,
Box::new(move |cc| {
// Görsel yükleyicileri etkinleştir (PNG desteği)
@@ -88,17 +89,19 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
}
if !autoinstall::checker::all_passed(&checks) {
eprintln!("{}: {}", t!("error"), "Critical check error. Installation stopped.");
eprintln!("{}: Critical check error. Installation stopped.", t!("error"));
std::process::exit(2);
}
// State'i hazırla
let mut state = GlobalState::default();
state.demo_mode = demo_mode;
let mut state = GlobalState {
demo_mode,
..Default::default()
};
apply_to_state(&af, &mut state);
if state.partition_plan.is_none() {
eprintln!("{}: {}", t!("error"), format!("{}: {}", t!("partition_plan_could_not_be_created"), af.install.disk));
eprintln!("{}: {}: {}", t!("error"), t!("partition_plan_could_not_be_created"), af.install.disk);
std::process::exit(3);
}
@@ -122,13 +125,9 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
use jobs::{InstallMessage, UiSender};
// CLI modunda egui Context yok; basit bir mpsc kanal kullanıyoruz.
// UiSender'a dummy bir context gerekeceğinden terminal çıktısı için
// doğrudan kanalı dinliyoruz.
let (tx, rx) = mpsc::channel::<InstallMessage>();
// Arka plan iş parçacığı — gerçek job kuyruğunu çalıştırır.
// egui::Context::default() CLI modunda request_repaint()'i no-op yapar;
// mesajlar mpsc kanalı üzerinden terminale aktarılır.
let cli_ctx = eframe::egui::Context::default();
let ui_sender = UiSender::new(tx, cli_ctx);
@@ -147,16 +146,33 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
let mut queue = jobs::build_full_job_queue(
plan_c,
&mount_c,
&source_c,
&locale_c,
&timezone_c,
&username_c,
&password_c,
&hostname_c,
&kb_layout_c,
&kb_variant_c,
demo_mode,
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));
});
@@ -182,32 +198,69 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
}
});
// eframe::Result döndürmek için Ok
Ok(())
}
struct YaliApp {
state: GlobalState,
steps: Vec<Box<dyn InstallerStep>>,
last_step: usize,
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(LicenseStep),
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 ö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
@@ -218,6 +271,45 @@ 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;
@@ -238,31 +330,31 @@ impl eframe::App for YaliApp {
.show(ctx, |ui| {
ui.add_space(20.0);
// PisiLinux logo
// Dağıtım logosu — branding.toml'dan dinamik yüklenir
ui.vertical_centered(|ui| {
let logo = if self.state.is_dark {
egui::include_image!("../assets/pisi-logo-dark.png")
let logo_uri = if self.state.is_dark {
&self.state.branding_yali_dark
} else {
egui::include_image!("../assets/pisi-logo-light.png")
&self.state.branding_yali_light
};
ui.add(
egui::Image::new(logo)
.max_width(120.0)
.rounding(egui::Rounding::same(8.0)),
);
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!("os_name"))
egui::RichText::new(t!("app_name"))
.strong()
.size(16.0)
.color(ui::theme::c_text_dim()),
);
// get desktop environment
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
ui.label(
egui::RichText::new(&format!("{} Desktop", desktop))
.size(14.0)
.color(ui::theme::c_text_dim()),
ui.colored_label(
egui::Color32::from_rgb(60, 60, 90),
format!("{}: {}", t!("version"), env!("CARGO_PKG_VERSION")),
);
});
@@ -305,15 +397,10 @@ impl eframe::App for YaliApp {
ui.add_space(2.0);
}
// Alt kısım: tema değiştirme + sürüm bilgisi
// Alt kısım: tema değiştirme
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.add_space(12.0);
ui.colored_label(
egui::Color32::from_rgb(60, 60, 90),
format!("YALI (v{})", env!("CARGO_PKG_VERSION")),
);
ui.add_space(6.0);
// Tema değiştirme butonu
let theme_icon = if self.state.is_dark { "☀" } else { "🌙" };
let theme_label = if self.state.is_dark { "Light" } else { "Dark" };
if ui.add(
@@ -341,11 +428,8 @@ impl eframe::App for YaliApp {
ui.horizontal(|ui| {
ui.add_space(8.0);
// İptal — kurulum ve bitiş ekranında gizle
if !is_exec && !is_finish {
if ui.add(ui::theme::secondary_button(&t!("cancel"))).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
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| {
@@ -357,7 +441,6 @@ impl eframe::App for YaliApp {
.unwrap_or(false);
if !is_finish {
// İleri / Tamamlandı butonu
if !is_exec || is_valid {
let next_label = if is_exec {
t!("finish_next")
@@ -367,7 +450,6 @@ impl eframe::App for YaliApp {
let btn = if is_valid {
ui::theme::primary_button(&next_label)
} else {
// Devre dışı görünüm için secondary kullan
ui::theme::secondary_button(&next_label)
};
if ui.add_enabled(is_valid, btn).clicked() {
@@ -375,7 +457,6 @@ impl eframe::App for YaliApp {
}
}
// Geri — kurulum ekranında gizle
if cur > 0 && !is_exec {
ui.add_space(8.0);
if ui.add(ui::theme::secondary_button(&t!("back"))).clicked() {
@@ -403,4 +484,4 @@ impl eframe::App for YaliApp {
});
});
}
}
}
+111
View File
@@ -0,0 +1,111 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
#[derive(Default)]
pub struct BootloaderStep {
show_password: bool,
timeout_str: String,
}
impl InstallerStep for BootloaderStep {
fn name(&self) -> String {
t!("bootloader").to_string()
}
fn on_enter(&mut self, state: &mut GlobalState) {
if self.timeout_str.is_empty() {
self.timeout_str = state.bootloader_timeout.to_string();
}
if state.bootloader_device.is_empty() {
if let Some(ref disk) = state.selected_disk {
state.bootloader_device = disk.clone();
}
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("bootloader_title"));
ui.label(t!("bootloader_description"));
ui.add_space(12.0);
egui::Grid::new("bootloader_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Hedef aygıt
ui.label(t!("bootloader_device_label"));
ui.horizontal(|ui| {
let mut device_list: Vec<String> = 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()
};
egui::ComboBox::from_id_source("boot_device_select")
.selected_text(&current)
.show_ui(ui, |ui| {
for dev in &device_list {
ui.selectable_value(
&mut state.bootloader_device,
dev.clone(),
dev,
);
}
});
ui.label(
egui::RichText::new(t!("bootloader_device_hint"))
.size(11.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
);
});
ui.end_row();
// 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();
// 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();
// Kernel parametreleri
ui.label(t!("bootloader_kernel_label"));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.kernel_options);
ui.label(
egui::RichText::new(t!("bootloader_kernel_hint"))
.size(11.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
);
});
ui.end_row();
});
}
fn is_complete(&self, state: &GlobalState) -> bool {
!state.bootloader_device.is_empty()
}
}
+86
View File
@@ -0,0 +1,86 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
const DM_OPTIONS: &[&str] = &["sddm", "lightdm", "gdm"];
#[derive(Default)]
pub struct DisplayManagerStep;
impl InstallerStep for DisplayManagerStep {
fn name(&self) -> String {
t!("display_manager").to_string()
}
fn on_enter(&mut self, state: &mut GlobalState) {
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.trim()
.to_lowercase();
if !desktop.is_empty() {
state.desktop_environment = desktop;
} else if state.desktop_environment.is_empty() {
state.desktop_environment = "plasma".to_string();
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("display_manager_title"));
ui.label(t!("display_manager_description"));
ui.add_space(12.0);
egui::Grid::new("dm_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Ekran yöneticisi
ui.label(t!("display_manager_select"));
egui::ComboBox::from_id_source("dm_combo")
.selected_text(state.display_manager.as_str())
.show_ui(ui, |ui| {
for dm in DM_OPTIONS {
ui.selectable_value(
&mut state.display_manager,
dm.to_string(),
*dm,
);
}
});
ui.end_row();
// Masaüstü ortamı (otomatik algılanır)
ui.label(t!("display_manager_de"));
if !state.desktop_environment.is_empty() && state.desktop_environment != "default" {
ui.colored_label(egui::Color32::from_rgb(120, 180, 120), &state.desktop_environment);
} else {
ui.colored_label(egui::Color32::from_rgb(200, 160, 40), t!("display_manager_de_not_found"));
}
ui.end_row();
// Otomatik oturum açma
ui.label(t!("display_manager_autologin"));
ui.checkbox(&mut state.autologin, "");
ui.end_row();
if state.autologin {
ui.label(t!("display_manager_autologin_user"));
ui.horizontal(|ui| {
let name = if !state.username.is_empty() {
state.username.clone()
} else {
t!("display_manager_autologin_hint").to_string()
};
ui.colored_label(egui::Color32::from_rgb(120, 180, 120), name);
});
ui.end_row();
}
});
ui.add_space(8.0);
ui.label(egui::RichText::new(t!("display_manager_note")).size(11.0).color(egui::Color32::from_rgb(140, 140, 160)));
}
fn is_complete(&self, _state: &GlobalState) -> bool {
true
}
}
+80 -26
View File
@@ -15,6 +15,7 @@ use crate::jobs::{self, InstallMessage, UiSender};
use crate::ui::{ErrorScreen, Slideshow};
use crate::ui::theme;
#[derive(Default)]
pub struct ExecutionStep {
rx: Option<mpsc::Receiver<InstallMessage>>,
tx: Option<mpsc::Sender<InstallMessage>>,
@@ -23,18 +24,6 @@ pub struct ExecutionStep {
slideshow: Option<Slideshow>,
}
impl Default for ExecutionStep {
fn default() -> Self {
Self {
rx: None,
tx: None,
finished: false,
error_screen: None,
slideshow: None,
}
}
}
impl InstallerStep for ExecutionStep {
fn name(&self) -> String { t!("execution").to_string() }
@@ -48,7 +37,7 @@ impl InstallerStep for ExecutionStep {
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_else(|_| "default".to_string())
.to_lowercase();
self.slideshow = Some(Slideshow::new(&desktop));
self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone()));
state.install_progress = 0.0;
state.install_log.clear();
@@ -70,8 +59,26 @@ impl InstallerStep for ExecutionStep {
let ctx = ui.ctx().clone();
let erase_disk = state.erase_disk;
let custom_partitions = state.custom_partitions.clone();
let volume_groups = state.volume_groups.clone();
let logical_volumes = state.logical_volumes.clone();
let selected_disk = state.selected_disk.clone();
let boot_device = state.bootloader_device.clone();
let bootloader_timeout = state.bootloader_timeout;
let bootloader_password = state.bootloader_password.clone();
let kernel_options = state.kernel_options.clone();
let use_ntp = state.use_ntp;
let root_password = state.root_password.clone();
let display_manager = state.display_manager.clone();
let autologin = state.autologin;
let desktop_environment = state.desktop_environment.clone();
let selected_package_groups = state.selected_package_groups.clone();
let manual_year = state.manual_year;
let manual_month = state.manual_month;
let manual_day = state.manual_day;
let manual_hour = state.manual_hour;
let manual_minute = state.manual_minute;
let ui_sender = UiSender::new(tx, ctx);
thread::spawn(move || {
@@ -86,11 +93,34 @@ impl InstallerStep for ExecutionStep {
if erase_disk {
if let Some(p) = plan {
let mut queue = jobs::build_full_job_queue(
p, "/mnt", &src,
&locale, &timezone,
&username, &password, &hostname,
&kb_layout, &kb_variant,
demo_mode,
p,
jobs::JobQueueConfig {
mount: "/mnt".to_string(),
source: src,
locale,
timezone,
username,
password,
hostname,
kb_layout,
kb_variant,
demo_mode,
boot_device,
bootloader_timeout,
bootloader_password,
kernel_options,
use_ntp,
root_password,
display_manager: display_manager.clone(),
autologin,
desktop_environment: desktop_environment.clone(),
selected_package_groups: selected_package_groups.clone(),
manual_year,
manual_month,
manual_day,
manual_hour,
manual_minute,
},
);
rt.block_on(queue.run_all(ui_sender));
} else {
@@ -98,14 +128,39 @@ impl InstallerStep for ExecutionStep {
t!("error_partition_plan_not_found").to_string()
));
}
} else {
if let Some(disk) = selected_disk {
} else if let Some(disk) = selected_disk {
let mut queue = jobs::build_custom_job_queue(
disk, custom_partitions, "/mnt", &src,
&locale, &timezone,
&username, &password, &hostname,
&kb_layout, &kb_variant,
demo_mode,
disk,
custom_partitions,
volume_groups,
logical_volumes,
jobs::JobQueueConfig {
mount: "/mnt".to_string(),
source: src,
locale,
timezone,
username,
password,
hostname,
kb_layout,
kb_variant,
demo_mode,
boot_device,
bootloader_timeout,
bootloader_password,
kernel_options,
use_ntp,
root_password,
display_manager: display_manager.clone(),
autologin,
desktop_environment: desktop_environment.clone(),
selected_package_groups: selected_package_groups.clone(),
manual_year,
manual_month,
manual_day,
manual_hour,
manual_minute,
},
);
rt.block_on(queue.run_all(ui_sender));
} else {
@@ -113,7 +168,6 @@ impl InstallerStep for ExecutionStep {
t!("error_target_disk_not_selected").to_string()
));
}
}
});
}
+52
View File
@@ -0,0 +1,52 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
#[derive(Default)]
pub struct LicenseStep;
const GPL_TEXT: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/LICENSE"));
impl InstallerStep for LicenseStep {
fn name(&self) -> String {
t!("license").to_string()
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("license_title"));
ui.label(t!("license_description"));
ui.add_space(8.0);
let avail = ui.available_height();
egui::ScrollArea::vertical()
.max_height(avail - 60.0)
.stick_to_bottom(true)
.auto_shrink([false; 2])
.show(ui, |ui| {
egui::Frame::group(ui.style())
.inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| {
ui.label(
egui::RichText::new(GPL_TEXT)
.size(14.0)
.family(egui::FontFamily::Monospace)
.color(egui::Color32::from_rgb(180, 180, 200)),
);
});
});
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.add(egui::Checkbox::new(&mut state.license_accepted, ""));
ui.label(
egui::RichText::new(t!("license_accept"))
.size(14.0)
.strong(),
);
});
}
fn is_complete(&self, state: &GlobalState) -> bool {
state.license_accepted
}
}
+11
View File
@@ -4,6 +4,12 @@ pub mod summary;
pub mod execution;
pub mod finish;
pub mod partition;
pub mod bootloader;
pub mod license;
pub mod network;
pub mod rescue;
pub mod display_manager;
pub mod netinstall;
pub use keyboard::KeyboardStep;
pub use users::UsersStep;
@@ -11,3 +17,8 @@ pub use summary::SummaryStep;
pub use execution::ExecutionStep;
pub use finish::FinishStep;
pub use partition::PartitionStep;
pub use bootloader::BootloaderStep;
pub use license::LicenseStep;
pub use network::NetworkStep;
pub use rescue::RescueStep;
pub use display_manager::DisplayManagerStep;
+68
View File
@@ -0,0 +1,68 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
pub const PACKAGE_GROUPS: &[(&str, &str, &[&str])] = &[
("office", "Office Suite", &["libreoffice", "calligra"]),
("multimedia", "Multimedia", &["vlc", "audacity", "gimp", "inkscape"]),
("development", "Development Tools",&["gcc", "make", "cmake", "git", "python3", "rust"]),
("network", "Network Tools", &["firefox", "chromium", "thunderbird", "filezilla"]),
("graphics", "Graphics & Design",&["blender", "krita", "darktable"]),
("games", "Games", &["steam", "lutris", "supertuxkart"]),
("vm", "Virtualization", &["virtualbox", "qemu", "docker"]),
("server", "Server", &["nginx", "apache", "mariadb", "postgresql"]),
];
#[derive(Default)]
#[allow(dead_code)]
pub struct NetinstallStep;
impl InstallerStep for NetinstallStep {
fn name(&self) -> String {
t!("netinstall").to_string()
}
fn on_enter(&mut self, state: &mut GlobalState) {
if state.selected_package_groups.is_empty() {
// Varsayılan ofis ve network
state.selected_package_groups = vec!["office".to_string(), "network".to_string()];
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("netinstall_title"));
ui.label(t!("netinstall_description"));
ui.add_space(10.0);
let screen_width = ui.available_width();
let cols = if screen_width > 700.0 { 3 } else if screen_width > 500.0 { 2 } else { 1 };
egui::Grid::new("netinstall_grid")
.num_columns(cols)
.spacing([8.0, 4.0])
.show(ui, |ui| {
for (key, label, _pkgs) in PACKAGE_GROUPS {
let mut selected = state.selected_package_groups.contains(&key.to_string());
ui.checkbox(&mut selected, *label);
if selected && !state.selected_package_groups.contains(&key.to_string()) {
state.selected_package_groups.push(key.to_string());
} else if !selected {
state.selected_package_groups.retain(|k| k != key);
}
}
});
ui.add_space(12.0);
if !state.selected_package_groups.is_empty() {
let total: usize = PACKAGE_GROUPS.iter()
.filter(|(k, _, _)| state.selected_package_groups.contains(&k.to_string()))
.map(|(_, _, pkgs)| pkgs.len())
.sum();
ui.label(format!("{} {} {}", total, t!("netinstall_package_count"), t!("netinstall_selected")));
}
}
fn is_complete(&self, _state: &GlobalState) -> bool {
true
}
}
+205
View File
@@ -0,0 +1,205 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
#[derive(Default)]
pub struct NetworkStep {
scan_results: Vec<WifiNetwork>,
show_password: Vec<bool>,
scanning: bool,
scan_done: bool,
proxy_enabled: bool,
}
#[derive(Clone, Default)]
struct WifiNetwork {
ssid: String,
signal: String,
secured: bool,
active: bool,
}
impl NetworkStep {
fn scan_wifi(&mut self) {
self.scanning = true;
self.scan_done = false;
self.scan_results.clear();
if let Ok(out) = std::process::Command::new("nmcli")
.args(["-t", "-f", "SSID,SIGNAL,SECURITY,ACTIVE", "dev", "wifi", "list"])
.output()
{
if out.status.success() {
let text = String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() >= 3 {
let ssid = parts[0].trim().to_string();
if ssid.is_empty() || ssid == "--" { continue; }
self.scan_results.push(WifiNetwork {
ssid,
signal: parts[1].to_string(),
secured: !parts[2].is_empty() && parts[2] != "(none)" && parts[2] != "--",
active: parts.get(3).map(|s| s.trim() == "yes").unwrap_or(false),
});
}
}
}
}
self.show_password = vec![false; self.scan_results.len()];
self.scanning = false;
self.scan_done = true;
}
fn connect_wifi(ssid: &str, password: &str) -> Result<String, String> {
let mut args = vec!["dev", "wifi", "connect", ssid];
if !password.is_empty() {
args.extend_from_slice(&["password", password]);
}
let out = std::process::Command::new("nmcli")
.args(&args)
.output()
.map_err(|e| format!("nmcli error: {}", e))?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
}
}
}
impl InstallerStep for NetworkStep {
fn name(&self) -> String {
t!("network").to_string()
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("network_title"));
ui.label(t!("network_description"));
ui.add_space(12.0);
// Mevcut bağlantı durumu
if let Ok(out) = std::process::Command::new("nmcli")
.args(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "dev", "status"])
.output()
{
let text = String::from_utf8_lossy(&out.stdout);
let connected: Vec<&str> = text.lines()
.filter(|l| l.contains(":connected:") || l.contains(":connecting:"))
.collect();
if !connected.is_empty() {
ui.colored_label(
egui::Color32::from_rgb(80, 170, 80),
format!("{} {}", t!("network_connected"), connected.join(", ")),
);
} else {
ui.colored_label(
egui::Color32::from_rgb(200, 140, 40),
t!("network_disconnected"),
);
}
}
ui.add_space(8.0);
// Proxy ayarları
ui.horizontal(|ui| {
ui.add(egui::Checkbox::new(&mut self.proxy_enabled, ""));
ui.label(t!("network_proxy_label"));
});
if self.proxy_enabled {
egui::Grid::new("proxy_grid")
.num_columns(2)
.spacing([12.0, 6.0])
.show(ui, |ui| {
ui.label("HTTP:");
ui.text_edit_singleline(&mut state.network_http_proxy);
ui.end_row();
ui.label("HTTPS:");
ui.text_edit_singleline(&mut state.network_https_proxy);
ui.end_row();
ui.label("FTP:");
ui.text_edit_singleline(&mut state.network_ftp_proxy);
ui.end_row();
});
}
ui.add_space(12.0);
// Wi-Fi tara
if !self.scan_done {
if ui.add(egui::Button::new(t!("network_scan"))).clicked() {
self.scan_wifi();
}
} else {
ui.horizontal(|ui| {
if ui.add(egui::Button::new(t!("network_rescan"))).clicked() {
self.scan_done = false;
}
ui.label(
egui::RichText::new(format!("{}: {}", t!("network_scan_count"), self.scan_results.len()))
.size(12.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
);
});
}
ui.add_space(6.0);
// Wi-Fi listesi
if !self.scan_results.is_empty() {
egui::ScrollArea::vertical()
.max_height(300.0)
.show(ui, |ui| {
egui::Frame::group(ui.style())
.inner_margin(egui::Margin::same(6.0))
.show(ui, |ui| {
for (i, net) in self.scan_results.clone().iter().enumerate() {
ui.horizontal(|ui| {
let lock = if net.secured { "🔒" } else { "🌐" };
let sig = net.signal.parse::<i32>().unwrap_or(0);
let bars = if sig > -50 { "▂▄▆█" } else if sig > -67 { "▂▄▆" } else if sig > -80 { "▂▄" } else { "▂" };
let active = if net.active { " ✓" } else { "" };
ui.label(format!("{} {}{}", lock, net.ssid, active));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.label(
egui::RichText::new(bars)
.size(10.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
);
if net.secured && !net.active {
ui.add(egui::TextEdit::singleline(&mut state.network_wifi_password)
.password(!self.show_password.get(i).copied().unwrap_or(false))
.hint_text(t!("network_password_hint"))
.desired_width(120.0));
if ui.small_button("👁").clicked() {
if self.show_password.len() <= i {
self.show_password.resize(i + 1, false);
}
self.show_password[i] = !self.show_password[i];
}
}
if !net.active && ui.add(egui::Button::new(t!("network_connect"))).clicked() {
let pw = if net.secured { state.network_wifi_password.clone() } else { String::new() };
match Self::connect_wifi(&net.ssid, &pw) {
Ok(msg) => {
state.install_log.push(format!("Wi-Fi: {} — {}", net.ssid, msg));
self.scan_done = false;
}
Err(e) => {
state.install_log.push(format!("Wi-Fi error: {}", e));
}
}
}
});
});
ui.separator();
}
});
});
}
}
fn is_complete(&self, _state: &GlobalState) -> bool {
true
}
}
+440 -36
View File
@@ -5,11 +5,84 @@ use rust_i18n::t;
// sysinfo::Disks mount point başına bir kayıt döndürür (partition'lar),
// ham disk listesi için lsblk kullanıyoruz.
// ─────────────────────────────────────────────
// Mevcut disk bölümlerini oku
// ─────────────────────────────────────────────
/// Verilen diskin (`/dev/sda`, `/dev/nvme0n1` vb.) mevcut bölümlerini
/// `lsblk` aracılığıyla okur ve `CustomPartition` listesi olarak döndürür.
///
/// Hem SATA/virtio tipi (`sda` → `sda1`, `sda2`) hem de NVMe tipi
/// (`nvme0n1` → `nvme0n1p1`, `nvme0n1p2`) aygıtları desteklenir.
/// Manuel bölümleme modunda disk seçildiğinde çağrılır; böylece kullanıcı
/// diskin mevcut bölüm düzenini görebilir.
fn read_existing_partitions(disk_path: &str) -> Vec<crate::installer::CustomPartition> {
use crate::installer::{CustomPartition, FsType};
let dev_name = disk_path.trim_start_matches("/dev/");
let out = std::process::Command::new("lsblk")
.args([
"-o", "NAME,SIZE,FSTYPE,MOUNTPOINT",
"-b", "--noheadings", "-l",
disk_path,
])
.output();
let mut parts = Vec::new();
if let Ok(o) = out {
if o.status.success() {
let text = String::from_utf8_lossy(&o.stdout);
for line in text.lines() {
let mut tokens = line.split_whitespace();
let name = match tokens.next() { Some(n) => n, None => continue };
// Diskin kendisini (örn. "sda") atla — yalnızca bölümleri al
if name == dev_name { continue; }
// Bu diske ait olmayan girişleri atla
// SATA: sda1, sda2 … NVMe: nvme0n1p1, nvme0n1p2 …
if !name.starts_with(dev_name) { continue; }
let size_bytes: u64 = tokens.next().unwrap_or("0").parse().unwrap_or(0);
let size_mb = size_bytes / (1024 * 1024);
let fstype_str = tokens.next().unwrap_or("");
let fstype = match fstype_str {
"ext4" => FsType::Ext4,
"ext3" => FsType::Ext3,
"btrfs" => FsType::Btrfs,
"xfs" => FsType::Xfs,
"vfat" => FsType::Fat32,
"ntfs" => FsType::Ntfs,
"swap" => FsType::Swap,
_ => FsType::Ext4, // bilinmeyen → varsayılan
};
// lsblk mount point alanı boşsa sonraki token gelmez; unwrap_or ile güvenli al
let mountpoint = tokens.next().unwrap_or("").to_string();
parts.push(CustomPartition {
device: format!("/dev/{}", name),
size_mb,
fstype,
mountpoint,
encrypt: false,
luks_password: String::new(),
luks_name: String::new(),
});
}
}
}
parts
}
/// `/sys/block` altındaki gerçek blok aygıtlarını (partition değil) listeler.
/// lsblk'dan JSON çıktısı ayrıştırılır; başarısız olursa /sys/block fallback.
/// lsblk'dan gelen çıktı akıllıca ayrıştırılır; başarısız olursa /sys/block fallback devreye girer.
fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
// lsblk -d: sadece diskler (partition'lar hariç)
// -o NAME,SIZE,MODEL,TYPE -b: byte cinsinden boyut
let out = std::process::Command::new("lsblk")
.args(["-d", "-o", "NAME,SIZE,MODEL", "-b", "--noheadings"])
.output();
@@ -17,43 +90,64 @@ fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
let mut disks = Vec::new();
if let Ok(o) = out {
let text = String::from_utf8_lossy(&o.stdout);
for line in text.lines() {
let cols: Vec<&str> = line.splitn(3, char::is_whitespace)
.filter(|s| !s.is_empty())
.collect();
if cols.len() < 2 { continue; }
if o.status.success() {
let text = String::from_utf8_lossy(&o.stdout);
for line in text.lines() {
// split_whitespace ardışık tüm boşlukları tek bir ayırıcı gibi yakalar
let mut tokens = line.split_whitespace();
let name_raw = match tokens.next() {
Some(n) => n,
None => continue,
};
let name_raw = cols[0]; // örn. "sda" veya "nvme0n1"
// loop, ram, sr aygıtlarını atla
if name_raw.starts_with("loop")
|| name_raw.starts_with("ram")
|| name_raw.starts_with("sr")
{ continue; }
// loop, ram, sr (CD-ROM) ve zram aygıtlarını atla
if name_raw.starts_with("loop")
|| name_raw.starts_with("ram")
|| name_raw.starts_with("sr")
|| name_raw.starts_with("zram")
{ continue; }
let size_bytes: u64 = cols[1].trim().parse().unwrap_or(0);
let model = if cols.len() >= 3 { cols[2].trim() } else { "" };
let size_str = match tokens.next() {
Some(s) => s,
None => continue,
};
disks.push(crate::installer::DiskInfo {
name: format!("/dev/{}", name_raw),
model: model.to_string(),
size_gb: size_bytes / 1_073_741_824,
size_bytes,
});
let size_bytes: u64 = size_str.parse().unwrap_or(0);
// Eğer boyut 0 ise ekleme yapma ki lsblk patlarsa fallback çalışabilsin
if size_bytes == 0 { continue; }
// Kalan tüm token'lar model ismidir (örn: ["VBOX", "HARDDISK"])
let model_tokens: Vec<&str> = tokens.collect();
let model = model_tokens.join(" ");
disks.push(crate::installer::DiskInfo {
name: format!("/dev/{}", name_raw),
model,
size_gb: size_bytes / 1_073_741_824,
size_bytes,
});
}
}
}
// lsblk başarısız olduysa /sys/block fallback
// lsblk başarısız olduysa veya geçerli disk bulamadıysa temiz /sys/block fallback'i çalışır
if disks.is_empty() {
if let Ok(rd) = std::fs::read_dir("/sys/block") {
for entry in rd.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("loop") || name.starts_with("ram") { continue; }
if name.starts_with("loop")
|| name.starts_with("ram")
|| name.starts_with("sr")
|| name.starts_with("zram")
{ continue; }
let size_path = format!("/sys/block/{}/size", name);
let size_sectors: u64 = std::fs::read_to_string(&size_path)
.unwrap_or_default().trim().parse().unwrap_or(0);
let size_bytes = size_sectors * 512;
if size_bytes == 0 { continue; }
let model_path = format!("/sys/block/{}/device/model", name);
let model = std::fs::read_to_string(&model_path)
@@ -74,7 +168,7 @@ fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
use crate::installer::{
CustomPartition, DiskInfo, FsType, GlobalState, InstallerStep,
PartitionPlan, PartitionTableType,
PartitionPlan, PartitionTableType, VolumeGroup,
};
const MIN_DISK_GB: u64 = 10;
@@ -96,18 +190,35 @@ struct ManualState {
add_size_str: String,
/// Diyalog: bağlama noktası
add_mountpoint: String,
/// Diyalog: LUKS şifrele
add_encrypt: bool,
/// Diyalog: LUKS parola
add_luks_password: String,
/// Silinmek istenen bölüm indeksi (onay isteği)
delete_idx: Option<usize>,
/// Genel hata mesajı
error: Option<String>,
/// Yeni VG adı (LVM)
add_vg_name: String,
// ── LVM Logical Volume Ekleme State'leri ───────────────────
adding_lv: bool,
add_lv_name: String,
add_lv_vg_idx: usize,
add_lv_size_str: String,
add_lv_fstype: usize,
add_lv_mountpoint: String,
}
const FS_OPTIONS: &[(&str, FsType)] = &[
("ext4", FsType::Ext4),
("ext3", FsType::Ext3),
("btrfs", FsType::Btrfs),
("xfs", FsType::Xfs),
("FAT32 (EFI)", FsType::Fat32),
("NTFS", FsType::Ntfs),
("swap", FsType::Swap),
("LVM PV", FsType::Lvm),
];
// ─────────────────────────────────────────────
@@ -118,6 +229,7 @@ pub struct PartitionStep {
scanned: bool,
is_uefi: bool,
manual: ManualState,
show_luks_password: bool,
}
impl Default for PartitionStep {
@@ -126,6 +238,7 @@ impl Default for PartitionStep {
scanned: false,
is_uefi: std::path::Path::new("/sys/firmware/efi").exists(),
manual: ManualState::default(),
show_luks_password: false,
}
}
}
@@ -163,12 +276,19 @@ fn make_plan(disk: &DiskInfo, is_uefi: bool) -> Option<PartitionPlan> {
efi_mb,
swap_mb,
root_mb,
encrypt_root: false,
luks_password: String::new(),
})
}
/// Manuel listedeki bölümlerin geçerliliğini kontrol eder.
fn validate_manual(parts: &[CustomPartition], is_uefi: bool) -> Option<String> {
let has_root = parts.iter().any(|p| p.mountpoint == "/");
fn validate_manual(
parts: &[CustomPartition],
lvs: &[crate::installer::LogicalVolume],
is_uefi: bool,
) -> Option<String> {
let has_root = parts.iter().any(|p| p.mountpoint == "/")
|| lvs.iter().any(|lv| lv.mountpoint == "/");
let has_efi = parts.iter().any(|p| p.mountpoint == "/boot/efi" || p.fstype == FsType::Fat32);
if !has_root {
@@ -224,7 +344,7 @@ impl InstallerStep for PartitionStep {
state.partition_plan.is_some()
} else {
!state.custom_partitions.is_empty()
&& validate_manual(&state.custom_partitions, self.is_uefi).is_none()
&& validate_manual(&state.custom_partitions, &state.logical_volumes, self.is_uefi).is_none()
}
}
}
@@ -234,7 +354,7 @@ impl InstallerStep for PartitionStep {
// ─────────────────────────────────────────────
impl PartitionStep {
fn show_auto_mode(&self, ui: &mut egui::Ui, state: &mut GlobalState) {
fn show_auto_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.group(|ui| {
ui.label(t!("select_disk_label"));
ui.add_space(4.0);
@@ -296,6 +416,30 @@ impl PartitionStep {
ui.add_space(8.0);
ui.label(format!("{}: {}", t!("partition_table_type"), plan.table_type));
// ── LUKS şifreleme ─────────────────────────────────────
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
let plan = state.partition_plan.as_mut().unwrap();
ui.checkbox(&mut plan.encrypt_root, t!("luks_enable_root"));
if plan.encrypt_root {
ui.horizontal(|ui| {
ui.label(t!("luks_password"));
if self.show_luks_password {
ui.text_edit_singleline(&mut plan.luks_password);
} else {
ui.add(egui::TextEdit::singleline(&mut plan.luks_password).password(true));
}
let eye = if self.show_luks_password { "🙈" } else { "👁" };
if ui.small_button(eye).clicked() {
self.show_luks_password = !self.show_luks_password;
}
});
if plan.luks_password.len() < 4 && !plan.luks_password.is_empty() {
ui.colored_label(egui::Color32::YELLOW, t!("luks_password_weak"));
}
}
ui.add_space(10.0);
egui::Frame::none()
.fill(egui::Color32::from_rgba_unmultiplied(200, 60, 40, 25))
@@ -329,8 +473,9 @@ impl PartitionStep {
let label = egui::RichText::new(format!("{} — {} GB ({})", disk.name, disk.size_gb, disk.model)).strong().color(crate::ui::theme::c_accent());
if ui.radio(selected, label).clicked() {
state.selected_disk = Some(disk.name.clone());
// Disk değişince listeyi sıfırla
state.custom_partitions.clear();
// Disk değişince mevcut bölümleri oku ve listeyi güncelle.
// Böylece kullanıcı diskin mevcut düzenini görebilir.
state.custom_partitions = read_existing_partitions(&disk.name);
self.manual.error = None;
}
}
@@ -375,8 +520,12 @@ impl PartitionStep {
};
ui.label(&part.device);
let mut fstype_label = part.fstype.label().to_string();
if part.encrypt {
fstype_label = format!("🔐 {}", fstype_label);
}
ui.label(&size_str);
ui.label(part.fstype.label());
ui.label(fstype_label);
ui.label(&part.mountpoint);
if ui.small_button("🗑").on_hover_text(t!("mp_delete")).clicked() {
delete_idx = Some(i);
@@ -451,7 +600,7 @@ impl PartitionStep {
// Dosya sistemi
ui.label(t!("mp_fstype"));
egui::ComboBox::from_id_source("fs_combo")
.selected_text(FS_OPTIONS[self.manual.add_fstype].0)
.selected_text(FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?"))
.show_ui(ui, |ui| {
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
ui.selectable_value(
@@ -472,6 +621,33 @@ impl PartitionStep {
.hint_text(t!("mp_add_dialog_hint"));
ui.add(mp_field);
ui.end_row();
// LUKS şifreleme
ui.label(t!("luks_enable_root"));
let fstype = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4);
let can_encrypt = matches!(fstype, FsType::Ext4 | FsType::Ext3 | FsType::Btrfs | FsType::Xfs);
ui.add_enabled_ui(can_encrypt, |ui| {
ui.checkbox(&mut self.manual.add_encrypt, "");
});
if !can_encrypt { self.manual.add_encrypt = false; }
ui.end_row();
if self.manual.add_encrypt {
ui.label(t!("luks_password"));
let pw = &mut self.manual.add_luks_password;
ui.horizontal(|ui| {
if self.show_luks_password {
ui.text_edit_singleline(pw);
} else {
ui.add(egui::TextEdit::singleline(pw).password(true));
}
let eye = if self.show_luks_password { "🙈" } else { "👁" };
if ui.small_button(eye).clicked() {
self.show_luks_password = !self.show_luks_password;
}
});
ui.end_row();
}
});
ui.add_space(8.0);
@@ -481,19 +657,26 @@ impl PartitionStep {
let size_mb: u64 = self.manual.add_size_str
.trim().parse().unwrap_or(0);
let mp = self.manual.add_mountpoint.trim().to_string();
let fstype = FS_OPTIONS[self.manual.add_fstype].1.clone();
let fstype = FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4);
let part_num = state.custom_partitions.len() + 1;
let device = crate::installer::part_path(&disk, part_num as u32);
if mp.is_empty() {
self.manual.error = Some(t!("mp_error_empty_mountpoint").to_string());
} else {
let encrypt = self.manual.add_encrypt;
let luks_password = if encrypt { self.manual.add_luks_password.clone() } else { String::new() };
state.custom_partitions.push(CustomPartition {
device,
size_mb,
fstype,
mountpoint: mp,
encrypt,
luks_password,
luks_name: String::new(),
});
self.manual.add_encrypt = false;
self.manual.add_luks_password.clear();
self.manual.error = None;
self.manual.adding = false;
}
@@ -505,13 +688,234 @@ impl PartitionStep {
});
}
// ── LVM Yapılandırması ──────────────────────────────────
ui.add_space(12.0);
let has_lvm_pvs = state.custom_partitions.iter().any(|p| p.fstype == FsType::Lvm);
if has_lvm_pvs {
ui.label(egui::RichText::new("LVM Volume Groups").strong().size(14.0));
ui.add_space(4.0);
egui::Grid::new("lvm_vg_grid")
.num_columns(4)
.spacing([12.0, 6.0])
.striped(true)
.show(ui, |ui| {
ui.label("VG Name");
ui.label("PV Devices");
ui.label("Size");
ui.label("");
ui.end_row();
let mut remove_vg: Option<usize> = None;
for (vi, vg) in state.volume_groups.iter().enumerate() {
let pv_list = vg.pv_devices.join(", ");
let size_str = if vg.size_mb >= 1024 {
format!("{:.1} GB", vg.size_mb as f64 / 1024.0)
} else {
format!("{} MB", vg.size_mb)
};
ui.label(&vg.name);
ui.label(&pv_list);
ui.label(&size_str);
if ui.small_button("🗑").clicked() {
remove_vg = Some(vi);
}
ui.end_row();
}
if let Some(vi) = remove_vg {
state.volume_groups.remove(vi);
}
// Yeni VG ekleme
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut self.manual.add_vg_name);
if ui.small_button("➕ VG").clicked()
&& !self.manual.add_vg_name.is_empty()
{
let pvs: Vec<String> = state.custom_partitions.iter()
.filter(|p| p.fstype == FsType::Lvm)
.map(|p| p.device.clone())
.collect();
let size: u64 = state.custom_partitions.iter()
.filter(|p| p.fstype == FsType::Lvm)
.map(|p| p.size_mb)
.sum();
state.volume_groups.push(VolumeGroup {
name: self.manual.add_vg_name.clone(),
pv_devices: pvs,
size_mb: size,
});
self.manual.add_vg_name.clear();
}
});
ui.label("");
ui.label("");
ui.end_row();
});
// ── LVM Logical Volumes Tablosu ──────────────────────────
let has_vgs = !state.volume_groups.is_empty();
if has_vgs {
ui.add_space(12.0);
ui.label(egui::RichText::new(t!("lvm_lv_title")).strong().size(14.0));
ui.add_space(4.0);
let mut remove_lv: Option<usize> = None;
egui::Grid::new("lvm_lv_grid")
.num_columns(5)
.spacing([12.0, 6.0])
.striped(true)
.show(ui, |ui| {
ui.label(egui::RichText::new(t!("lvm_lv_name")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("lvm_lv_vg")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("mp_size")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("mp_mountpoint")).strong().color(crate::ui::theme::c_text()));
ui.label(egui::RichText::new(t!("mp_action")).strong().color(crate::ui::theme::c_text()));
ui.end_row();
for (li, lv) in state.logical_volumes.iter().enumerate() {
let size_str = if lv.size_mb == 0 {
t!("mp_size_remaining").to_string()
} else {
format!("{} MB", lv.size_mb)
};
ui.label(&lv.name);
ui.label(&lv.vg_name);
ui.label(size_str);
ui.label(&lv.mountpoint);
if ui.small_button("🗑").clicked() {
remove_lv = Some(li);
}
ui.end_row();
}
});
if let Some(li) = remove_lv {
state.logical_volumes.remove(li);
}
ui.add_space(8.0);
// "Logical Volume Ekle" butonu
let lv_btn = egui::Button::new(format!("➕ {}", t!("lvm_lv_add")))
.fill(accent.gamma_multiply(0.15));
if ui.add(lv_btn).clicked() {
self.manual.adding_lv = true;
self.manual.add_lv_name.clear();
self.manual.add_lv_vg_idx = 0;
self.manual.add_lv_size_str.clear();
self.manual.add_lv_fstype = 0;
self.manual.add_lv_mountpoint.clear();
}
}
}
// ── Logical Volume Ekleme Diyaloğu ────────────────────────
if self.manual.adding_lv {
egui::Window::new(t!("lvm_lv_add"))
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ui.ctx(), |ui| {
egui::Grid::new("add_lv_grid")
.num_columns(2)
.spacing([12.0, 8.0])
.show(ui, |ui| {
// Hacim Grubu (VG) seçimi
ui.label(t!("lvm_lv_vg"));
let current_vg = state.volume_groups.get(self.manual.add_lv_vg_idx)
.map(|vg| vg.name.as_str())
.unwrap_or("?");
egui::ComboBox::from_id_source("lv_vg_combo")
.selected_text(current_vg)
.show_ui(ui, |ui| {
for (vi, vg) in state.volume_groups.iter().enumerate() {
ui.selectable_value(
&mut self.manual.add_lv_vg_idx, vi, &vg.name
);
}
});
ui.end_row();
// LV Adı
ui.label(t!("lvm_lv_name"));
ui.text_edit_singleline(&mut self.manual.add_lv_name);
ui.end_row();
// Dosya sistemi
ui.label(t!("mp_fstype"));
egui::ComboBox::from_id_source("lv_fs_combo")
.selected_text(FS_OPTIONS.get(self.manual.add_lv_fstype).map(|x| x.0).unwrap_or("?"))
.show_ui(ui, |ui| {
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
ui.selectable_value(
&mut self.manual.add_lv_fstype, i, *label
);
}
});
ui.end_row();
// Boyut
ui.label(t!("mp_size_hint"));
ui.text_edit_singleline(&mut self.manual.add_lv_size_str);
ui.end_row();
// Bağlama noktası
ui.label(t!("mp_mountpoint"));
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_lv_mountpoint)
.hint_text(t!("mp_add_dialog_hint"));
ui.add(mp_field);
ui.end_row();
});
ui.add_space(8.0);
ui.horizontal(|ui| {
let ok_btn = egui::Button::new(t!("mp_add_btn")).fill(accent.gamma_multiply(0.2));
if ui.add(ok_btn).clicked() {
let size_mb: u64 = self.manual.add_lv_size_str.trim().parse().unwrap_or(0);
let mp = self.manual.add_lv_mountpoint.trim().to_string();
let lv_name = self.manual.add_lv_name.trim().to_string();
let fstype = FS_OPTIONS.get(self.manual.add_lv_fstype).map(|x| x.1.clone()).unwrap_or(FsType::Ext4);
if lv_name.is_empty() {
self.manual.error = Some("LV Adı boş olamaz.".to_string());
} else if mp.is_empty() {
self.manual.error = Some(t!("mp_error_empty_mountpoint").to_string());
} else if let Some(vg) = state.volume_groups.get(self.manual.add_lv_vg_idx) {
// VG sınır doğrulaması
let current_sum: u64 = state.logical_volumes.iter()
.filter(|lv| lv.vg_name == vg.name)
.map(|lv| lv.size_mb)
.sum();
if current_sum + size_mb > vg.size_mb && size_mb > 0 {
self.manual.error = Some(t!("lvm_lv_error_size").to_string());
} else {
state.logical_volumes.push(crate::installer::LogicalVolume {
name: lv_name,
vg_name: vg.name.clone(),
size_mb,
fstype,
mountpoint: mp,
});
self.manual.error = None;
self.manual.adding_lv = false;
}
} else {
self.manual.error = Some(t!("lvm_lv_error_vg").to_string());
}
}
if ui.button(t!("cancel")).clicked() {
self.manual.adding_lv = false;
}
});
});
}
// ── Doğrulama mesajları ─────────────────────────────────
ui.add_space(6.0);
if let Some(ref err) = self.manual.error {
ui.colored_label(egui::Color32::from_rgb(217, 0, 91), err);
}
if !state.custom_partitions.is_empty() {
if let Some(warn) = validate_manual(&state.custom_partitions, self.is_uefi) {
if let Some(warn) = validate_manual(&state.custom_partitions, &state.logical_volumes, self.is_uefi) {
ui.colored_label(egui::Color32::from_rgb(220, 160, 40), warn);
} else {
ui.colored_label(
+516
View File
@@ -0,0 +1,516 @@
//! Kurtarma modu adımı.
//!
//! Özellikler:
//! - Mevcut Linux kurulumlarını tarar (lsblk)
//! - Seçilen bölümü /mnt'a bağlar
//! - chroot kabuğu başlatır
//! - GRUB'u yeniden yükler
//! - **Pisi paket geçmişini listeler ve `pisi takeback <N>` ile geri alır**
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
/// Pisi geçmişindeki tek bir işlem kaydı.
#[derive(Clone, Debug)]
struct PisiHistoryEntry {
/// İşlem numarası (takeback argümanı)
pub op: u32,
/// Tarih/saat dizisi
pub date: String,
/// İşlem türü (install, remove, upgrade, …)
pub op_type: String,
/// Etkilenen paket sayısı veya açıklaması
pub summary: String,
}
/// Pisi geçmiş geri alma modunun durumu.
#[derive(Default)]
struct PisiHistoryState {
entries: Vec<PisiHistoryEntry>,
selected: Option<usize>,
scanned: bool,
running: bool,
result_msg: Option<String>,
result_ok: Option<bool>,
}
#[derive(Default)]
pub struct RescueStep {
detected: Vec<String>,
selected: Option<usize>,
log: Vec<String>,
mount_done: bool,
pisi: PisiHistoryState,
/// Aktif sekme: 0=Chroot/GRUB, 1=Pisi Geçmiş
tab: usize,
}
impl RescueStep {
fn detect_installations(&mut self) {
self.detected.clear();
self.mount_done = false;
if let Ok(out) = std::process::Command::new("lsblk")
.args(["-o", "NAME,FSTYPE,MOUNTPOINT", "-l", "-n"])
.output()
{
let text = String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let fstype = parts[1];
if fstype == "ext4" || fstype == "btrfs" || fstype == "xfs" {
let dev = format!("/dev/{}", parts[0]);
let mp = parts.get(2).unwrap_or(&"-").to_string();
self.detected.push(format!("{} ({}) [{}]", dev, fstype, mp));
}
}
}
}
}
/// Seçili bölümü /mnt'a bağlar. Başarılıysa `true` döner.
fn mount_selected(&mut self) -> bool {
if let Some(idx) = self.selected {
if let Some(entry) = self.detected.get(idx) {
// Entry: "/dev/sda1 (ext4) [-]" — sadece ilk token aygıt yolu
let dev = entry.split_whitespace().next().unwrap_or("");
if dev.is_empty() { return false; }
// /mnt boşsa bağla
let r = std::process::Command::new("mount")
.args([dev, "/mnt"])
.status();
if let Ok(s) = r {
if s.success() {
self.mount_done = true;
self.log.push(format!("✓ {} → /mnt bağlandı.", dev));
return true;
} else {
self.log.push(format!("⚠ {} bağlanamadı (zaten bağlı veya hata).", dev));
self.mount_done = true; // Belki zaten bağlı
return true;
}
}
}
}
false
}
fn run_rescue_cmd(cmd: &str, args: &[&str]) -> Result<String, String> {
let out = std::process::Command::new(cmd)
.args(args)
.output()
.map_err(|e| format!("{} hatası: {}", cmd, e))?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_string())
} else {
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
}
}
/// `pisi history` çıktısını ayrıştırır.
/// Örnek satır: "Operation 42 [2024-03-15 12:00:01]: install [vim]"
fn parse_pisi_history(raw: &str) -> Vec<PisiHistoryEntry> {
let mut entries = Vec::new();
for line in raw.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') { continue; }
// "Operation 42 [2024-03-15 12:00:01]: install [vim, curl]"
if let Some(rest) = line.strip_prefix("Operation ") {
let mut parts = rest.splitn(2, ' ');
let op_str = parts.next().unwrap_or("0");
let remainder = parts.next().unwrap_or("");
let op: u32 = op_str.parse().unwrap_or(0);
// Tarih: "[2024-03-15 12:00:01]"
let date = if let Some(start) = remainder.find('[') {
if let Some(end) = remainder.find(']') {
remainder[start+1..end].to_string()
} else { String::new() }
} else { String::new() };
// Tür + özet: ": install [vim, curl]"
let (op_type, summary) = if let Some(colon) = remainder.rfind("]: ") {
let after = &remainder[colon+3..];
if let Some(sp) = after.find(' ') {
(after[..sp].to_string(), after[sp+1..].to_string())
} else {
(after.to_string(), String::new())
}
} else { ("unknown".to_string(), remainder.to_string()) };
entries.push(PisiHistoryEntry { op, date, op_type, summary });
}
}
// En yeni önce
entries.sort_by(|a, b| b.op.cmp(&a.op));
entries
}
/// /mnt chroot içinde `pisi history` çalıştırır ve ayrıştırır.
fn scan_pisi_history(&mut self) {
self.pisi.entries.clear();
self.pisi.result_msg = None;
self.pisi.selected = None;
let out = std::process::Command::new("chroot")
.args(["/mnt", "pisi", "history"])
.output();
match out {
Ok(o) => {
let text = String::from_utf8_lossy(&o.stdout).to_string();
let entries = Self::parse_pisi_history(&text);
if entries.is_empty() {
self.pisi.result_msg = Some(t!("rescue_pisi_history_empty").to_string());
self.pisi.result_ok = Some(false);
}
self.pisi.entries = entries;
}
Err(e) => {
self.pisi.result_msg = Some(format!("{}", e));
self.pisi.result_ok = Some(false);
}
}
self.pisi.scanned = true;
}
/// `pisi takeback <op>` çalıştırır.
fn run_takeback(&mut self, op: u32) {
self.pisi.running = true;
self.pisi.result_msg = Some(t!("rescue_pisi_takeback_start").to_string());
self.pisi.result_ok = None;
let op_str = op.to_string();
let out = std::process::Command::new("chroot")
.args(["/mnt", "pisi", "takeback", &op_str])
.output();
self.pisi.running = false;
match out {
Ok(o) if o.status.success() => {
self.pisi.result_ok = Some(true);
self.pisi.result_msg = Some(t!("rescue_pisi_history_ok").to_string());
self.log.push(format!("✓ pisi takeback {} başarılı.", op));
}
Ok(o) => {
let err = String::from_utf8_lossy(&o.stderr).trim().to_string();
self.pisi.result_ok = Some(false);
self.pisi.result_msg = Some(t!("rescue_pisi_history_err", err = err).to_string());
self.log.push(format!("❌ pisi takeback {}: {}", op, err));
}
Err(e) => {
self.pisi.result_ok = Some(false);
self.pisi.result_msg = Some(t!("rescue_pisi_history_err", err = e.to_string()).to_string());
}
}
}
// ─── UI Panelleri ────────────────────────────────────────────────
fn show_chroot_panel(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.add_space(6.0);
// Bölüm listesi
ui.label(egui::RichText::new(t!("rescue_detected")).strong().color(theme::c_text()));
ui.add_space(4.0);
egui::ScrollArea::vertical()
.id_source("rescue_partition_list")
.max_height(130.0)
.show(ui, |ui| {
for (i, entry) in self.detected.iter().enumerate() {
let is_selected = self.selected == Some(i);
let label = egui::RichText::new(entry)
.family(egui::FontFamily::Monospace)
.color(if is_selected { theme::c_accent() } else { theme::c_text() });
if ui.selectable_label(is_selected, label).clicked() {
self.selected = Some(i);
self.mount_done = false;
}
}
});
ui.add_space(10.0);
// İşlem butonları
ui.horizontal(|ui| {
// 1. Bağla
if ui.add_enabled(
self.selected.is_some() && !self.mount_done,
theme::secondary_button("📂 /mnt'a Bağla"),
).clicked() {
self.mount_selected();
}
// 2. Chroot
if ui.add_enabled(
self.mount_done,
theme::secondary_button(&t!("rescue_chroot")),
).clicked() {
self.log.push(t!("rescue_chroot_start").to_string());
match Self::run_rescue_cmd("chroot", &["/mnt", "/bin/bash", "-c", "echo 'chroot OK'"]) {
Ok(msg) => self.log.push(format!("✓ {}", msg)),
Err(e) => self.log.push(format!("❌ {}", e)),
}
}
// 3. GRUB yeniden yükle
if ui.add_enabled(
self.mount_done,
theme::secondary_button(&t!("rescue_grub")),
).clicked() {
self.log.push(t!("rescue_grub_start").to_string());
let boot_dev = state.bootloader_device.clone();
let steps: &[(&str, &[&str])] = &[
("mount", &["--bind", "/proc", "/mnt/proc"]),
("mount", &["--bind", "/sys", "/mnt/sys"]),
("mount", &["--bind", "/dev", "/mnt/dev"]),
];
for (cmd, args) in steps {
match Self::run_rescue_cmd(cmd, args) {
Ok(m) if !m.is_empty() => self.log.push(m),
Ok(_) => {},
Err(e) => self.log.push(format!("⚠ {}", e)),
}
}
// grub2-install
let grub_args: Vec<&str> = vec!["grub2-install", &boot_dev];
match Self::run_rescue_cmd("chroot", &{
let mut a: Vec<&str> = vec!["/mnt"];
a.extend_from_slice(&grub_args);
a
}) {
Ok(m) => self.log.push(format!("✓ grub2-install: {}", m)),
Err(e) => self.log.push(format!("❌ grub2-install: {}", e)),
}
// grub2-mkconfig
match Self::run_rescue_cmd("chroot", &["/mnt", "grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"]) {
Ok(m) => self.log.push(format!("✓ grub2-mkconfig: {}", m)),
Err(e) => self.log.push(format!("❌ grub2-mkconfig: {}", e)),
}
}
});
}
fn show_pisi_panel(&mut self, ui: &mut egui::Ui) {
ui.add_space(6.0);
ui.label(
egui::RichText::new(t!("rescue_pisi_history_desc"))
.size(12.0)
.color(theme::c_text_dim()),
);
ui.add_space(8.0);
if !self.mount_done {
theme::warning_box(ui, "Önce 'Chroot & GRUB' sekmesinden bölümü /mnt'a bağlayın.");
return;
}
// Tara butonu
ui.horizontal(|ui| {
if ui.add(theme::secondary_button(&t!("rescue_pisi_history_scan"))).clicked() {
self.scan_pisi_history();
}
if self.pisi.scanned {
ui.label(
egui::RichText::new(format!("{} işlem listelendi", self.pisi.entries.len()))
.size(12.0)
.color(theme::c_text_dim()),
);
}
});
if self.pisi.entries.is_empty() {
if let Some(ref msg) = self.pisi.result_msg.clone() {
ui.add_space(6.0);
theme::warning_box(ui, msg);
}
return;
}
ui.add_space(8.0);
// Geçmiş tablosu
egui::ScrollArea::vertical()
.id_source("pisi_history_scroll")
.max_height(200.0)
.show(ui, |ui| {
egui::Grid::new("pisi_history_grid")
.num_columns(4)
.spacing([12.0, 4.0])
.striped(true)
.show(ui, |ui| {
// Başlık satırı
ui.label(egui::RichText::new("#").strong().color(theme::c_text_dim()));
ui.label(egui::RichText::new("Tarih").strong().color(theme::c_text_dim()));
ui.label(egui::RichText::new("İşlem").strong().color(theme::c_text_dim()));
ui.label(egui::RichText::new("Paketler").strong().color(theme::c_text_dim()));
ui.end_row();
for (i, entry) in self.pisi.entries.iter().enumerate() {
let is_selected = self.pisi.selected == Some(i);
let row_color = if is_selected {
theme::c_accent()
} else {
theme::c_text()
};
let op_label = egui::RichText::new(entry.op.to_string())
.family(egui::FontFamily::Monospace)
.color(row_color);
if ui.selectable_label(is_selected, op_label).clicked() {
self.pisi.selected = Some(i);
}
ui.label(egui::RichText::new(&entry.date).size(11.0).color(row_color));
// İşlem türüne renkli badge
let type_color = match entry.op_type.as_str() {
"install" => egui::Color32::from_rgb(80, 180, 80),
"remove" => egui::Color32::from_rgb(200, 70, 70),
"upgrade" => egui::Color32::from_rgb(70, 140, 220),
_ => theme::c_text_dim(),
};
ui.colored_label(type_color, &entry.op_type);
// Özet — truncate
let summary = if entry.summary.len() > 50 {
format!("{}…", &entry.summary[..50])
} else {
entry.summary.clone()
};
ui.label(egui::RichText::new(summary).size(11.0).color(theme::c_text_dim()));
ui.end_row();
}
});
});
ui.add_space(10.0);
// Geri al butonu
let can_takeback = self.pisi.selected.is_some() && !self.pisi.running;
if ui.add_enabled(can_takeback, theme::primary_button(&t!("rescue_pisi_takeback"))).clicked() {
if let Some(idx) = self.pisi.selected {
let op = self.pisi.entries[idx].op;
self.run_takeback(op);
}
}
// Sonuç mesajı
if let Some(ref msg) = self.pisi.result_msg.clone() {
ui.add_space(6.0);
match self.pisi.result_ok {
Some(true) => theme::success_box(ui, msg),
Some(false) => theme::error_box(ui, msg),
None => { ui.label(msg); }
}
}
}
}
impl InstallerStep for RescueStep {
fn name(&self) -> String { t!("rescue").to_string() }
fn on_enter(&mut self, _state: &mut GlobalState) {
self.detect_installations();
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
theme::section_heading(ui, &t!("rescue_title"));
ui.label(
egui::RichText::new(t!("rescue_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(10.0);
if self.detected.is_empty() {
theme::warning_box(ui, &t!("rescue_no_installations"));
ui.add_space(6.0);
if ui.add(theme::secondary_button(&t!("rescue_scan"))).clicked() {
self.detect_installations();
}
return;
}
// ─── Sekmeler ───────────────────────────────────────────────
ui.horizontal(|ui| {
let tab_labels = [
("💻 Chroot & GRUB", 0usize),
("📜 Pisi Geçmiş", 1usize),
];
for (label, idx) in &tab_labels {
let active = self.tab == *idx;
let rt = egui::RichText::new(*label)
.strong()
.color(if active { theme::c_accent() } else { theme::c_text_dim() });
if ui.selectable_label(active, rt).clicked() {
self.tab = *idx;
}
ui.separator();
}
});
ui.separator();
ui.add_space(4.0);
match self.tab {
0 => self.show_chroot_panel(ui, state),
1 => self.show_pisi_panel(ui),
_ => {}
}
ui.add_space(12.0);
// Log alanı — her iki sekmede de görünür
if !self.log.is_empty() {
ui.separator();
ui.add_space(4.0);
ui.label(
egui::RichText::new("Log")
.size(11.0)
.color(theme::c_text_dim()),
);
egui::ScrollArea::vertical()
.id_source("rescue_log")
.max_height(160.0)
.stick_to_bottom(true)
.show(ui, |ui| {
egui::Frame::group(ui.style())
.inner_margin(egui::Margin::same(8.0))
.show(ui, |ui| {
for line in &self.log {
let color = if line.starts_with("✓") {
theme::c_success()
} else if line.starts_with("❌") {
theme::c_error()
} else if line.starts_with("⚠") {
theme::c_warning()
} else {
theme::c_text_dim()
};
ui.colored_label(
color,
egui::RichText::new(line)
.size(11.0)
.family(egui::FontFamily::Monospace),
);
}
});
});
}
// Geri dön butonu
ui.add_space(8.0);
if ui.add(theme::secondary_button(&t!("rescue_return"))).clicked() {
state.rescue_mode = false;
}
}
fn is_complete(&self, _state: &GlobalState) -> bool {
false // Rescue modu her zaman "tamamlanmamış" — normal kuruluma dönmek için rescue_mode=false
}
}
+33
View File
@@ -78,6 +78,39 @@ impl InstallerStep for SummaryStep {
summary_row(ui, t!("summary_custom_partitions"), &plan_lines.join("\n"));
}
// Tarih/Saat
let dt_str = if state.use_ntp {
t!("summary_ntp_enabled").to_string()
} else {
format!("{:04}-{:02}-{:02} {:02}:{:02} (NTP off)",
state.manual_year, state.manual_month, state.manual_day,
state.manual_hour, state.manual_minute)
};
summary_row(ui, t!("summary_datetime"), &dt_str);
// Bootloader
let bl_str = format!("{} ({}s timeout{})",
state.bootloader_device,
state.bootloader_timeout,
if state.bootloader_password.is_empty() { "" } else { ", password protected" }
);
summary_row(ui, t!("summary_bootloader"), &bl_str);
// Display Manager
let dm_str = format!("{} / {} ({})",
state.display_manager,
state.desktop_environment,
if state.autologin { t!("summary_autologin") } else { std::borrow::Cow::Borrowed("manual login") }
);
summary_row(ui, t!("summary_display_manager"), &dm_str);
let root_str = if state.root_password.is_empty() {
t!("summary_root_disabled").to_string()
} else {
t!("summary_root_enabled").to_string()
};
summary_row(ui, t!("summary_root"), &root_str);
summary_row(ui, t!("summary_username"), &state.username);
summary_row(ui, t!("summary_hostname"), &state.hostname);
});
+171 -90
View File
@@ -2,11 +2,13 @@ use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
/// Kullanıcı adı, şifre ve hostname ayarları adımı.
/// Calamares'in Users modülüne karşılık gelir.
/// Kullanıcı adı, şifre, hostname ve yönetici (root) şifresi ayarları adımı.
pub struct UsersStep {
show_password: bool,
show_password_confirm: bool,
show_root_password: bool,
show_root_password_confirm: bool,
use_same_for_admin: bool,
}
impl Default for UsersStep {
@@ -14,6 +16,9 @@ impl Default for UsersStep {
Self {
show_password: false,
show_password_confirm: false,
show_root_password: false,
show_root_password_confirm: false,
use_same_for_admin: true,
}
}
}
@@ -77,110 +82,186 @@ impl InstallerStep for UsersStep {
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("users_title"));
ui.label(t!("users_description"));
use crate::ui::theme;
theme::section_heading(ui, &t!("users_title"));
ui.label(
egui::RichText::new(t!("users_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(12.0);
egui::Grid::new("users_grid")
.num_columns(2)
.spacing([12.0, 10.0])
// ─── Kullanıcı Bilgileri Grubu ──────────────────────────────────
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| {
// Kullanıcı adı
ui.label(t!("username_label"));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.username);
if !state.username.is_empty() {
if is_valid_username(&state.username) {
ui.colored_label(egui::Color32::from_rgb(80, 170, 80), "✓");
} else {
ui.colored_label(egui::Color32::from_rgb(200, 60, 40), "✗");
}
}
});
ui.end_row();
ui.set_width(ui.available_width());
egui::Grid::new("users_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Kullanıcı adı
ui.label(egui::RichText::new(t!("username_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.text_edit_singleline(&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_error(), "✗");
}
}
});
ui.end_row();
// Şifre
ui.label(t!("password_label"));
ui.horizontal(|ui| {
if self.show_password {
ui.text_edit_singleline(&mut state.password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password).password(true));
}
if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() {
self.show_password = !self.show_password;
}
});
ui.end_row();
// Şifre
ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if self.show_password {
ui.text_edit_singleline(&mut state.password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password).password(true));
}
if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() {
self.show_password = !self.show_password;
}
if !state.password.is_empty() {
let (label, color) = password_strength(&state.password);
ui.colored_label(color, label);
}
});
ui.end_row();
// Şifre gücü göstergesi
ui.label("");
if !state.password.is_empty() {
let (label, color) = password_strength(&state.password);
ui.colored_label(color, label);
}
ui.end_row();
// Şifre doğrulama
ui.label(egui::RichText::new(t!("password_confirm_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if self.show_password_confirm {
ui.text_edit_singleline(&mut state.password_confirm);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password_confirm).password(true));
}
if ui.small_button(if self.show_password_confirm { "🙈" } else { "👁" }).clicked() {
self.show_password_confirm = !self.show_password_confirm;
}
if !state.password_confirm.is_empty() {
if state.password == state.password_confirm {
ui.colored_label(theme::c_success(), "✓");
} else {
ui.colored_label(theme::c_error(), t!("passwords_no_match"));
}
}
});
ui.end_row();
// Şifre doğrulama
ui.label(t!("password_confirm_label"));
ui.horizontal(|ui| {
if self.show_password_confirm {
ui.text_edit_singleline(&mut state.password_confirm);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password_confirm).password(true));
}
if ui.small_button(if self.show_password_confirm { "🙈" } else { "👁" }).clicked() {
self.show_password_confirm = !self.show_password_confirm;
}
if !state.password_confirm.is_empty() {
if state.password == state.password_confirm {
ui.colored_label(egui::Color32::from_rgb(80, 170, 80), "✓");
} else {
ui.colored_label(egui::Color32::from_rgb(200, 60, 40), t!("passwords_no_match"));
}
}
});
ui.end_row();
// Hostname
ui.label(t!("hostname_label"));
ui.horizontal(|ui| {
// Username değişince hostname otomatik güncellenir (boşsa)
if state.hostname.is_empty() && !state.username.is_empty() {
state.hostname = format!("{}-pc", state.username);
}
ui.text_edit_singleline(&mut state.hostname);
if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) {
ui.colored_label(egui::Color32::from_rgb(80, 170, 80), "✓");
} else {
ui.colored_label(egui::Color32::from_rgb(200, 60, 40), "✗");
}
}
});
ui.end_row();
// Hostname
ui.label(egui::RichText::new(t!("hostname_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if state.hostname.is_empty() && !state.username.is_empty() {
state.hostname = format!("{}-pc", state.username);
}
ui.text_edit_singleline(&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_error(), "✗");
}
}
});
ui.end_row();
});
});
// Hata mesajları
ui.add_space(14.0);
// ─── Yönetici (root) Ayarları ─────────────────────────────────────
ui.checkbox(&mut self.use_same_for_admin, egui::RichText::new(t!("use_same_for_admin")).strong().color(theme::c_text()));
if self.use_same_for_admin {
state.root_password = state.password.clone();
state.root_password_confirm = state.password_confirm.clone();
} else {
ui.add_space(8.0);
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| {
ui.set_width(ui.available_width());
ui.label(egui::RichText::new(t!("admin_description")).size(12.0).color(theme::c_text_dim()));
ui.add_space(8.0);
egui::Grid::new("admin_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Root şifresi
ui.label(egui::RichText::new(t!("admin_password_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if self.show_root_password {
ui.text_edit_singleline(&mut state.root_password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.root_password).password(true));
}
if ui.small_button(if self.show_root_password { "🙈" } else { "👁" }).clicked() {
self.show_root_password = !self.show_root_password;
}
if !state.root_password.is_empty() && state.root_password.len() < 4 {
ui.colored_label(theme::c_error(), t!("admin_password_short"));
}
});
ui.end_row();
// Şifre doğrulama
ui.label(egui::RichText::new(t!("admin_password_confirm_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if self.show_root_password_confirm {
ui.text_edit_singleline(&mut state.root_password_confirm);
} else {
ui.add(egui::TextEdit::singleline(&mut state.root_password_confirm).password(true));
}
if ui.small_button(if self.show_root_password_confirm { "🙈" } else { "👁" }).clicked() {
self.show_root_password_confirm = !self.show_root_password_confirm;
}
if !state.root_password_confirm.is_empty() {
if state.root_password == state.root_password_confirm {
ui.colored_label(theme::c_success(), "✓");
} else {
ui.colored_label(theme::c_error(), t!("passwords_no_match"));
}
}
});
ui.end_row();
});
});
}
// Hata mesajları gösterimi
ui.add_space(8.0);
if !state.username.is_empty() && !is_valid_username(&state.username) {
ui.colored_label(
egui::Color32::from_rgb(200, 60, 40),
t!("username_invalid"),
);
ui.colored_label(theme::c_error(), t!("username_invalid"));
}
if !state.hostname.is_empty() && !is_valid_hostname(&state.hostname) {
ui.colored_label(
egui::Color32::from_rgb(200, 60, 40),
t!("hostname_invalid"),
);
ui.colored_label(theme::c_error(), t!("hostname_invalid"));
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
is_valid_username(&state.username)
let user_ok = is_valid_username(&state.username)
&& is_valid_hostname(&state.hostname)
&& !state.password.is_empty()
&& state.password == state.password_confirm
&& state.password.len() >= 4
&& state.password == state.password_confirm;
if self.use_same_for_admin {
user_ok
} else {
user_ok && (
state.root_password.is_empty()
|| (state.root_password.len() >= 4 && state.root_password == state.root_password_confirm)
)
}
}
}
+48 -57
View File
@@ -5,6 +5,7 @@
//! Masaüstü ortamına göre farklı slayt setleri yüklenebilir.
use eframe::egui;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use rust_i18n::t;
use std::borrow::Cow;
@@ -19,22 +20,34 @@ pub struct Slide {
/// Masaüstü ortamına göre slayt seti döner.
/// ISO meta verisi ileride `/etc/livecd-desktop` gibi bir dosyadan okunabilir.
pub fn slides_for_desktop(desktop: &str) -> Vec<Slide> {
let mut slides = default_slides();
let specific_slides = match desktop {
"budgie" => budgie_slides(),
"cinnamon" => cinnamon_slides(),
"gnome" => gnome_slides(),
"kde" | "plasma" => kde_slides(),
"lxde" => lxde_slides(),
"lxqt" => lxqt_slides(),
"lumina" => lumina_slides(),
"mate" => mate_slides(),
"xfce" => xfce_slides(),
_ => vec![],
};
slides.extend(specific_slides);
slides
pub fn slides_for_desktop(desktop: &str, branding_slides: &[crate::branding::SlideItem]) -> Vec<Slide> {
if !branding_slides.is_empty() {
branding_slides
.iter()
.filter(|item| item.desktop.is_empty() || item.desktop.to_lowercase() == desktop.to_lowercase())
.map(|item| Slide {
icon: Box::leak(item.icon.clone().into_boxed_str()),
title: Cow::Owned(item.title.clone()),
description: Cow::Owned(item.description.clone()),
})
.collect()
} else {
let mut slides = default_slides();
let specific_slides = match desktop {
"budgie" => budgie_slides(),
"cinnamon" => cinnamon_slides(),
"gnome" => gnome_slides(),
"kde" | "plasma" => kde_slides(),
"lxde" => lxde_slides(),
"lxqt" => lxqt_slides(),
"lumina" => lumina_slides(),
"mate" => mate_slides(),
"xfce" => xfce_slides(),
_ => vec![],
};
slides.extend(specific_slides);
slides
}
}
fn default_slides() -> Vec<Slide> {
@@ -175,7 +188,7 @@ fn lxde_slides() -> Vec<Slide> {
description: t!("lxde_description"),
},
Slide {
icon: "lxde_openbox_title",
icon: "lxde_openbox",
title: t!("lxde_openbox_title"),
description: t!("lxde_openbox_description"),
},
@@ -185,7 +198,7 @@ fn lxde_slides() -> Vec<Slide> {
description: t!("lxde_pcmanfm_description"),
},
Slide {
icon: "lxde_lxpanel_title",
icon: "lxde_lxpanel",
title: t!("lxde_lxpanel_title"),
description: t!("lxde_lxpanel_description"),
},
@@ -200,7 +213,7 @@ fn lxqt_slides() -> Vec<Slide> {
description: t!("lxqt_description"),
},
Slide {
icon: "lxqt_panel_title",
icon: "lxqt_panel",
title: t!("lxqt_panel_title"),
description: t!("lxqt_panel_description"),
},
@@ -220,17 +233,17 @@ fn lumina_slides() -> Vec<Slide> {
description: t!("lumina_description"),
},
Slide {
icon: "lumina_applications_title",
icon: "lumina_applications",
title: t!("lumina_applications_title"),
description: t!("lumina_applications_description"),
},
Slide {
icon: "lumina_theme_title",
icon: "lumina_theme",
title: t!("lumina_theme_title"),
description: t!("lumina_theme_description"),
},
Slide {
icon: "lumina_panel_title",
icon: "lumina_panel",
title: t!("lumina_panel_title"),
description: t!("lumina_panel_description"),
},
@@ -250,12 +263,12 @@ fn mate_slides() -> Vec<Slide> {
description: t!("mate_caja_description"),
},
Slide {
icon: "mate_settings_title",
icon: "mate_settings",
title: t!("mate_settings_title"),
description: t!("mate_settings_description"),
},
Slide {
icon: "mate_applications_title",
icon: "mate_applications",
title: t!("mate_applications_title"),
description: t!("mate_applications_description"),
},
@@ -270,7 +283,7 @@ fn xfce_slides() -> Vec<Slide> {
description: t!("xfce_description"),
},
Slide {
icon: "xfce_panel_title",
icon: "xfce_panel",
title: t!("xfce_panel_title"),
description: t!("xfce_panel_description"),
},
@@ -288,20 +301,23 @@ fn xfce_slides() -> Vec<Slide> {
}
// ─── Slayt gösterisi widget'ı ────────────────────────────────────
// ─── Slayt gösterisi widget'ı ───────────────────────────────────────────
pub struct Slideshow {
slides: Vec<Slide>,
current: usize,
last_advance: Instant,
/// branding.toml'dan gelen ikon-adı → `file://` URI eşlemesi
icons: HashMap<String, String>,
}
impl Slideshow {
pub fn new(desktop: &str) -> Self {
pub fn new(desktop: &str, icons: HashMap<String, String>, branding_slides: Vec<crate::branding::SlideItem>) -> Self {
Self {
slides: slides_for_desktop(desktop),
slides: slides_for_desktop(desktop, &branding_slides),
current: 0,
last_advance: Instant::now(),
icons,
}
}
@@ -321,34 +337,9 @@ impl Slideshow {
}
let slide = &self.slides[self.current];
let is_dark = ui.visuals().dark_mode;
// Eşleşen slayt simgeleri için görsel belirle
let image_source = match slide.icon {
"budgie" => Some(egui::include_image!("../../assets/budgie.png")),
"cinnamon" => Some(egui::include_image!("../../assets/cinnamon.png")),
"community" => Some(egui::include_image!("../../assets/community.png")),
"file_manager" => Some(egui::include_image!("../../assets/file_manager.png")),
"gnome" => Some(egui::include_image!("../../assets/gnome.png")),
"kde" => Some(egui::include_image!("../../assets/kde.png")),
"lxqt" => Some(egui::include_image!("../../assets/lxqt.png")),
"lumina" => Some(egui::include_image!("../../assets/lumina.png")),
"lxde" => Some(egui::include_image!("../../assets/lxde.png")),
"mate" => Some(egui::include_image!("../../assets/mate.png")),
"pisilogo" => {
if is_dark {
Some(egui::include_image!("../../assets/pisi-logo-dark.png"))
} else {
Some(egui::include_image!("../../assets/pisi-logo-light.png"))
}
}
"pisi" => Some(egui::include_image!("../../assets/pisi-software-center.png")),
"pisi_speed" => Some(egui::include_image!("../../assets/pisi_speed.png")),
"securety" => Some(egui::include_image!("../../assets/securety.png")),
"xfce" => Some(egui::include_image!("../../assets/xfce.png")),
"xfce_settings" => Some(egui::include_image!("../../assets/xfce_settings.png")),
_ => None,
};
// İkona karşılık gelen URI'yi branding haritasından al
let resolved_uri = self.icons.get(slide.icon).cloned();
egui::Frame::none()
.fill(crate::ui::theme::c_bg_widget())
@@ -358,9 +349,9 @@ impl Slideshow {
ui.set_min_height(180.0);
ui.vertical_centered(|ui| {
// Simge veya Görsel
if let Some(img) = image_source {
if let Some(uri) = resolved_uri {
ui.add(
egui::Image::new(img)
egui::Image::from_uri(uri)
.max_height(64.0)
.rounding(egui::Rounding::same(8.0)),
);