forked from pisilinux-rs/yali-rs
update
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
//! ISO ortamı ve donanım uyumluluk kontrolleri.
|
||||
//!
|
||||
//! WelcomeStep açılmadan önce çalışır.
|
||||
//! Her kontrol bir `CheckResult` döner: Pass / Warn / Fail.
|
||||
|
||||
use sysinfo::System;
|
||||
use std::path::Path;
|
||||
use rust_i18n::t;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// SONUÇ TİPİ
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum CheckStatus { Pass, Warn, Fail }
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CheckResult {
|
||||
pub name: String,
|
||||
pub status: CheckStatus,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl CheckResult {
|
||||
fn pass(name: impl Into<String>, msg: impl Into<String>) -> Self {
|
||||
Self { name: name.into(), status: CheckStatus::Pass, message: msg.into() }
|
||||
}
|
||||
fn warn(name: impl Into<String>, msg: impl Into<String>) -> Self {
|
||||
Self { name: name.into(), status: CheckStatus::Warn, message: msg.into() }
|
||||
}
|
||||
fn fail(name: impl Into<String>, msg: impl Into<String>) -> Self {
|
||||
Self { name: name.into(), status: CheckStatus::Fail, message: msg.into() }
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// TÜM KONTROLLERİ ÇALIŞTIR
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
pub fn run_all() -> Vec<CheckResult> {
|
||||
vec![
|
||||
check_ram(),
|
||||
check_disk_space(),
|
||||
check_internet(),
|
||||
check_boot_mode(),
|
||||
check_cpu_arch(),
|
||||
check_live_env(),
|
||||
check_nvme_support(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Tüm kontroller geçti mi? (Fail yoksa true)
|
||||
pub fn all_passed(results: &[CheckResult]) -> bool {
|
||||
results.iter().all(|r| r.status != CheckStatus::Fail)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// BİREYSEL KONTROLLER
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// RAM kontrolü. Minimum 1 GB, önerilen 2 GB.
|
||||
fn check_ram() -> CheckResult {
|
||||
let mut sys = System::new();
|
||||
sys.refresh_memory();
|
||||
let mb = sys.total_memory() / 1024 / 1024;
|
||||
|
||||
if mb < 1024 {
|
||||
CheckResult::fail(
|
||||
t!("check_ram"),
|
||||
t!("check_ram_fail", mb = mb)
|
||||
)
|
||||
} else if mb < 2048 {
|
||||
CheckResult::warn(
|
||||
t!("check_ram"),
|
||||
t!("check_ram_warn", mb = mb)
|
||||
)
|
||||
} else {
|
||||
CheckResult::pass(t!("check_ram"), t!("check_ram_pass", mb = mb))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
if max_gb < 10 {
|
||||
CheckResult::fail(
|
||||
t!("check_disk_space"),
|
||||
t!("check_disk_fail", size = max_gb),
|
||||
)
|
||||
} else if max_gb < 20 {
|
||||
CheckResult::warn(
|
||||
t!("check_disk_space"),
|
||||
t!("check_disk_warn", size = max_gb),
|
||||
)
|
||||
} else {
|
||||
CheckResult::pass(t!("check_disk_space"), t!("check_disk_pass", size = max_gb))
|
||||
}
|
||||
}
|
||||
|
||||
/// İnternet bağlantısı kontrolü (isteğe bağlı).
|
||||
fn check_internet() -> CheckResult {
|
||||
// DNS çözümlemesi ile basit kontrol
|
||||
let connected = std::net::TcpStream::connect_timeout(
|
||||
&"8.8.8.8:53".parse().unwrap(),
|
||||
std::time::Duration::from_secs(3),
|
||||
).is_ok();
|
||||
|
||||
if connected {
|
||||
CheckResult::pass(t!("check_internet"), t!("check_net_pass"))
|
||||
} else {
|
||||
CheckResult::warn(
|
||||
t!("check_internet"),
|
||||
t!("check_net_warn"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// UEFI mi BIOS mu?
|
||||
fn check_boot_mode() -> CheckResult {
|
||||
if Path::new("/sys/firmware/efi").exists() {
|
||||
CheckResult::pass(t!("check_boot_mode"), t!("check_uefi_pass"))
|
||||
} else {
|
||||
CheckResult::pass(t!("check_boot_mode"), t!("check_bios_pass"))
|
||||
}
|
||||
}
|
||||
|
||||
/// CPU mimarisi kontrolü (yalnızca x86_64 destekleniyor).
|
||||
fn check_cpu_arch() -> CheckResult {
|
||||
let arch = std::env::consts::ARCH;
|
||||
if arch == "x86_64" {
|
||||
// CPU özelliklerini de kontrol et
|
||||
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
|
||||
let has_sse2 = cpuinfo.contains("sse2");
|
||||
if has_sse2 {
|
||||
CheckResult::pass(t!("check_cpu"), t!("check_cpu_pass"))
|
||||
} else {
|
||||
CheckResult::warn(t!("check_cpu"), t!("check_cpu_warn"))
|
||||
}
|
||||
} else {
|
||||
CheckResult::fail(
|
||||
t!("check_cpu"),
|
||||
t!("check_cpu_fail", arch = arch)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Canlı sistem ortamı kontrolü.
|
||||
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"),
|
||||
)
|
||||
} else {
|
||||
CheckResult::pass(
|
||||
t!("check_live_sys"),
|
||||
t!("check_live_pass"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// NVMe ve diğer modern disk arayüzlerinin çekirdek desteğini kontrol eder.
|
||||
fn check_nvme_support() -> CheckResult {
|
||||
// /dev/nvme* cihazları var mı?
|
||||
let has_nvme_dev = std::fs::read_dir("/dev")
|
||||
.map(|dir| dir.flatten().any(|e| e.file_name().to_string_lossy().starts_with("nvme")))
|
||||
.unwrap_or(false);
|
||||
|
||||
// Çekirdek modülü yüklü mü?
|
||||
let modules = std::fs::read_to_string("/proc/modules").unwrap_or_default();
|
||||
let nvme_loaded = modules.contains("nvme ");
|
||||
|
||||
if has_nvme_dev {
|
||||
CheckResult::pass(t!("check_nvme"), t!("check_nvme_pass1"))
|
||||
} else if nvme_loaded {
|
||||
CheckResult::pass(t!("check_nvme"), t!("check_nvme_pass2"))
|
||||
} else {
|
||||
// NVMe yoksa bu bir sorun değil; SATA/virtio kullanılıyor olabilir
|
||||
CheckResult::pass(t!("check_nvme"), t!("check_nvme_pass3"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
//! Otomatik kurulum (answer file) desteği.
|
||||
//!
|
||||
//! 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;
|
||||
|
||||
pub mod checker;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// ANSWER FILE YAPISI
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, 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() }
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// DOĞRULAMA
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
#[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> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn validate(af: &AnswerFile) -> Result<(), AnswerError> {
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
|
||||
// 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) {
|
||||
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();
|
||||
|
||||
// Locale'den dil kodunu çıkar: "tr_TR.UTF-8" → "tr"
|
||||
state.language = af.install.locale
|
||||
.split('_')
|
||||
.next()
|
||||
.unwrap_or("tr")
|
||||
.to_string();
|
||||
|
||||
// Bölümleme planını hesapla
|
||||
let is_uefi = Path::new("/sys/firmware/efi").exists();
|
||||
let mut ps = PartitionStep::default();
|
||||
ps.on_enter(state);
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
|
||||
/// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur.
|
||||
///
|
||||
/// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1`
|
||||
/// - NVMe: `/dev/nvme0n1` + 1 → `/dev/nvme0n1p1`
|
||||
/// - MMC: `/dev/mmcblk0` + 1 → `/dev/mmcblk0p1`
|
||||
pub fn part_path(disk: &str, num: u32) -> String {
|
||||
let needs_p = disk.contains("nvme") || disk.contains("mmcblk") || disk.contains("loop");
|
||||
if needs_p {
|
||||
format!("{}p{}", disk, num)
|
||||
} else {
|
||||
format!("{}{}", disk, num)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Calamares'in "ViewStep" yapısına benzer bir Trait.
|
||||
pub trait InstallerStep {
|
||||
fn name(&self) -> String;
|
||||
fn on_enter(&mut self, _state: &mut GlobalState) {}
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState);
|
||||
fn is_complete(&self, state: &GlobalState) -> bool;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// VERİ YAPILARI
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DiskInfo {
|
||||
pub name: String, // ör. "/dev/sda"
|
||||
pub model: String,
|
||||
pub size_gb: u64,
|
||||
pub size_bytes: u64,
|
||||
}
|
||||
|
||||
/// Otomatik bölümleme planı.
|
||||
/// Disk seçimi yapılınca hesaplanır; Özet ekranında gösterilir.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PartitionPlan {
|
||||
pub disk: String,
|
||||
pub table_type: PartitionTableType, // GPT veya MBR
|
||||
pub efi_mb: u64, // 0 → BIOS, >0 → UEFI
|
||||
pub swap_mb: u64,
|
||||
pub root_mb: u64, // kalan alan
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum PartitionTableType {
|
||||
#[default]
|
||||
Gpt,
|
||||
Mbr,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PartitionTableType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PartitionTableType::Gpt => write!(f, "GPT"),
|
||||
PartitionTableType::Mbr => write!(f, "MBR"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Manuel bölümleme için desteklenen dosya sistemleri.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum FsType {
|
||||
Ext4,
|
||||
Btrfs,
|
||||
Xfs,
|
||||
Fat32, // EFI bölümü için
|
||||
Swap,
|
||||
}
|
||||
|
||||
impl FsType {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
FsType::Ext4 => "ext4",
|
||||
FsType::Btrfs => "btrfs",
|
||||
FsType::Xfs => "xfs",
|
||||
FsType::Fat32 => "FAT32 (EFI)",
|
||||
FsType::Swap => "swap",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FsType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.label())
|
||||
}
|
||||
}
|
||||
|
||||
/// Kullanıcının manuel olarak tanımladığı tek bir bölüm.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CustomPartition {
|
||||
/// Bölüm aygit adı; oluşturulacak (ör. "/dev/sda1")
|
||||
pub device: String,
|
||||
/// Boyut MB cinsinden; 0 = "kalan alan"
|
||||
pub size_mb: u64,
|
||||
/// Dosya sistemi
|
||||
pub fstype: FsType,
|
||||
/// Bağlama noktası; swap için "swap"
|
||||
pub mountpoint: String,
|
||||
}
|
||||
|
||||
/// Kurulum boyunca toplanan tüm veriler.
|
||||
pub struct GlobalState {
|
||||
pub language: String,
|
||||
pub timezone: String,
|
||||
pub selected_disk: Option<String>,
|
||||
pub erase_disk: bool,
|
||||
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 keyboard_layout: String,
|
||||
pub keyboard_variant: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub password_confirm: String,
|
||||
pub hostname: String,
|
||||
pub current_step: usize,
|
||||
pub available_disks: Vec<DiskInfo>,
|
||||
pub install_progress: f32,
|
||||
pub install_log: Vec<String>,
|
||||
pub demo_mode: bool,
|
||||
pub is_dark: bool,
|
||||
}
|
||||
|
||||
impl Default for GlobalState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: "tr".to_string(),
|
||||
timezone: "Europe/Istanbul".to_string(),
|
||||
selected_disk: None,
|
||||
erase_disk: true,
|
||||
partition_plan: None,
|
||||
custom_partitions: Vec::new(),
|
||||
keyboard_layout: "tr".to_string(),
|
||||
keyboard_variant: String::new(),
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
password_confirm: String::new(),
|
||||
hostname: String::new(),
|
||||
current_step: 0,
|
||||
available_disks: Vec::new(),
|
||||
install_progress: 0.0,
|
||||
install_log: Vec::new(),
|
||||
demo_mode: false,
|
||||
is_dark: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// WELCOME STEP (sistem gereksinimleri + dil seçimi)
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
use crate::autoinstall::checker::{CheckResult, CheckStatus, run_all, all_passed};
|
||||
|
||||
pub struct WelcomeStep {
|
||||
checks: Vec<CheckResult>,
|
||||
checked: bool,
|
||||
last_language: String,
|
||||
}
|
||||
|
||||
impl Default for WelcomeStep {
|
||||
fn default() -> Self {
|
||||
Self { checks: Vec::new(), checked: false, last_language: String::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallerStep for WelcomeStep {
|
||||
fn name(&self) -> String { t!("welcome").to_string() }
|
||||
|
||||
fn on_enter(&mut self, state: &mut GlobalState) {
|
||||
if !self.checked || self.last_language != state.language {
|
||||
rust_i18n::set_locale(&state.language);
|
||||
self.checks = run_all();
|
||||
self.checked = true;
|
||||
self.last_language = state.language.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
use crate::ui::theme;
|
||||
|
||||
if self.last_language != state.language {
|
||||
rust_i18n::set_locale(&state.language);
|
||||
self.checks = run_all();
|
||||
self.last_language = state.language.clone();
|
||||
}
|
||||
|
||||
theme::section_heading(ui, &t!("welcome_title"));
|
||||
ui.label(
|
||||
egui::RichText::new(t!("welcome_description"))
|
||||
.color(theme::c_text_dim())
|
||||
.size(13.0),
|
||||
);
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Dil seçimi (Açılır Liste / ComboBox)
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(t!("select_language"));
|
||||
let current_lang_label = if state.language == "tr" { "🇹🇷 Türkçe" } else { "🇺🇸 English" };
|
||||
egui::ComboBox::from_id_source("language_select")
|
||||
.selected_text(current_lang_label)
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut state.language, "tr".to_string(), "🇹🇷 Türkçe");
|
||||
ui.selectable_value(&mut state.language, "en".to_string(), "🇺🇸 English");
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(14.0);
|
||||
ui.separator();
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(t!("system_checks_title"))
|
||||
.size(15.0)
|
||||
.strong()
|
||||
.color(theme::c_text()),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Kontrol tablosu
|
||||
egui::Grid::new("checks_grid")
|
||||
.num_columns(3)
|
||||
.spacing([10.0, 6.0])
|
||||
.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()),
|
||||
CheckStatus::Fail => ("❌", theme::c_error()),
|
||||
};
|
||||
ui.colored_label(color, icon);
|
||||
ui.strong(&check.name);
|
||||
ui.label(
|
||||
egui::RichText::new(&check.message)
|
||||
.size(12.0)
|
||||
.color(theme::c_text_dim()),
|
||||
);
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
||||
// Hata varsa uyarı
|
||||
ui.add_space(10.0);
|
||||
let has_fail = self.checks.iter().any(|c| c.status == CheckStatus::Fail);
|
||||
if has_fail {
|
||||
theme::error_box(ui, &t!("check_fail_warning"));
|
||||
} else if self.checks.iter().any(|c| c.status == CheckStatus::Warn) {
|
||||
theme::warning_box(ui, &t!("check_warn_notice"));
|
||||
} else if !self.checks.is_empty() {
|
||||
theme::success_box(ui, &t!("check_all_pass"));
|
||||
}
|
||||
|
||||
// Yeniden tara butonu
|
||||
ui.add_space(8.0);
|
||||
if ui.add(theme::secondary_button(&t!("recheck"))).clicked() {
|
||||
self.checked = false;
|
||||
self.on_enter(state);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// LOCATION STEP
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
pub struct LocationStep {
|
||||
search_query: String,
|
||||
timezones: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for LocationStep {
|
||||
fn default() -> Self {
|
||||
let tz_list = vec![
|
||||
"Africa/Cairo", "America/Argentina/Buenos_Aires", "America/New_York",
|
||||
"America/Sao_Paulo", "Asia/Baku", "Asia/Dubai", "Asia/Istanbul",
|
||||
"Asia/Tokyo", "Australia/Sydney", "Europe/Berlin", "Europe/Istanbul",
|
||||
"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 }
|
||||
}
|
||||
}
|
||||
|
||||
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 is_complete(&self, state: &GlobalState) -> bool { !state.timezone.is_empty() }
|
||||
}
|
||||
|
||||
+1313
File diff suppressed because it is too large
Load Diff
+405
@@ -0,0 +1,405 @@
|
||||
mod installer;
|
||||
mod steps;
|
||||
mod jobs;
|
||||
mod ui;
|
||||
mod autoinstall;
|
||||
|
||||
use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep};
|
||||
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep};
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
|
||||
rust_i18n::i18n!("locales");
|
||||
|
||||
fn main() -> eframe::Result<()> {
|
||||
// ── CLI: otomatik kurulum modu ───────────────────────
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let demo_mode = args.contains(&"--demo".to_string());
|
||||
|
||||
if let Some(pos) = args.iter().position(|a| a == "--auto-install") {
|
||||
if let Some(path) = args.get(pos + 1) {
|
||||
return run_auto_install(path, demo_mode);
|
||||
} else {
|
||||
eprintln!("Hata: --auto-install <dosya.toml> şeklinde kullanın.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run_gui(demo_mode)
|
||||
}
|
||||
|
||||
/// Grafik arayüzü başlatır (normal mod).
|
||||
fn run_gui(demo_mode: bool) -> eframe::Result<()> {
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([1100.0, 760.0])
|
||||
.with_min_inner_size([900.0, 650.0])
|
||||
.with_title(t!("installer").to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
eframe::run_native(
|
||||
&t!("installer").to_string(),
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
// Görsel yükleyicileri etkinleştir (PNG desteği)
|
||||
egui_extras::install_image_loaders(&cc.egui_ctx);
|
||||
|
||||
ui::theme::apply(&cc.egui_ctx, true);
|
||||
let mut app = YaliApp::default();
|
||||
app.state.demo_mode = demo_mode;
|
||||
Box::new(app)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Answer file ile tam otomatik, UI-siz kurulum.
|
||||
fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
|
||||
use autoinstall::{load, apply_to_state};
|
||||
use std::path::Path;
|
||||
|
||||
println!("=== Yali-RS Otomatik Kurulum ===");
|
||||
println!("{}: {}", t!("answer_file"), path);
|
||||
|
||||
// Dosyayı yükle ve doğrula
|
||||
let af = match load(Path::new(path)) {
|
||||
Ok(af) => af,
|
||||
Err(e) => {
|
||||
eprintln!("{}: {}", t!("error"), e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
println!("{}: {}", t!("disk"), af.install.disk);
|
||||
println!("{}: {}", t!("timezone"), af.install.timezone);
|
||||
println!("{}: {}", t!("username"), af.user.username);
|
||||
println!("{}: {}", t!("hostname"), af.user.hostname);
|
||||
|
||||
// Sistem kontrollerini çalıştır
|
||||
println!("\n--- {} ---", t!("system_checks"));
|
||||
let checks = autoinstall::checker::run_all();
|
||||
for c in &checks {
|
||||
let sym = match c.status {
|
||||
autoinstall::checker::CheckStatus::Pass => "✓",
|
||||
autoinstall::checker::CheckStatus::Warn => "⚠",
|
||||
autoinstall::checker::CheckStatus::Fail => "✗",
|
||||
};
|
||||
println!("[{}] {}: {}", sym, c.name, c.message);
|
||||
}
|
||||
|
||||
if !autoinstall::checker::all_passed(&checks) {
|
||||
eprintln!("{}: {}", t!("error"), "Critical check error. Installation stopped.");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
// State'i hazırla
|
||||
let mut state = GlobalState::default();
|
||||
state.demo_mode = demo_mode;
|
||||
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));
|
||||
std::process::exit(3);
|
||||
}
|
||||
|
||||
println!("\n--- {} ---", t!("installation_started"));
|
||||
|
||||
// JobQueue'yu oluştur ve tokio'da çalıştır
|
||||
let locale = af.install.locale.clone();
|
||||
let timezone = af.install.timezone.clone();
|
||||
let username = af.user.username.clone();
|
||||
let password = af.user.password.clone();
|
||||
let hostname = af.user.hostname.clone();
|
||||
let kb_layout = af.install.keyboard.clone();
|
||||
let kb_variant = af.install.keyboard_variant.clone();
|
||||
let source = af.install.source.clone();
|
||||
let mount = af.install.mount.clone();
|
||||
let plan = state.partition_plan.unwrap();
|
||||
|
||||
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
|
||||
rt.block_on(async {
|
||||
use std::sync::mpsc;
|
||||
use jobs::{InstallMessage, UiSender};
|
||||
|
||||
// CLI modunda egui Context yok; basit bir mpsc kanal kullanıyoruz.
|
||||
// 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);
|
||||
|
||||
let plan_c = plan.clone();
|
||||
let locale_c = locale.clone();
|
||||
let timezone_c = timezone.clone();
|
||||
let username_c = username.clone();
|
||||
let password_c = password.clone();
|
||||
let hostname_c = hostname.clone();
|
||||
let kb_layout_c = kb_layout.clone();
|
||||
let kb_variant_c= kb_variant.clone();
|
||||
let source_c = source.clone();
|
||||
let mount_c = mount.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
|
||||
let mut queue = jobs::build_full_job_queue(
|
||||
plan_c,
|
||||
&mount_c,
|
||||
&source_c,
|
||||
&locale_c,
|
||||
&timezone_c,
|
||||
&username_c,
|
||||
&password_c,
|
||||
&hostname_c,
|
||||
&kb_layout_c,
|
||||
&kb_variant_c,
|
||||
demo_mode,
|
||||
);
|
||||
rt.block_on(queue.run_all(ui_sender));
|
||||
});
|
||||
|
||||
// Kanalı oku ve terminale yaz
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(InstallMessage::Log(line)) => println!(" {}", line),
|
||||
Ok(InstallMessage::Progress(p)) => {
|
||||
print!("\r {}: [{:50}] {:.0}%", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
}
|
||||
Ok(InstallMessage::Done) => {
|
||||
println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description"));
|
||||
break;
|
||||
}
|
||||
Ok(InstallMessage::Error(e)) => {
|
||||
eprintln!("\n{}: {}", t!("error"), e);
|
||||
std::process::exit(4);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// eframe::Result döndürmek için Ok
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct YaliApp {
|
||||
state: GlobalState,
|
||||
steps: Vec<Box<dyn InstallerStep>>,
|
||||
last_step: usize,
|
||||
}
|
||||
|
||||
impl Default for YaliApp {
|
||||
fn default() -> Self {
|
||||
let mut app = Self {
|
||||
state: GlobalState::default(),
|
||||
steps: vec![
|
||||
Box::new(WelcomeStep::default()),
|
||||
Box::new(LocationStep::default()),
|
||||
Box::new(KeyboardStep::default()),
|
||||
Box::new(PartitionStep::default()),
|
||||
Box::new(UsersStep::default()),
|
||||
Box::new(SummaryStep),
|
||||
Box::new(ExecutionStep::default()),
|
||||
Box::new(FinishStep),
|
||||
],
|
||||
last_step: usize::MAX,
|
||||
};
|
||||
app.steps[0].on_enter(&mut app.state);
|
||||
app.last_step = 0;
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for YaliApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
rust_i18n::set_locale(&self.state.language);
|
||||
|
||||
let cur = self.state.current_step;
|
||||
let n_steps = self.steps.len();
|
||||
let is_exec = cur == n_steps - 2;
|
||||
let is_finish = cur == n_steps - 1;
|
||||
|
||||
// Adım değişmişse on_enter çağır
|
||||
if cur != self.last_step {
|
||||
if let Some(step) = self.steps.get_mut(cur) {
|
||||
step.on_enter(&mut self.state);
|
||||
}
|
||||
self.last_step = cur;
|
||||
}
|
||||
|
||||
// ── Sol panel: adım listesi ──────────────────────────
|
||||
egui::SidePanel::left("sidebar")
|
||||
.exact_width(195.0)
|
||||
.frame(egui::Frame::none().fill(ui::theme::c_sidebar()))
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(20.0);
|
||||
|
||||
// PisiLinux logo
|
||||
ui.vertical_centered(|ui| {
|
||||
let logo = if self.state.is_dark {
|
||||
egui::include_image!("../assets/pisi-logo-dark.png")
|
||||
} else {
|
||||
egui::include_image!("../assets/pisi-logo-light.png")
|
||||
};
|
||||
ui.add(
|
||||
egui::Image::new(logo)
|
||||
.max_width(120.0)
|
||||
.rounding(egui::Rounding::same(8.0)),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new(t!("os_name"))
|
||||
.size(11.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(11.0)
|
||||
.color(ui::theme::c_text_dim()),
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
ui.separator();
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Adım listesi
|
||||
for (i, step) in self.steps.iter().enumerate() {
|
||||
let label = format!("{}. {}", i + 1, step.name());
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(if i == cur {
|
||||
egui::Color32::from_rgba_unmultiplied(0x21, 0x96, 0xF3, 30)
|
||||
} else {
|
||||
egui::Color32::TRANSPARENT
|
||||
})
|
||||
.rounding(egui::Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::symmetric(10.0, 5.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(175.0);
|
||||
if i == cur {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(ui::theme::c_accent(), "▶");
|
||||
ui.colored_label(ui::theme::c_text(), label);
|
||||
});
|
||||
} else if i < cur {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(ui::theme::c_success(), "✓");
|
||||
ui.colored_label(ui::theme::c_text_dim(), label);
|
||||
});
|
||||
} else {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(80, 80, 100),
|
||||
format!(" {}", label),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(2.0);
|
||||
}
|
||||
|
||||
// Alt kısım: tema değiştirme + sürüm bilgisi
|
||||
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),
|
||||
"Yali-RS v0.2.0",
|
||||
);
|
||||
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(
|
||||
egui::Button::new(
|
||||
egui::RichText::new(format!("{} {}", theme_icon, theme_label))
|
||||
.size(12.0)
|
||||
)
|
||||
.rounding(egui::Rounding::same(6.0))
|
||||
.min_size(egui::Vec2::new(100.0, 28.0))
|
||||
).clicked() {
|
||||
self.state.is_dark = !self.state.is_dark;
|
||||
ui::theme::set_theme_mode(self.state.is_dark);
|
||||
ui::theme::apply(ctx, self.state.is_dark);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Alt panel: navigasyon ────────────────────────────
|
||||
egui::TopBottomPanel::bottom("footer")
|
||||
.frame(egui::Frame::none()
|
||||
.fill(ui::theme::c_bg_panel())
|
||||
.stroke(egui::Stroke::new(1.0, ui::theme::c_border())))
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// İ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);
|
||||
}
|
||||
}
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(8.0);
|
||||
|
||||
let is_valid = self.steps
|
||||
.get(cur)
|
||||
.map(|s| s.is_complete(&self.state))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_finish {
|
||||
// İleri / Tamamlandı butonu
|
||||
if !is_exec || is_valid {
|
||||
let next_label = if is_exec {
|
||||
t!("finish_next")
|
||||
} else {
|
||||
t!("next")
|
||||
};
|
||||
let btn = if is_valid {
|
||||
ui::theme::primary_button(&next_label)
|
||||
} else {
|
||||
// Devre dışı görünüm için secondary kullan
|
||||
ui::theme::secondary_button(&next_label)
|
||||
};
|
||||
if ui.add_enabled(is_valid, btn).clicked() {
|
||||
self.state.current_step += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
self.state.current_step -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
});
|
||||
|
||||
// ── Merkez panel ─────────────────────────────────────
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::none()
|
||||
.fill(ui::theme::c_bg_dark())
|
||||
.inner_margin(egui::Margin::same(20.0)))
|
||||
.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.id_source("main_scroll")
|
||||
.show(ui, |ui| {
|
||||
if let Some(step) = self.steps.get_mut(cur) {
|
||||
step.show(ui, &mut self.state);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
//! Kurulumun gerçek olarak yürütüldüğü ekran.
|
||||
//!
|
||||
//! Sol yarı : Slayt gösterisi (Slideshow)
|
||||
//! Sağ yarı : İlerleme çubuğu + renkli log terminali
|
||||
//!
|
||||
//! Hata olursa tüm panel ErrorScreen ile kaplanır.
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
|
||||
use crate::installer::{GlobalState, InstallerStep};
|
||||
use crate::jobs::{self, InstallMessage, UiSender};
|
||||
use crate::ui::{ErrorScreen, Slideshow};
|
||||
use crate::ui::theme;
|
||||
|
||||
pub struct ExecutionStep {
|
||||
rx: Option<mpsc::Receiver<InstallMessage>>,
|
||||
tx: Option<mpsc::Sender<InstallMessage>>,
|
||||
finished: bool,
|
||||
error_screen: Option<ErrorScreen>,
|
||||
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() }
|
||||
|
||||
fn on_enter(&mut self, state: &mut GlobalState) {
|
||||
let (tx, rx) = mpsc::channel::<InstallMessage>();
|
||||
self.rx = Some(rx);
|
||||
self.tx = Some(tx);
|
||||
self.finished = false;
|
||||
self.error_screen = None;
|
||||
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_else(|_| "default".to_string())
|
||||
.to_lowercase();
|
||||
self.slideshow = Some(Slideshow::new(&desktop));
|
||||
|
||||
state.install_progress = 0.0;
|
||||
state.install_log.clear();
|
||||
state.install_log.push(t!("install_starting").to_string());
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
// ── İş parçacığını ilk show()'da başlat ───────────────
|
||||
if let Some(tx) = self.tx.take() {
|
||||
let timezone = state.timezone.clone();
|
||||
let language = state.language.clone();
|
||||
let plan = state.partition_plan.clone();
|
||||
let username = state.username.clone();
|
||||
let password = state.password.clone();
|
||||
let hostname = state.hostname.clone();
|
||||
let kb_layout = state.keyboard_layout.clone();
|
||||
let kb_variant = state.keyboard_variant.clone();
|
||||
let demo_mode = state.demo_mode;
|
||||
let ctx = ui.ctx().clone();
|
||||
let erase_disk = state.erase_disk;
|
||||
let custom_partitions = state.custom_partitions.clone();
|
||||
let selected_disk = state.selected_disk.clone();
|
||||
|
||||
let ui_sender = UiSender::new(tx, ctx);
|
||||
|
||||
thread::spawn(move || {
|
||||
let locale = if language == "tr" {
|
||||
"tr_TR.UTF-8".to_string()
|
||||
} else {
|
||||
"en_US.UTF-8".to_string()
|
||||
};
|
||||
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
|
||||
let src = jobs::detect_source_dir();
|
||||
|
||||
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,
|
||||
);
|
||||
rt.block_on(queue.run_all(ui_sender));
|
||||
} else {
|
||||
ui_sender.send(InstallMessage::Error(
|
||||
t!("error_partition_plan_not_found").to_string()
|
||||
));
|
||||
}
|
||||
} 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,
|
||||
);
|
||||
rt.block_on(queue.run_all(ui_sender));
|
||||
} else {
|
||||
ui_sender.send(InstallMessage::Error(
|
||||
t!("error_target_disk_not_selected").to_string()
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Kanaldaki mesajları tüket ──────────────────────────
|
||||
if let Some(ref rx) = self.rx {
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(InstallMessage::Log(line)) => {
|
||||
state.install_log.push(line);
|
||||
}
|
||||
Ok(InstallMessage::Progress(p)) => {
|
||||
state.install_progress = p;
|
||||
}
|
||||
Ok(InstallMessage::Done) => {
|
||||
state.install_progress = 1.0;
|
||||
self.finished = true;
|
||||
}
|
||||
Ok(InstallMessage::Error(e)) => {
|
||||
self.error_screen = Some(ErrorScreen::new(
|
||||
t!("install_failed"),
|
||||
e,
|
||||
state.install_log.clone(),
|
||||
));
|
||||
}
|
||||
Err(mpsc::TryRecvError::Empty) => break,
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
if !self.finished && self.error_screen.is_none() {
|
||||
self.error_screen = Some(ErrorScreen::new(
|
||||
t!("install_failed"),
|
||||
t!("error_thread_unexpectedly_terminated").to_string(),
|
||||
state.install_log.clone(),
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hata ekranı ────────────────────────────────────────
|
||||
if let Some(ref mut err) = self.error_screen {
|
||||
let mut retry = false;
|
||||
let mut quit = false;
|
||||
err.show(ui, || retry = true, || quit = true);
|
||||
if retry { self.on_enter(state); }
|
||||
if quit { ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); }
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Normal kurulum görünümü ────────────────────────────
|
||||
// Üst: Slayt gösterisi | Alt: ilerleme + log
|
||||
let available_w = ui.available_width();
|
||||
let available_h = ui.available_height();
|
||||
|
||||
ui.vertical(|ui| {
|
||||
// Üst kısım — slayt gösterisi
|
||||
let slide_h = (available_h * 0.45).max(280.0);
|
||||
ui.allocate_ui(egui::vec2(available_w, slide_h), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(8.0);
|
||||
theme::section_heading(ui, &t!("execution_title"));
|
||||
ui.label(
|
||||
egui::RichText::new(t!("execution_description"))
|
||||
.color(theme::c_text_dim())
|
||||
.size(13.0),
|
||||
);
|
||||
ui.add_space(16.0);
|
||||
|
||||
if let Some(ref mut ss) = self.slideshow {
|
||||
ss.show(ui);
|
||||
ui.add_space(10.0);
|
||||
// Manuel ileri/geri butonları
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button("‹").clicked() { ss.previous(); }
|
||||
if ui.small_button("›").clicked() { ss.advance(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
ui.separator();
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Alt kısım — ilerleme + log
|
||||
let log_h = available_h - slide_h - 50.0;
|
||||
ui.allocate_ui(egui::vec2(available_w, log_h), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// İlerleme çubuğu
|
||||
let running = !self.finished
|
||||
&& self.error_screen.is_none()
|
||||
&& state.install_progress > 0.0;
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(theme::c_bg_widget())
|
||||
.rounding(egui::Rounding::same(8.0))
|
||||
.inner_margin(egui::Margin::same(12.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(available_w - 4.0);
|
||||
|
||||
// Yüzde + adım etiketi
|
||||
let step_label = state.install_log.last()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
format!("{:.0}%", state.install_progress * 100.0)
|
||||
)
|
||||
.strong()
|
||||
.color(theme::c_accent()),
|
||||
);
|
||||
ui.label(
|
||||
egui::RichText::new(&step_label)
|
||||
.size(12.0)
|
||||
.color(theme::c_text_dim()),
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(4.0);
|
||||
ui.add(
|
||||
egui::ProgressBar::new(state.install_progress)
|
||||
.animate(running)
|
||||
.desired_height(10.0),
|
||||
);
|
||||
});
|
||||
|
||||
// Durum etiketi
|
||||
ui.add_space(8.0);
|
||||
if self.finished {
|
||||
theme::success_box(ui, &t!("install_done"));
|
||||
} else if running {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.colored_label(theme::c_text_dim(), t!("install_in_progress"));
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Log terminali
|
||||
let terminal_h = if self.finished { log_h - 60.0 } else { log_h - 80.0 };
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(terminal_h.max(100.0))
|
||||
.stick_to_bottom(true)
|
||||
.id_source("exec_log")
|
||||
.show(ui, |ui| {
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgb(12, 12, 20))
|
||||
.rounding(egui::Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(available_w - 8.0);
|
||||
for line in &state.install_log {
|
||||
let color = log_color(line);
|
||||
ui.colored_label(color, line);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Kurulum devam ediyorsa sürekli yeniden çiz
|
||||
if !self.finished && self.error_screen.is_none() {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete(&self, _state: &GlobalState) -> bool {
|
||||
self.finished && self.error_screen.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn log_color(line: &str) -> egui::Color32 {
|
||||
if line.starts_with('✗') {
|
||||
theme::c_error()
|
||||
} else if line.starts_with('✓') {
|
||||
theme::c_success()
|
||||
} else if line.starts_with('▶') {
|
||||
theme::c_accent()
|
||||
} else if line.starts_with('⚠') {
|
||||
theme::c_warning()
|
||||
} else {
|
||||
egui::Color32::from_rgb(185, 185, 205)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
use crate::installer::{GlobalState, InstallerStep};
|
||||
use crate::ui::theme;
|
||||
|
||||
pub struct FinishStep;
|
||||
|
||||
impl InstallerStep for FinishStep {
|
||||
fn name(&self) -> String { t!("finish").to_string() }
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, _state: &mut GlobalState) {
|
||||
ui.add_space(32.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
// Büyük onay simgesi
|
||||
ui.label(
|
||||
egui::RichText::new("✔")
|
||||
.size(72.0)
|
||||
.color(theme::c_success()),
|
||||
);
|
||||
ui.add_space(16.0);
|
||||
|
||||
ui.label(
|
||||
egui::RichText::new(t!("finish_title"))
|
||||
.size(24.0)
|
||||
.strong()
|
||||
.color(theme::c_text()),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new(t!("finish_description"))
|
||||
.size(14.0)
|
||||
.color(theme::c_text_dim()),
|
||||
);
|
||||
|
||||
ui.add_space(32.0);
|
||||
|
||||
// Yeniden Başlat — ana eylem butonu
|
||||
if ui.add(
|
||||
theme::primary_button(&t!("finish_restart"))
|
||||
.min_size(egui::vec2(200.0, 44.0)),
|
||||
).clicked() {
|
||||
let _ = std::process::Command::new("systemctl")
|
||||
.arg("reboot")
|
||||
.spawn();
|
||||
}
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Canlı masaüstüne dön — ikincil eylem
|
||||
if ui.add(
|
||||
theme::secondary_button(&t!("finish_live"))
|
||||
.min_size(egui::vec2(200.0, 36.0)),
|
||||
).clicked() {
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn is_complete(&self, _state: &GlobalState) -> bool { true }
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
use crate::installer::{GlobalState, InstallerStep};
|
||||
|
||||
/// Klavye düzeni ve varyantı seçimi.
|
||||
/// Gerçek uygulamada mevcut düzenler `localectl list-keymaps`
|
||||
/// veya `/usr/share/X11/xkb/rules/evdev.lst` dosyasından okunabilir.
|
||||
pub struct KeyboardStep {
|
||||
test_input: String,
|
||||
layouts: Vec<(&'static str, &'static str)>, // (kod, gösterim adı)
|
||||
}
|
||||
|
||||
impl Default for KeyboardStep {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
test_input: String::new(),
|
||||
layouts: vec![
|
||||
("tr", "Türkçe (Q)"),
|
||||
("tr:f", "Türkçe (F)"),
|
||||
("us", "İngilizce (US)"),
|
||||
("de", "Almanca"),
|
||||
("fr", "Fransızca"),
|
||||
("es", "İspanyolca"),
|
||||
("ru", "Rusça"),
|
||||
("ar", "Arapça"),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallerStep for KeyboardStep {
|
||||
fn name(&self) -> String {
|
||||
t!("keyboard").to_string()
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, state: &mut GlobalState) {
|
||||
// Varsayılan düzen sistemi diline göre seçilir
|
||||
if state.keyboard_layout.is_empty() {
|
||||
state.keyboard_layout = if state.language == "tr" {
|
||||
"tr".to_string()
|
||||
} else {
|
||||
"us".to_string()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
ui.heading(t!("keyboard_title"));
|
||||
ui.label(t!("keyboard_description"));
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
ui.columns(2, |cols| {
|
||||
// Sol: Düzen listesi
|
||||
cols[0].label(t!("keyboard_layout_label"));
|
||||
egui::ScrollArea::vertical()
|
||||
.id_source("kbd_layouts")
|
||||
.max_height(260.0)
|
||||
.show(&mut cols[0], |ui| {
|
||||
// &(code, display) pattern ile code: &str, display: &str olur
|
||||
for &(code, display) in &self.layouts {
|
||||
// tr:f kodunu layout+variant olarak ayır
|
||||
let (layout, variant) = if let Some(pos) = code.find(':') {
|
||||
(&code[..pos], &code[pos + 1..])
|
||||
} else {
|
||||
(code, "")
|
||||
};
|
||||
|
||||
let is_selected = state.keyboard_layout == layout
|
||||
&& state.keyboard_variant == variant;
|
||||
|
||||
if ui.selectable_label(is_selected, display).clicked() {
|
||||
state.keyboard_layout = layout.to_string();
|
||||
state.keyboard_variant = variant.to_string();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sağ: Test alanı
|
||||
cols[1].label(t!("keyboard_test_label"));
|
||||
cols[1].add_space(4.0);
|
||||
cols[1].add(
|
||||
egui::TextEdit::multiline(&mut self.test_input)
|
||||
.desired_rows(5)
|
||||
.hint_text(t!("keyboard_test_hint")),
|
||||
);
|
||||
|
||||
cols[1].add_space(8.0);
|
||||
cols[1].label(format!("{}: {}", t!("keyboard_selected"), {
|
||||
let variant_part = if state.keyboard_variant.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({})", state.keyboard_variant)
|
||||
};
|
||||
format!("{}{}", state.keyboard_layout, variant_part)
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
fn is_complete(&self, state: &GlobalState) -> bool {
|
||||
!state.keyboard_layout.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub mod keyboard;
|
||||
pub mod users;
|
||||
pub mod summary;
|
||||
pub mod execution;
|
||||
pub mod finish;
|
||||
pub mod partition;
|
||||
|
||||
pub use keyboard::KeyboardStep;
|
||||
pub use users::UsersStep;
|
||||
pub use summary::SummaryStep;
|
||||
pub use execution::ExecutionStep;
|
||||
pub use finish::FinishStep;
|
||||
pub use partition::PartitionStep;
|
||||
@@ -0,0 +1,585 @@
|
||||
//! Disk bölümleme adımı — Otomatik ve Manuel mod destekli.
|
||||
|
||||
use eframe::egui;
|
||||
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.
|
||||
|
||||
/// `/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.
|
||||
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();
|
||||
|
||||
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; }
|
||||
|
||||
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; }
|
||||
|
||||
let size_bytes: u64 = cols[1].trim().parse().unwrap_or(0);
|
||||
let model = if cols.len() >= 3 { cols[2].trim() } else { "" };
|
||||
|
||||
disks.push(crate::installer::DiskInfo {
|
||||
name: format!("/dev/{}", name_raw),
|
||||
model: model.to_string(),
|
||||
size_gb: size_bytes / 1_073_741_824,
|
||||
size_bytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// lsblk başarısız olduysa /sys/block fallback
|
||||
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; }
|
||||
|
||||
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;
|
||||
|
||||
let model_path = format!("/sys/block/{}/device/model", name);
|
||||
let model = std::fs::read_to_string(&model_path)
|
||||
.unwrap_or_default().trim().to_string();
|
||||
|
||||
disks.push(crate::installer::DiskInfo {
|
||||
name: format!("/dev/{}", name),
|
||||
model,
|
||||
size_gb: size_bytes / 1_073_741_824,
|
||||
size_bytes,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disks
|
||||
}
|
||||
|
||||
use crate::installer::{
|
||||
CustomPartition, DiskInfo, FsType, GlobalState, InstallerStep,
|
||||
PartitionPlan, PartitionTableType,
|
||||
};
|
||||
|
||||
const MIN_DISK_GB: u64 = 10;
|
||||
const EFI_MB: u64 = 512;
|
||||
const SWAP_MB_MIN: u64 = 512;
|
||||
const SWAP_MB_MAX: u64 = 8 * 1024;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Manuel bölümleme için yerel durum
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Default)]
|
||||
struct ManualState {
|
||||
/// "Bölüm Ekle" diyaloğu açık mı?
|
||||
adding: bool,
|
||||
/// Diyalog: seçili dosya sistemi
|
||||
add_fstype: usize, // FsType listesindeki indeks
|
||||
/// Diyalog: boyut alanı (MB, metin girişi)
|
||||
add_size_str: String,
|
||||
/// Diyalog: bağlama noktası
|
||||
add_mountpoint: String,
|
||||
/// Silinmek istenen bölüm indeksi (onay isteği)
|
||||
delete_idx: Option<usize>,
|
||||
/// Genel hata mesajı
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
const FS_OPTIONS: &[(&str, FsType)] = &[
|
||||
("ext4", FsType::Ext4),
|
||||
("btrfs", FsType::Btrfs),
|
||||
("xfs", FsType::Xfs),
|
||||
("FAT32 (EFI)", FsType::Fat32),
|
||||
("swap", FsType::Swap),
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// PartitionStep
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
pub struct PartitionStep {
|
||||
scanned: bool,
|
||||
is_uefi: bool,
|
||||
manual: ManualState,
|
||||
}
|
||||
|
||||
impl Default for PartitionStep {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scanned: false,
|
||||
is_uefi: std::path::Path::new("/sys/firmware/efi").exists(),
|
||||
manual: ManualState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RAM miktarına göre önerilen swap alanı (MB).
|
||||
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(SWAP_MB_MIN, SWAP_MB_MAX)
|
||||
}
|
||||
|
||||
/// Seçili disk için otomatik bölümleme planı hesapla.
|
||||
fn make_plan(disk: &DiskInfo, is_uefi: bool) -> Option<PartitionPlan> {
|
||||
if disk.size_gb < MIN_DISK_GB {
|
||||
return None;
|
||||
}
|
||||
let total_mb = disk.size_bytes / 1024 / 1024;
|
||||
let efi_mb = if is_uefi { EFI_MB } else { 0 };
|
||||
let swap_mb = recommended_swap_mb();
|
||||
let overhead = efi_mb + swap_mb + 512;
|
||||
let root_mb = total_mb.saturating_sub(overhead);
|
||||
if root_mb < 6144 { return None; }
|
||||
Some(PartitionPlan {
|
||||
disk: disk.name.clone(),
|
||||
table_type: if is_uefi { PartitionTableType::Gpt } else { PartitionTableType::Mbr },
|
||||
efi_mb,
|
||||
swap_mb,
|
||||
root_mb,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 == "/");
|
||||
let has_efi = parts.iter().any(|p| p.mountpoint == "/boot/efi" || p.fstype == FsType::Fat32);
|
||||
|
||||
if !has_root {
|
||||
return Some(t!("mp_no_root").to_string());
|
||||
}
|
||||
if is_uefi && !has_efi {
|
||||
return Some(t!("mp_no_efi").to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl InstallerStep for PartitionStep {
|
||||
fn name(&self) -> String { t!("partition").to_string() }
|
||||
|
||||
fn on_enter(&mut self, state: &mut GlobalState) {
|
||||
if !self.scanned {
|
||||
state.available_disks = list_block_devices();
|
||||
state.available_disks.retain(|d| d.size_gb >= 4);
|
||||
self.scanned = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
ui.heading(t!("partition_title"));
|
||||
ui.label(t!("partition_description"));
|
||||
ui.add_space(10.0);
|
||||
|
||||
if state.available_disks.is_empty() {
|
||||
ui.colored_label(egui::Color32::RED, t!("no_disks_found"));
|
||||
if ui.button(t!("rescan_disks")).clicked() {
|
||||
self.scanned = false;
|
||||
self.on_enter(state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Mod seçimi ─────────────────────────────────────────
|
||||
ui.horizontal(|ui| {
|
||||
ui.selectable_value(&mut state.erase_disk, true, t!("erase_disk_label"));
|
||||
ui.selectable_value(&mut state.erase_disk, false, t!("manual_partition_label"));
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
|
||||
if state.erase_disk {
|
||||
self.show_auto_mode(ui, state);
|
||||
} else {
|
||||
self.show_manual_mode(ui, state);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete(&self, state: &GlobalState) -> bool {
|
||||
if state.erase_disk {
|
||||
state.partition_plan.is_some()
|
||||
} else {
|
||||
!state.custom_partitions.is_empty()
|
||||
&& validate_manual(&state.custom_partitions, self.is_uefi).is_none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Otomatik mod
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
impl PartitionStep {
|
||||
fn show_auto_mode(&self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
ui.group(|ui| {
|
||||
ui.label(t!("select_disk_label"));
|
||||
ui.add_space(4.0);
|
||||
let disks = state.available_disks.clone();
|
||||
for disk in &disks {
|
||||
let too_small = disk.size_gb < MIN_DISK_GB;
|
||||
let label = format!("{} — {} GB ({})", disk.name, disk.size_gb, disk.model);
|
||||
ui.add_enabled_ui(!too_small, |ui| {
|
||||
let selected = state.selected_disk.as_deref() == Some(&disk.name);
|
||||
if ui.radio(selected, &label).clicked() {
|
||||
state.selected_disk = Some(disk.name.clone());
|
||||
state.partition_plan = make_plan(disk, self.is_uefi);
|
||||
}
|
||||
});
|
||||
if too_small {
|
||||
ui.small(t!("disk_min_required", err = t!("disk_too_small"), n = MIN_DISK_GB.to_string()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(ref plan) = state.partition_plan {
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
ui.add_space(8.0);
|
||||
ui.strong(t!("partition_plan_title"));
|
||||
ui.add_space(6.0);
|
||||
partition_bar(ui, plan);
|
||||
ui.add_space(8.0);
|
||||
|
||||
egui::Grid::new("plan_grid")
|
||||
.num_columns(3)
|
||||
.spacing([20.0, 6.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.strong(t!("plan_col_part"));
|
||||
ui.strong(t!("plan_col_fs"));
|
||||
ui.strong(t!("plan_col_size"));
|
||||
ui.end_row();
|
||||
|
||||
if plan.efi_mb > 0 {
|
||||
ui.label(format!("{}1 (EFI)", plan.disk));
|
||||
ui.label("FAT32");
|
||||
ui.label(format!("{} MB", plan.efi_mb));
|
||||
ui.end_row();
|
||||
}
|
||||
let swap_part = if plan.efi_mb > 0 { 2 } else { 1 };
|
||||
ui.label(format!("{}{} (swap)", plan.disk, swap_part));
|
||||
ui.label("swap");
|
||||
ui.label(format!("{} MB", plan.swap_mb));
|
||||
ui.end_row();
|
||||
|
||||
let root_part = swap_part + 1;
|
||||
ui.label(format!("{}{}{}", plan.disk, root_part, t!("plan_root_suffix")));
|
||||
ui.label("ext4");
|
||||
ui.label(format!("{:.1} GB", plan.root_mb as f64 / 1024.0));
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(format!("{}: {}", t!("partition_table_type"), plan.table_type));
|
||||
|
||||
ui.add_space(10.0);
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgba_unmultiplied(200, 60, 40, 25))
|
||||
.rounding(5.0)
|
||||
.inner_margin(egui::Margin::same(8.0))
|
||||
.show(ui, |ui| {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(210, 80, 60),
|
||||
format!("⚠ {}: {}", t!("erase_warning"), plan.disk),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Manuel mod
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
impl PartitionStep {
|
||||
fn show_manual_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
let accent = crate::ui::theme::c_accent();
|
||||
|
||||
// ── Disk seçimi (hedef disk) ────────────────────────────
|
||||
ui.group(|ui| {
|
||||
ui.label(t!("select_disk_label"));
|
||||
ui.add_space(4.0);
|
||||
let disks = state.available_disks.clone();
|
||||
for disk in &disks {
|
||||
let selected = state.selected_disk.as_deref() == Some(&disk.name);
|
||||
let label = format!("{} — {} GB ({})", disk.name, disk.size_gb, disk.model);
|
||||
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();
|
||||
self.manual.error = None;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
let disk = match &state.selected_disk {
|
||||
Some(d) => d.clone(),
|
||||
None => {
|
||||
ui.label(t!("mp_select_disk_first"));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Mevcut bölüm listesi tablosu ───────────────────────
|
||||
ui.strong(t!("mp_plan_title"));
|
||||
ui.add_space(4.0);
|
||||
|
||||
let mut delete_idx: Option<usize> = None;
|
||||
|
||||
egui::Grid::new("manual_parts_grid")
|
||||
.num_columns(5)
|
||||
.spacing([12.0, 6.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
// Başlık
|
||||
ui.strong(t!("mp_device"));
|
||||
ui.strong(t!("mp_size"));
|
||||
ui.strong(t!("mp_fstype"));
|
||||
ui.strong(t!("mp_mountpoint"));
|
||||
ui.strong(t!("mp_action"));
|
||||
ui.end_row();
|
||||
|
||||
for (i, part) in state.custom_partitions.iter().enumerate() {
|
||||
let size_str = if part.size_mb == 0 {
|
||||
t!("mp_size_remaining").to_string()
|
||||
} else if part.size_mb >= 1024 {
|
||||
format!("{:.1} GB", part.size_mb as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{} MB", part.size_mb)
|
||||
};
|
||||
|
||||
ui.label(&part.device);
|
||||
ui.label(&size_str);
|
||||
ui.label(part.fstype.label());
|
||||
ui.label(&part.mountpoint);
|
||||
if ui.small_button("🗑").on_hover_text(t!("mp_delete")).clicked() {
|
||||
delete_idx = Some(i);
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
|
||||
// Silme onayı
|
||||
if let Some(idx) = delete_idx {
|
||||
self.manual.delete_idx = Some(idx);
|
||||
}
|
||||
if let Some(idx) = self.manual.delete_idx {
|
||||
let dev = state.custom_partitions.get(idx)
|
||||
.map(|p| p.device.clone())
|
||||
.unwrap_or_default();
|
||||
egui::Window::new(t!("mp_delete_confirm_title"))
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.show(ui.ctx(), |ui| {
|
||||
ui.label(t!("mp_confirm_delete", dev = dev));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button(t!("mp_delete_confirm_yes")).clicked() {
|
||||
if idx < state.custom_partitions.len() {
|
||||
state.custom_partitions.remove(idx);
|
||||
}
|
||||
self.manual.delete_idx = None;
|
||||
}
|
||||
if ui.button(t!("cancel")).clicked() {
|
||||
self.manual.delete_idx = None;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// ── "Bölüm Ekle" butonu ────────────────────────────────
|
||||
let btn = egui::Button::new(format!("➕ {}", t!("mp_add")))
|
||||
.fill(accent.gamma_multiply(0.15));
|
||||
if ui.add(btn).clicked() {
|
||||
self.manual.adding = true;
|
||||
let next_num = state.custom_partitions.len() + 1;
|
||||
self.manual.add_size_str.clear();
|
||||
self.manual.add_mountpoint.clear();
|
||||
self.manual.add_fstype = 0;
|
||||
// Akıllı varsayılan bağlama noktası
|
||||
self.manual.add_mountpoint = match next_num {
|
||||
1 if self.is_uefi => "/boot/efi".to_string(),
|
||||
1 => "/".to_string(),
|
||||
2 if self.is_uefi => "swap".to_string(),
|
||||
2 => "swap".to_string(),
|
||||
3 => "/".to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
}
|
||||
|
||||
// ── "Bölüm Ekle" diyaloğu ──────────────────────────────
|
||||
if self.manual.adding {
|
||||
egui::Window::new(t!("mp_add"))
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
|
||||
.show(ui.ctx(), |ui| {
|
||||
egui::Grid::new("add_part_grid")
|
||||
.num_columns(2)
|
||||
.spacing([12.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
|
||||
// Dosya sistemi
|
||||
ui.label(t!("mp_fstype"));
|
||||
egui::ComboBox::from_id_source("fs_combo")
|
||||
.selected_text(FS_OPTIONS[self.manual.add_fstype].0)
|
||||
.show_ui(ui, |ui| {
|
||||
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
|
||||
ui.selectable_value(
|
||||
&mut self.manual.add_fstype, i, *label
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
|
||||
// Boyut
|
||||
ui.label(t!("mp_size_hint"));
|
||||
ui.text_edit_singleline(&mut self.manual.add_size_str);
|
||||
ui.end_row();
|
||||
|
||||
// Bağlama noktası
|
||||
ui.label(t!("mp_mountpoint"));
|
||||
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_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_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 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 {
|
||||
state.custom_partitions.push(CustomPartition {
|
||||
device,
|
||||
size_mb,
|
||||
fstype,
|
||||
mountpoint: mp,
|
||||
});
|
||||
self.manual.error = None;
|
||||
self.manual.adding = false;
|
||||
}
|
||||
}
|
||||
if ui.button(t!("cancel")).clicked() {
|
||||
self.manual.adding = 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) {
|
||||
ui.colored_label(egui::Color32::from_rgb(220, 160, 40), warn);
|
||||
} else {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(60, 180, 90),
|
||||
t!("mp_plan_valid"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Yardımcı: renkli bölüm şeridi
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
fn partition_bar(ui: &mut egui::Ui, plan: &PartitionPlan) {
|
||||
let total = (plan.efi_mb + plan.swap_mb + plan.root_mb) as f32;
|
||||
let bar_height = 28.0;
|
||||
let (rect, _) = ui.allocate_exact_size(
|
||||
egui::vec2(ui.available_width(), bar_height),
|
||||
egui::Sense::hover(),
|
||||
);
|
||||
let painter = ui.painter();
|
||||
|
||||
struct Seg<'a> { frac: f32, color: egui::Color32, label: &'a str }
|
||||
let mut segments: Vec<Seg> = Vec::new();
|
||||
|
||||
if plan.efi_mb > 0 {
|
||||
segments.push(Seg {
|
||||
frac: plan.efi_mb as f32 / total,
|
||||
color: egui::Color32::from_rgb(80, 160, 220),
|
||||
label: "EFI",
|
||||
});
|
||||
}
|
||||
segments.push(Seg {
|
||||
frac: plan.swap_mb as f32 / total,
|
||||
color: egui::Color32::from_rgb(220, 160, 40),
|
||||
label: "swap",
|
||||
});
|
||||
segments.push(Seg {
|
||||
frac: plan.root_mb as f32 / total,
|
||||
color: egui::Color32::from_rgb(80, 190, 100),
|
||||
label: "/",
|
||||
});
|
||||
|
||||
let mut x = rect.left();
|
||||
let rounding = egui::Rounding::same(4.0);
|
||||
for (i, seg) in segments.iter().enumerate() {
|
||||
let w = seg.frac * rect.width();
|
||||
let seg_rect = egui::Rect::from_min_size(
|
||||
egui::pos2(x, rect.top()),
|
||||
egui::vec2(w - 1.0, bar_height),
|
||||
);
|
||||
let seg_rounding = if i == 0 {
|
||||
egui::Rounding { nw: rounding.nw, sw: rounding.sw, ne: 0.0, se: 0.0 }
|
||||
} else if i == segments.len() - 1 {
|
||||
egui::Rounding { nw: 0.0, sw: 0.0, ne: rounding.ne, se: rounding.se }
|
||||
} else {
|
||||
egui::Rounding::ZERO
|
||||
};
|
||||
painter.rect_filled(seg_rect, seg_rounding, seg.color);
|
||||
painter.text(
|
||||
seg_rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
seg.label,
|
||||
egui::FontId::proportional(12.0),
|
||||
egui::Color32::WHITE,
|
||||
);
|
||||
x += w;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
use crate::installer::{GlobalState, InstallerStep};
|
||||
|
||||
/// Kurulumdan önce kullanıcının tüm seçimlerini gösteren özet ekranı.
|
||||
/// Calamares'in Summary modülüne karşılık gelir.
|
||||
pub struct SummaryStep;
|
||||
|
||||
impl InstallerStep for SummaryStep {
|
||||
fn name(&self) -> String {
|
||||
t!("summary").to_string()
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
ui.heading(t!("summary_title"));
|
||||
ui.label(t!("summary_description"));
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
|
||||
egui::Grid::new("summary_grid")
|
||||
.num_columns(2)
|
||||
.spacing([20.0, 8.0])
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
summary_row(ui, t!("summary_language"), &state.language);
|
||||
summary_row(ui, t!("summary_timezone"), &state.timezone);
|
||||
|
||||
let kb = if state.keyboard_variant.is_empty() {
|
||||
state.keyboard_layout.clone()
|
||||
} else {
|
||||
format!("{} ({})", state.keyboard_layout, state.keyboard_variant)
|
||||
};
|
||||
summary_row(ui, t!("summary_keyboard"), &kb);
|
||||
|
||||
let disk_str = match &state.selected_disk {
|
||||
Some(d) => {
|
||||
if state.erase_disk {
|
||||
format!("{} — {}", d, t!("summary_erase_mode"))
|
||||
} else {
|
||||
format!("{} — {}", d, t!("summary_manual_mode"))
|
||||
}
|
||||
}
|
||||
None => t!("summary_not_selected").to_string(),
|
||||
};
|
||||
summary_row(ui, t!("summary_disk"), &disk_str);
|
||||
|
||||
// Bölüm planı detayları
|
||||
if state.erase_disk {
|
||||
if let Some(ref plan) = state.partition_plan {
|
||||
let plan_str = if plan.efi_mb > 0 {
|
||||
format!("EFI {}MB | swap {}MB | / {:.1}GB ({})",
|
||||
plan.efi_mb, plan.swap_mb,
|
||||
plan.root_mb as f64 / 1024.0,
|
||||
plan.table_type)
|
||||
} else {
|
||||
format!("swap {}MB | / {:.1}GB ({})",
|
||||
plan.swap_mb,
|
||||
plan.root_mb as f64 / 1024.0,
|
||||
plan.table_type)
|
||||
};
|
||||
summary_row(ui, t!("summary_partition_plan"), &plan_str);
|
||||
}
|
||||
} else if !state.custom_partitions.is_empty() {
|
||||
let mut plan_lines = Vec::new();
|
||||
for part in &state.custom_partitions {
|
||||
let size_str = if part.size_mb == 0 {
|
||||
t!("mp_size_remaining").to_string()
|
||||
} else if part.size_mb >= 1024 {
|
||||
format!("{:.1} GB", part.size_mb as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{} MB", part.size_mb)
|
||||
};
|
||||
plan_lines.push(format!("{} ➔ {} ({}, {})", part.device, part.mountpoint, part.fstype, size_str));
|
||||
}
|
||||
summary_row(ui, t!("summary_custom_partitions"), &plan_lines.join("\n"));
|
||||
}
|
||||
|
||||
summary_row(ui, t!("summary_username"), &state.username);
|
||||
summary_row(ui, t!("summary_hostname"), &state.hostname);
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Uyarı kutusu
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgba_unmultiplied(200, 60, 40, 30))
|
||||
.rounding(6.0)
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(180, 60, 40),
|
||||
t!("summary_warning"),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn is_complete(&self, _state: &GlobalState) -> bool {
|
||||
// Özet her zaman geçerlidir; önceki adımlar bunu garantiler.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn summary_row(ui: &mut egui::Ui, label: impl Into<String>, value: &str) {
|
||||
ui.strong(label.into());
|
||||
ui.label(value);
|
||||
ui.end_row();
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
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.
|
||||
pub struct UsersStep {
|
||||
show_password: bool,
|
||||
show_password_confirm: bool,
|
||||
}
|
||||
|
||||
impl Default for UsersStep {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_password: false,
|
||||
show_password_confirm: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kullanıcı adının geçerliliğini kontrol eder.
|
||||
/// Linux kuralları: küçük harf veya alt çizgi ile başlamalı,
|
||||
/// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli.
|
||||
fn is_valid_username(s: &str) -> bool {
|
||||
if s.is_empty() || s.len() > 32 {
|
||||
return false;
|
||||
}
|
||||
let mut chars = s.chars();
|
||||
// İlk karakter küçük harf veya alt çizgi olmalı
|
||||
match chars.next() {
|
||||
Some(c) if c.is_ascii_lowercase() || c == '_' => {}
|
||||
_ => return false,
|
||||
}
|
||||
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
|
||||
}
|
||||
|
||||
/// Hostname kuralları: 1-63 karakter, yalnızca [a-zA-Z0-9-], tire ile başlayamaz/bitemez.
|
||||
fn is_valid_hostname(s: &str) -> bool {
|
||||
if s.is_empty() || s.len() > 63 {
|
||||
return false;
|
||||
}
|
||||
if s.starts_with('-') || s.ends_with('-') {
|
||||
return false;
|
||||
}
|
||||
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
|
||||
}
|
||||
|
||||
fn password_strength(p: &str) -> (String, egui::Color32) {
|
||||
let len = p.len();
|
||||
let has_upper = p.chars().any(|c| c.is_uppercase());
|
||||
let has_digit = p.chars().any(|c| c.is_ascii_digit());
|
||||
let has_special = p.chars().any(|c| "!@#$%^&*()_+-=[]{}|;':\",./<>?".contains(c));
|
||||
let score = (len >= 8) as u8
|
||||
+ (len >= 12) as u8
|
||||
+ has_upper as u8
|
||||
+ has_digit as u8
|
||||
+ has_special as u8;
|
||||
match score {
|
||||
0..=1 => (t!("pwd_very_weak").to_string(), egui::Color32::from_rgb(200, 60, 40)),
|
||||
2 => (t!("pwd_weak").to_string(), egui::Color32::from_rgb(220, 140, 40)),
|
||||
3 => (t!("pwd_medium").to_string(), egui::Color32::from_rgb(200, 180, 40)),
|
||||
4 => (t!("pwd_strong").to_string(), egui::Color32::from_rgb(80, 170, 80)),
|
||||
_ => (t!("pwd_very_strong").to_string(), egui::Color32::from_rgb(40, 140, 60)),
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallerStep for UsersStep {
|
||||
fn name(&self) -> String {
|
||||
t!("users").to_string()
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, state: &mut GlobalState) {
|
||||
// Hostname boşsa username'den öner
|
||||
if state.hostname.is_empty() && !state.username.is_empty() {
|
||||
state.hostname = format!("{}-pc", state.username);
|
||||
}
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
ui.heading(t!("users_title"));
|
||||
ui.label(t!("users_description"));
|
||||
ui.add_space(12.0);
|
||||
|
||||
egui::Grid::new("users_grid")
|
||||
.num_columns(2)
|
||||
.spacing([12.0, 10.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();
|
||||
|
||||
// Ş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 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(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();
|
||||
});
|
||||
|
||||
// Hata mesajları
|
||||
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"),
|
||||
);
|
||||
}
|
||||
if !state.hostname.is_empty() && !is_valid_hostname(&state.hostname) {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(200, 60, 40),
|
||||
t!("hostname_invalid"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_complete(&self, state: &GlobalState) -> bool {
|
||||
is_valid_username(&state.username)
|
||||
&& is_valid_hostname(&state.hostname)
|
||||
&& !state.password.is_empty()
|
||||
&& state.password == state.password_confirm
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//! Hata yakalama ve kullanıcıya bildirme ekranı.
|
||||
//!
|
||||
//! Kurulum sırasında kritik bir hata oluştuğunda
|
||||
//! bu ekran tüm merkez paneli kaplar.
|
||||
//! Log detayları açılıp kapanabilir; kullanıcı
|
||||
//! hata raporunu panoya kopyalayabilir.
|
||||
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
use crate::ui::theme;
|
||||
|
||||
pub struct ErrorScreen {
|
||||
pub title: String,
|
||||
pub message: String,
|
||||
pub log_lines: Vec<String>,
|
||||
log_expanded: bool,
|
||||
copied: bool,
|
||||
copied_timer: u8, // frame sayacı; 0 olunca "Kopyalandı" etiketi kaybolur
|
||||
}
|
||||
|
||||
impl ErrorScreen {
|
||||
pub fn new(title: impl Into<String>, message: impl Into<String>, log: Vec<String>) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
message: message.into(),
|
||||
log_lines: log,
|
||||
log_expanded: false,
|
||||
copied: false,
|
||||
copied_timer: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ekranı çizer. `on_retry` ve `on_quit` kapanışları buton basımlarında çağrılır.
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
on_retry: impl FnOnce(),
|
||||
on_quit: impl FnOnce(),
|
||||
) {
|
||||
// "Kopyalandı" zamanlayıcısını azalt
|
||||
if self.copied && self.copied_timer > 0 {
|
||||
self.copied_timer -= 1;
|
||||
if self.copied_timer == 0 {
|
||||
self.copied = false;
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
ui.add_space(24.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
// ── Büyük hata simgesi ─────────────────────────
|
||||
ui.label(
|
||||
egui::RichText::new("✗")
|
||||
.size(56.0)
|
||||
.color(theme::c_error()),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
|
||||
// ── Başlık ────────────────────────────────────
|
||||
ui.label(
|
||||
egui::RichText::new(&self.title)
|
||||
.size(22.0)
|
||||
.strong()
|
||||
.color(theme::c_text()),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
// ── Hata mesajı ───────────────────────────────
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgba_unmultiplied(0xF4, 0x43, 0x36, 18))
|
||||
.stroke(egui::Stroke::new(1.0, theme::c_error()))
|
||||
.rounding(egui::Rounding::same(8.0))
|
||||
.inner_margin(egui::Margin::same(12.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width().min(560.0));
|
||||
ui.colored_label(theme::c_error(), &self.message);
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
// ── Log bölümü (aç/kapat) ─────────────────────
|
||||
let log_label = if self.log_expanded {
|
||||
t!("error_hide_log")
|
||||
} else {
|
||||
t!("error_show_log")
|
||||
};
|
||||
|
||||
if ui.button(log_label).clicked() {
|
||||
self.log_expanded = !self.log_expanded;
|
||||
}
|
||||
|
||||
if self.log_expanded {
|
||||
ui.add_space(6.0);
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(180.0)
|
||||
.id_source("error_log")
|
||||
.show(ui, |ui| {
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgb(14, 14, 22))
|
||||
.rounding(egui::Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width().min(560.0));
|
||||
for line in &self.log_lines {
|
||||
let color = log_line_color(line);
|
||||
ui.colored_label(color, line);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Panoya kopyala
|
||||
let copy_label = if self.copied {
|
||||
t!("error_copied")
|
||||
} else {
|
||||
t!("error_copy_log")
|
||||
};
|
||||
|
||||
if ui.small_button(copy_label).clicked() && !self.copied {
|
||||
let full_log = self.log_lines.join("\n");
|
||||
ui.ctx().copy_text(full_log);
|
||||
self.copied = true;
|
||||
self.copied_timer = 120; // ~2 saniye (60fps varsayımı)
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(20.0);
|
||||
|
||||
// ── Eylem butonları ───────────────────────────
|
||||
ui.horizontal(|ui| {
|
||||
// Ortalamak için boş genişlik hesapla
|
||||
let btn_w = 140.0;
|
||||
let gap = 16.0;
|
||||
let total = btn_w * 2.0 + gap;
|
||||
let side = (ui.available_width() - total) / 2.0;
|
||||
ui.add_space(side.max(0.0));
|
||||
|
||||
if ui.add(theme::danger_button(&t!("error_quit"))).clicked() {
|
||||
on_quit();
|
||||
}
|
||||
|
||||
ui.add_space(gap);
|
||||
|
||||
if ui.add(theme::primary_button(&t!("error_retry"))).clicked() {
|
||||
on_retry();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn log_line_color(line: &str) -> egui::Color32 {
|
||||
if line.starts_with('✗') || line.to_lowercase().contains("hata") || line.to_lowercase().contains("error") {
|
||||
theme::c_error()
|
||||
} else if line.starts_with('✓') {
|
||||
theme::c_success()
|
||||
} else if line.starts_with('▶') {
|
||||
theme::c_accent()
|
||||
} else if line.starts_with('⚠') {
|
||||
theme::c_warning()
|
||||
} else {
|
||||
egui::Color32::from_rgb(180, 180, 200)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod theme;
|
||||
pub mod slideshow;
|
||||
pub mod error_screen;
|
||||
|
||||
pub use error_screen::ErrorScreen;
|
||||
pub use slideshow::Slideshow;
|
||||
@@ -0,0 +1,428 @@
|
||||
//! Kurulum sırasında gösterilen slayt gösterisi.
|
||||
//!
|
||||
//! Her slayt: başlık, açıklama metni ve isteğe bağlı bir simge karakteri.
|
||||
//! Slaytlar otomatik olarak 6 saniyede bir değişir.
|
||||
//! Masaüstü ortamına göre farklı slayt setleri yüklenebilir.
|
||||
|
||||
use eframe::egui;
|
||||
use std::time::{Duration, Instant};
|
||||
use rust_i18n::t;
|
||||
use std::borrow::Cow;
|
||||
|
||||
const SLIDE_INTERVAL: Duration = Duration::from_secs(6);
|
||||
|
||||
pub struct Slide {
|
||||
pub icon: &'static str,
|
||||
pub title: Cow<'static, str>,
|
||||
pub description: Cow<'static, str>,
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
fn default_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "pisilogo",
|
||||
title: t!("welcome_slide_title"),
|
||||
description: t!("welcome_slide_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "pisi",
|
||||
title: t!("package_manager_title"),
|
||||
description: t!("package_manager_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "community",
|
||||
title: t!("community_title"),
|
||||
description: t!("community_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "pisi_speed",
|
||||
title: t!("speed_title"),
|
||||
description: t!("speed_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "securety",
|
||||
title: t!("security_title"),
|
||||
description: t!("security_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "customization",
|
||||
title: t!("customization_title"),
|
||||
description: t!("customization_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn budgie_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "budgie",
|
||||
title: t!("budgie_title"),
|
||||
description: t!("budgie_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "budgie_applets",
|
||||
title: t!("budgie_applets_title"),
|
||||
description: t!("budgie_applets_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "budgie_raven",
|
||||
title: t!("budgie_raven_title"),
|
||||
description: t!("budgie_raven_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "budgie_gtk_theme",
|
||||
title: t!("budgie_gtk_theme_title"),
|
||||
description: t!("budgie_gtk_theme_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn cinnamon_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "cinnamon",
|
||||
title: t!("cinnamon_title"),
|
||||
description: t!("cinnamon_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "cinnamon_panel",
|
||||
title: t!("cinnamon_panel_title"),
|
||||
description: t!("cinnamon_panel_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "cinnamon_settings",
|
||||
title: t!("cinnamon_settings_title"),
|
||||
description: t!("cinnamon_settings_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "cinnamon_desklets",
|
||||
title: t!("cinnamon_desklets_title"),
|
||||
description: t!("cinnamon_desklets_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn gnome_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "gnome",
|
||||
title: t!("gnome_title"),
|
||||
description: t!("gnome_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "gnome_search",
|
||||
title: t!("gnome_search_title"),
|
||||
description: t!("gnome_search_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "gnome_extensions",
|
||||
title: t!("gnome_extensions_title"),
|
||||
description: t!("gnome_extensions_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn kde_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "kde",
|
||||
title: t!("kde_title"),
|
||||
description: t!("kde_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("dolphin_title"),
|
||||
description: t!("dolphin_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "kde_connect",
|
||||
title: t!("kde_connect_title"),
|
||||
description: t!("kde_connect_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "discover",
|
||||
title: t!("discover_title"),
|
||||
description: t!("discover_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn lxde_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "lxde",
|
||||
title: t!("lxde_title"),
|
||||
description: t!("lxde_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lxde_openbox_title",
|
||||
title: t!("lxde_openbox_title"),
|
||||
description: t!("lxde_openbox_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("lxde_pcmanfm_title"),
|
||||
description: t!("lxde_pcmanfm_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lxde_lxpanel_title",
|
||||
title: t!("lxde_lxpanel_title"),
|
||||
description: t!("lxde_lxpanel_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn lxqt_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "lxqt",
|
||||
title: t!("lxqt_title"),
|
||||
description: t!("lxqt_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lxqt_panel_title",
|
||||
title: t!("lxqt_panel_title"),
|
||||
description: t!("lxqt_panel_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("lxqt_pcmanfm_qt_title"),
|
||||
description: t!("lxqt_pcmanfm_qt_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn lumina_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "lumina",
|
||||
title: t!("lumina_title"),
|
||||
description: t!("lumina_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lumina_applications_title",
|
||||
title: t!("lumina_applications_title"),
|
||||
description: t!("lumina_applications_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lumina_theme_title",
|
||||
title: t!("lumina_theme_title"),
|
||||
description: t!("lumina_theme_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lumina_panel_title",
|
||||
title: t!("lumina_panel_title"),
|
||||
description: t!("lumina_panel_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn mate_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "mate",
|
||||
title: t!("mate_title"),
|
||||
description: t!("mate_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("mate_caja_title"),
|
||||
description: t!("mate_caja_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "mate_settings_title",
|
||||
title: t!("mate_settings_title"),
|
||||
description: t!("mate_settings_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "mate_applications_title",
|
||||
title: t!("mate_applications_title"),
|
||||
description: t!("mate_applications_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn xfce_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "xfce",
|
||||
title: t!("xfce_title"),
|
||||
description: t!("xfce_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "xfce_panel_title",
|
||||
title: t!("xfce_panel_title"),
|
||||
description: t!("xfce_panel_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("xfce_thunar_title"),
|
||||
description: t!("xfce_thunar_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "xfce_settings",
|
||||
title: t!("xfce_settings_title"),
|
||||
description: t!("xfce_settings_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
// ─── Slayt gösterisi widget'ı ────────────────────────────────────
|
||||
|
||||
pub struct Slideshow {
|
||||
slides: Vec<Slide>,
|
||||
current: usize,
|
||||
last_advance: Instant,
|
||||
}
|
||||
|
||||
impl Slideshow {
|
||||
pub fn new(desktop: &str) -> Self {
|
||||
Self {
|
||||
slides: slides_for_desktop(desktop),
|
||||
current: 0,
|
||||
last_advance: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Her frame'de çağrılır. Otomatik ilerlemeyi ve çizimi yönetir.
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||
// Otomatik ilerleme
|
||||
if self.last_advance.elapsed() >= SLIDE_INTERVAL {
|
||||
self.current = (self.current + 1) % self.slides.len();
|
||||
self.last_advance = Instant::now();
|
||||
ui.ctx().request_repaint_after(SLIDE_INTERVAL);
|
||||
} else {
|
||||
// Bir sonraki değişime kadar geri sayım için tekrar çiz
|
||||
let remaining = SLIDE_INTERVAL
|
||||
.checked_sub(self.last_advance.elapsed())
|
||||
.unwrap_or_default();
|
||||
ui.ctx().request_repaint_after(remaining);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(crate::ui::theme::c_bg_widget())
|
||||
.rounding(egui::Rounding::same(10.0))
|
||||
.inner_margin(egui::Margin::same(24.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_min_height(180.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
// Simge veya Görsel
|
||||
if let Some(img) = image_source {
|
||||
ui.add(
|
||||
egui::Image::new(img)
|
||||
.max_height(64.0)
|
||||
.rounding(egui::Rounding::same(8.0)),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
egui::RichText::new(slide.icon)
|
||||
.size(48.0),
|
||||
);
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Başlık
|
||||
ui.label(
|
||||
egui::RichText::new(slide.title.clone())
|
||||
.size(17.0)
|
||||
.strong()
|
||||
.color(crate::ui::theme::c_text()),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Açıklama
|
||||
ui.label(
|
||||
egui::RichText::new(slide.description.clone())
|
||||
.size(13.0)
|
||||
.color(crate::ui::theme::c_text_dim()),
|
||||
);
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Nokta göstergesi
|
||||
ui.horizontal(|ui| {
|
||||
let dot_size = egui::vec2(8.0, 8.0);
|
||||
for i in 0..self.slides.len() {
|
||||
let color = if i == self.current {
|
||||
crate::ui::theme::c_accent()
|
||||
} else {
|
||||
crate::ui::theme::c_border()
|
||||
};
|
||||
let (rect, response) = ui.allocate_exact_size(dot_size, egui::Sense::click());
|
||||
ui.painter().circle_filled(rect.center(), 4.0, color);
|
||||
if response.clicked() {
|
||||
self.current = i;
|
||||
self.last_advance = Instant::now();
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn advance(&mut self) {
|
||||
self.current = (self.current + 1) % self.slides.len();
|
||||
self.last_advance = Instant::now();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.current == 0 {
|
||||
self.current = self.slides.len() - 1;
|
||||
} else {
|
||||
self.current -= 1;
|
||||
}
|
||||
self.last_advance = Instant::now();
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
//! PisiLinux kurulum teması.
|
||||
//!
|
||||
//! PisiLinux renk paleti:
|
||||
//! Ana mavi : #1A6FA8 Vurgu mavi: #2196F3
|
||||
//! Koyu arka: #050b0c18 Panel arka : #252535
|
||||
//! Kenarlık : #3A3A5C Metin : #E0E0F0
|
||||
//! Yeşil : #4CAF50 Kırmızı : #F44336
|
||||
//! Sarı : #FF9800
|
||||
|
||||
use eframe::egui::{self, Color32, FontId, Rounding, Stroke, Vec2, Visuals};
|
||||
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
// ─── Renk Atomics ──────────────────────────────────────────────
|
||||
static BG_DARK: AtomicU32 = AtomicU32::new(0x121214);
|
||||
static BG_PANEL: AtomicU32 = AtomicU32::new(0x1A1A1E);
|
||||
static BG_WIDGET: AtomicU32 = AtomicU32::new(0x242428);
|
||||
static BORDER: AtomicU32 = AtomicU32::new(0x323238);
|
||||
static TEXT: AtomicU32 = AtomicU32::new(0xF3F3F5);
|
||||
static TEXT_DIM: AtomicU32 = AtomicU32::new(0x9E9EAF);
|
||||
static SUCCESS: AtomicU32 = AtomicU32::new(0x4CAF50);
|
||||
static WARNING: AtomicU32 = AtomicU32::new(0xFF9800);
|
||||
static ERROR: AtomicU32 = AtomicU32::new(0xF44336);
|
||||
static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D);
|
||||
static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b)
|
||||
|
||||
// Helper to convert hex to Color32
|
||||
fn color_from_hex(rgb: u32) -> Color32 {
|
||||
Color32::from_rgb((rgb >> 16) as u8, ((rgb >> 8) & 0xFF) as u8, (rgb & 0xFF) as u8)
|
||||
}
|
||||
|
||||
pub fn c_bg_dark() -> Color32 { color_from_hex(BG_DARK.load(Ordering::Relaxed)) }
|
||||
pub fn c_bg_panel() -> Color32 { color_from_hex(BG_PANEL.load(Ordering::Relaxed)) }
|
||||
pub fn c_bg_widget() -> Color32 { color_from_hex(BG_WIDGET.load(Ordering::Relaxed)) }
|
||||
pub fn c_border() -> Color32 { color_from_hex(BORDER.load(Ordering::Relaxed)) }
|
||||
pub fn c_text() -> Color32 { color_from_hex(TEXT.load(Ordering::Relaxed)) }
|
||||
pub fn c_text_dim() -> Color32 { color_from_hex(TEXT_DIM.load(Ordering::Relaxed)) }
|
||||
pub fn c_success() -> Color32 { color_from_hex(SUCCESS.load(Ordering::Relaxed)) }
|
||||
pub fn c_warning() -> Color32 { color_from_hex(WARNING.load(Ordering::Relaxed)) }
|
||||
pub fn c_error() -> Color32 { color_from_hex(ERROR.load(Ordering::Relaxed)) }
|
||||
pub fn c_sidebar() -> Color32 { color_from_hex(SIDEBAR.load(Ordering::Relaxed)) }
|
||||
|
||||
pub fn set_theme_mode(is_dark: bool) {
|
||||
if is_dark {
|
||||
BG_DARK.store(0x121214, Ordering::Relaxed);
|
||||
BG_PANEL.store(0x1A1A1E, Ordering::Relaxed);
|
||||
BG_WIDGET.store(0x242428, Ordering::Relaxed);
|
||||
BORDER.store(0x323238, Ordering::Relaxed);
|
||||
TEXT.store(0xF3F3F5, Ordering::Relaxed);
|
||||
TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed);
|
||||
SIDEBAR.store(0x0B0B0D, Ordering::Relaxed);
|
||||
} else {
|
||||
BG_DARK.store(0xF0F0F5, Ordering::Relaxed);
|
||||
BG_PANEL.store(0xFFFFFF, Ordering::Relaxed);
|
||||
BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed);
|
||||
BORDER.store(0xD0D0DA, Ordering::Relaxed);
|
||||
TEXT.store(0x20202A, Ordering::Relaxed);
|
||||
TEXT_DIM.store(0x60606A, Ordering::Relaxed);
|
||||
SIDEBAR.store(0xE4E4EA, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn c_accent() -> Color32 {
|
||||
let rgb = ACCENT_COLOR.load(std::sync::atomic::Ordering::Relaxed);
|
||||
Color32::from_rgb((rgb >> 16) as u8, ((rgb >> 8) & 0xFF) as u8, (rgb & 0xFF) as u8)
|
||||
}
|
||||
|
||||
pub fn c_accent_dim() -> Color32 {
|
||||
let rgb = ACCENT_COLOR.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let r = (((rgb >> 16) & 0xFF) as f32 * 0.8) as u8;
|
||||
let g = (((rgb >> 8) & 0xFF) as f32 * 0.8) as u8;
|
||||
let b = ((rgb & 0xFF) as f32 * 0.8) as u8;
|
||||
Color32::from_rgb(r, g, b)
|
||||
}
|
||||
|
||||
/// egui bağlamına PisiLinux temasını uygular.
|
||||
/// `cc.egui_ctx` üzerinden `setup_custom_fonts` ve `set_visuals` çağrısını birleştirir.
|
||||
pub fn apply(ctx: &egui::Context, is_dark: bool) {
|
||||
setup_fonts(ctx);
|
||||
ctx.set_visuals(build_visuals(is_dark));
|
||||
ctx.set_style(build_style());
|
||||
}
|
||||
|
||||
fn setup_fonts(ctx: &egui::Context) {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
|
||||
// egui'nin yerleşik fontlarına ek olarak sistem fontunu dene.
|
||||
// ISO ortamında genellikle DejaVu veya Noto bulunur.
|
||||
// Bulunamazsa egui varsayılanı kullanılır.
|
||||
for path in [
|
||||
"/usr/share/fonts/exo-1/Exo2-Medium.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
] {
|
||||
if let Ok(data) = std::fs::read(path) {
|
||||
fonts.font_data.insert(
|
||||
"system_font".to_owned(),
|
||||
egui::FontData::from_owned(data),
|
||||
);
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, "system_font".to_owned());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
|
||||
fn build_visuals(is_dark: bool) -> Visuals {
|
||||
let mut v = if is_dark { Visuals::dark() } else { Visuals::light() };
|
||||
|
||||
// Genel arka planlar
|
||||
v.window_fill = c_bg_dark();
|
||||
v.panel_fill = c_bg_panel();
|
||||
v.faint_bg_color = c_bg_widget();
|
||||
v.extreme_bg_color = c_sidebar();
|
||||
v.code_bg_color = if is_dark { Color32::from_rgb(0x18, 0x18, 0x28) } else { Color32::from_rgb(0xEE, 0xEE, 0xF5) };
|
||||
|
||||
// Metin
|
||||
v.override_text_color = Some(c_text());
|
||||
|
||||
// Kenarlıklar
|
||||
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, c_border());
|
||||
v.widgets.inactive.bg_stroke = Stroke::new(1.0, c_border());
|
||||
v.widgets.hovered.bg_stroke = Stroke::new(1.5, c_accent());
|
||||
v.widgets.active.bg_stroke = Stroke::new(2.0, c_accent());
|
||||
|
||||
// Widget dolguları
|
||||
v.widgets.noninteractive.bg_fill = c_bg_widget();
|
||||
v.widgets.inactive.bg_fill = c_bg_widget();
|
||||
v.widgets.hovered.bg_fill = if is_dark { Color32::from_rgb(0x35, 0x35, 0x55) } else { Color32::from_rgb(0xD5, 0xD5, 0xE5) };
|
||||
v.widgets.active.bg_fill = c_accent_dim();
|
||||
|
||||
// Metin renkleri
|
||||
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, c_text_dim());
|
||||
v.widgets.inactive.fg_stroke = Stroke::new(1.0, c_text());
|
||||
v.widgets.hovered.fg_stroke = Stroke::new(1.5, c_text());
|
||||
v.widgets.active.fg_stroke = Stroke::new(2.0, if is_dark { Color32::WHITE } else { Color32::BLACK });
|
||||
|
||||
// Yuvarlak köşeler
|
||||
v.widgets.noninteractive.rounding = Rounding::same(6.0);
|
||||
v.widgets.inactive.rounding = Rounding::same(6.0);
|
||||
v.widgets.hovered.rounding = Rounding::same(6.0);
|
||||
v.widgets.active.rounding = Rounding::same(6.0);
|
||||
|
||||
// Seçim rengi
|
||||
v.selection.bg_fill = c_accent_dim();
|
||||
v.selection.stroke = Stroke::new(1.0, c_accent());
|
||||
|
||||
// Hyperlink
|
||||
v.hyperlink_color = c_accent();
|
||||
|
||||
// Pencere kenarlığı
|
||||
v.window_stroke = Stroke::new(1.0, c_border());
|
||||
v.window_rounding = Rounding::same(8.0);
|
||||
|
||||
v
|
||||
}
|
||||
|
||||
fn build_style() -> egui::Style {
|
||||
let mut style = egui::Style::default();
|
||||
|
||||
// Genel boşluk ve boyutlar
|
||||
style.spacing.item_spacing = Vec2::new(8.0, 6.0);
|
||||
style.spacing.button_padding = Vec2::new(12.0, 6.0);
|
||||
style.spacing.indent = 16.0;
|
||||
style.spacing.scroll.bar_width = 8.0;
|
||||
|
||||
// Metin boyutları
|
||||
style.text_styles = [
|
||||
(egui::TextStyle::Small, FontId::proportional(11.0)),
|
||||
(egui::TextStyle::Body, FontId::proportional(14.0)),
|
||||
(egui::TextStyle::Button, FontId::proportional(14.0)),
|
||||
(egui::TextStyle::Heading, FontId::proportional(20.0)),
|
||||
(egui::TextStyle::Monospace, FontId::monospace(13.0)),
|
||||
]
|
||||
.into();
|
||||
|
||||
style
|
||||
}
|
||||
|
||||
// ─── Yardımcı widget'lar ─────────────────────────────────────────
|
||||
|
||||
/// Vurgulu "ana eylem" butonu (mavi dolgu).
|
||||
pub fn primary_button(text: &str) -> egui::Button<'_> {
|
||||
egui::Button::new(egui::RichText::new(text).color(Color32::WHITE))
|
||||
.fill(c_accent())
|
||||
.stroke(Stroke::new(1.0, c_accent()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.min_size(Vec2::new(100.0, 32.0))
|
||||
}
|
||||
|
||||
/// İkincil "iptal/geri" butonu (şeffaf dolgu).
|
||||
pub fn secondary_button(text: &str) -> egui::Button<'_> {
|
||||
egui::Button::new(egui::RichText::new(text).color(c_text_dim()))
|
||||
.fill(Color32::TRANSPARENT)
|
||||
.stroke(Stroke::new(1.0, c_border()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.min_size(Vec2::new(80.0, 32.0))
|
||||
}
|
||||
|
||||
/// Kırmızı "tehlikeli eylem" butonu (disk silme onayı vb.).
|
||||
pub fn danger_button(text: &str) -> egui::Button<'_> {
|
||||
egui::Button::new(egui::RichText::new(text).color(Color32::WHITE))
|
||||
.fill(Color32::from_rgb(0xC6, 0x28, 0x28))
|
||||
.stroke(Stroke::new(1.0, c_error()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.min_size(Vec2::new(100.0, 32.0))
|
||||
}
|
||||
|
||||
/// Bölüm başlığı — büyük, açık renkli, alt çizgisiz.
|
||||
pub fn section_heading(ui: &mut egui::Ui, text: &str) {
|
||||
ui.label(
|
||||
egui::RichText::new(text)
|
||||
.size(18.0)
|
||||
.color(c_text())
|
||||
.strong(),
|
||||
);
|
||||
ui.add_space(2.0);
|
||||
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 1.0), egui::Sense::hover());
|
||||
ui.painter().line_segment(
|
||||
[rect.left_center(), rect.right_center()],
|
||||
Stroke::new(1.0, c_accent_dim()),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
|
||||
/// Hata kutusu — kırmızı çerçeveli bildirim.
|
||||
pub fn error_box(ui: &mut egui::Ui, msg: &str) {
|
||||
egui::Frame::none()
|
||||
.fill(Color32::from_rgba_unmultiplied(0xF4, 0x43, 0x36, 20))
|
||||
.stroke(Stroke::new(1.0, c_error()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(c_error(), "✗");
|
||||
ui.colored_label(c_error(), msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Uyarı kutusu — turuncu çerçeveli bildirim.
|
||||
pub fn warning_box(ui: &mut egui::Ui, msg: &str) {
|
||||
egui::Frame::none()
|
||||
.fill(Color32::from_rgba_unmultiplied(0xFF, 0x98, 0x00, 20))
|
||||
.stroke(Stroke::new(1.0, c_warning()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(c_warning(), "⚠");
|
||||
ui.colored_label(c_warning(), msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Başarı kutusu — yeşil çerçeveli bildirim.
|
||||
pub fn success_box(ui: &mut egui::Ui, msg: &str) {
|
||||
egui::Frame::none()
|
||||
.fill(Color32::from_rgba_unmultiplied(0x4C, 0xAF, 0x50, 20))
|
||||
.stroke(Stroke::new(1.0, c_success()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(c_success(), "✓");
|
||||
ui.colored_label(c_success(), msg);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user