3 Commits

17 changed files with 619 additions and 125 deletions
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Licensed under the GNU General Public License, version 3.
# See the file http://www.gnu.org/licenses/gpl.txt
from pisi.actionsapi import pisitools
from pisi.actionsapi import shelltools
def setup():
shelltools.system("cargo fetch --locked")
def build():
shelltools.system("cargo build --release --frozen")
def install():
pisitools.dobin("target/release/yali-rs")
pisitools.insinto("/usr/share/yali", "branding*.toml")
pisitools.insinto("/usr/share/yali/locales", "locales/*.toml")
pisitools.insinto("/usr/share/yali/assets", "assets/*")
pisitools.insinto("/usr/share/applications", "assets/yali.desktop")
pisitools.insinto("/usr/share/pixmaps", "assets/yali.png")
+1 -1
View File
@@ -15,7 +15,7 @@ pisi_logo_dark = "assets/pisi-logo-dark.png"
pisi_logo_light = "assets/pisi-logo-light.png" pisi_logo_light = "assets/pisi-logo-light.png"
# Title of the installation window # Title of the installation window
title = "PiSi Linux Installation" title = "YALI Installation Wizard"
# Subtitle displayed in the left sidebar # Subtitle displayed in the left sidebar
subtitle = "YALI" subtitle = "YALI"
+1 -1
View File
@@ -20,7 +20,7 @@ pisi_logo_dark = "assets/pisi-logo-dark.png"
pisi_logo_light = "assets/pisi-logo-light.png" pisi_logo_light = "assets/pisi-logo-light.png"
# Kurulum penceresinin başlığı (pencere çubuğu) # Kurulum penceresinin başlığı (pencere çubuğu)
title = "PiSi Linux Kurulumu" title = "YALI Kurulum Aracı"
# Sol kenar çubuğunda görüntülenen dağıtım ismi # Sol kenar çubuğunda görüntülenen dağıtım ismi
subtitle = "YALI" subtitle = "YALI"
+12
View File
@@ -25,8 +25,14 @@ keyboard = "Keyboard"
keyboard_title = "Keyboard Layout" keyboard_title = "Keyboard Layout"
keyboard_description = "Select your keyboard layout and test it below." keyboard_description = "Select your keyboard layout and test it below."
keyboard_layout_label = "Layout" keyboard_layout_label = "Layout"
keyboard_variant_label = "Variant"
keyboard_layout_search_hint = "Search layouts..."
keyboard_layout_search_no_match = "No layouts match"
keyboard_variant_search_hint = "Search variants..."
keyboard_variant_search_no_match = "No variants match"
keyboard_test_label = "Test Area" keyboard_test_label = "Test Area"
keyboard_test_hint = "Type here to test your keyboard…" keyboard_test_hint = "Type here to test your keyboard…"
keyboard_variant_default = "Default"
keyboard_selected = "Selected" keyboard_selected = "Selected"
# ── Partition ───────────────────────────────────────────── # ── Partition ─────────────────────────────────────────────
@@ -45,6 +51,7 @@ erase_warning = "WARNING: All data on the selected disk will be erased"
users = "Users" users = "Users"
users_title = "User Account" users_title = "User Account"
users_description = "Create a user account for your system." users_description = "Create a user account for your system."
real_name_label = "Full Name"
username_label = "Username" username_label = "Username"
password_label = "Password" password_label = "Password"
password_confirm_label = "Confirm password" password_confirm_label = "Confirm password"
@@ -154,6 +161,10 @@ pwd_very_strong = "Very strong"
use_same_for_admin = "Use the same password for the administrator (root) account" use_same_for_admin = "Use the same password for the administrator (root) account"
# ── Checker ─────────────────────────────────────────────── # ── Checker ───────────────────────────────────────────────
check_root = "Permissions"
check_root_pass = "Running with root privileges."
check_root_fail = "Not running as root! The installer requires root privileges. Please run with 'sudo yali-rs' or 'pkexec yali-rs'."
check_disk_space = "Disk Space" check_disk_space = "Disk Space"
check_disk_fail = "Maximum disk size is %{size} GB. Minimum 10 GB is required for installation." check_disk_fail = "Maximum disk size is %{size} GB. Minimum 10 GB is required for installation."
check_disk_warn = "%{size} GB disk available. 20 GB or more recommended." check_disk_warn = "%{size} GB disk available. 20 GB or more recommended."
@@ -248,6 +259,7 @@ mp_error_empty_mountpoint = "Mount point cannot be empty."
mp_plan_valid = "✓ Partition plan is valid. You can proceed." mp_plan_valid = "✓ Partition plan is valid. You can proceed."
mp_select_disk_first = "← Select a disk first." mp_select_disk_first = "← Select a disk first."
mp_add_dialog_hint = "/ or /home or swap" mp_add_dialog_hint = "/ or /home or swap"
mp_select_hint = "Select…"
mp_add_btn = "✓ Add" mp_add_btn = "✓ Add"
mp_size_hint = "Size (MB, 0=remaining):" mp_size_hint = "Size (MB, 0=remaining):"
mp_delete_confirm_yes = "✓ Yes, Delete" mp_delete_confirm_yes = "✓ Yes, Delete"
+13 -1
View File
@@ -25,8 +25,14 @@ keyboard = "Klavye"
keyboard_title = "Klavye Düzeni" keyboard_title = "Klavye Düzeni"
keyboard_description = "Kullanmak istediğiniz klavye düzenini seçin ve aşağıdan test edin." keyboard_description = "Kullanmak istediğiniz klavye düzenini seçin ve aşağıdan test edin."
keyboard_layout_label = "Düzen" keyboard_layout_label = "Düzen"
keyboard_variant_label = "Varyant"
keyboard_layout_search_hint = "Düzenlerde ara..."
keyboard_layout_search_no_match = "Eşleşen düzen yok"
keyboard_variant_search_hint = "Varyantlarda ara..."
keyboard_variant_search_no_match = "Eşleşen varyant yok"
keyboard_test_label = "Test Alanı" keyboard_test_label = "Test Alanı"
keyboard_test_hint = "Buraya yazarak klavyenizi test edin…" keyboard_test_hint = "Buraya yazarak klavyenizi test edin…"
keyboard_variant_default = "Varsayılan"
keyboard_selected = "Seçili" keyboard_selected = "Seçili"
# ── Partition ───────────────────────────────────────────── # ── Partition ─────────────────────────────────────────────
@@ -45,6 +51,7 @@ erase_warning = "UYARI: Seçilen diskteki tüm veriler silinecektir"
users = "Kullanıcı" users = "Kullanıcı"
users_title = "Kullanıcı Bilgileri" users_title = "Kullanıcı Bilgileri"
users_description = "Sisteminiz için bir kullanıcı hesabı oluşturun." users_description = "Sisteminiz için bir kullanıcı hesabı oluşturun."
real_name_label = "Ad Soyad"
username_label = "Kullanıcı adı" username_label = "Kullanıcı adı"
password_label = "Şifre" password_label = "Şifre"
password_confirm_label = "Şifre (tekrar)" password_confirm_label = "Şifre (tekrar)"
@@ -86,7 +93,7 @@ retry_install = "Yeniden Dene"
finish_next = "Tamamlandı →" finish_next = "Tamamlandı →"
# ── Finish ──────────────────────────────────────────────── # ── Finish ────────────────────────────────────────────────
finish_description = "PisiLinux başarıyla kuruldu. Şimdi sisteminizi yeniden başlatabilirsiniz." finish_description = "Pisi GNU/Linux başarıyla kuruldu. Şimdi sisteminizi yeniden başlatabilirsiniz."
finish_restart = "🔄 Yeniden Başlat" finish_restart = "🔄 Yeniden Başlat"
finish_live = "Canlı Masaüstüne Dön" finish_live = "Canlı Masaüstüne Dön"
install_in_progress = "Kurulum devam ediyor, lütfen bekleyin…" install_in_progress = "Kurulum devam ediyor, lütfen bekleyin…"
@@ -131,6 +138,10 @@ pwd_strong = "Güçlü"
pwd_very_strong = "Çok güçlü" pwd_very_strong = "Çok güçlü"
# ── Checker ─────────────────────────────────────────────── # ── Checker ───────────────────────────────────────────────
check_root = "Yetki"
check_root_pass = "Root yetkisi mevcut."
check_root_fail = "Root yetkisi yok! Kurulum için yönetici (root) olarak çalıştırılmalıdır. Lütfen 'sudo yali-rs' veya 'pkexec yali-rs' ile tekrar deneyin."
check_disk_space = "Disk Alanı" check_disk_space = "Disk Alanı"
check_disk_fail = "Maksimum disk boyutu %{size} GB. Kurulum için minimum 10 GB gerekli." check_disk_fail = "Maksimum disk boyutu %{size} GB. Kurulum için minimum 10 GB gerekli."
check_disk_warn = "%{size} GB disk mevcut. 20 GB veya üzeri önerilir." check_disk_warn = "%{size} GB disk mevcut. 20 GB veya üzeri önerilir."
@@ -220,6 +231,7 @@ mp_error_empty_mountpoint = "Bağlama noktası boş olamaz."
mp_plan_valid = "✓ Bölüm planı geçerli. İleri tuşuna basabilirsiniz." mp_plan_valid = "✓ Bölüm planı geçerli. İleri tuşuna basabilirsiniz."
mp_select_disk_first = "← Önce bir disk seçin." mp_select_disk_first = "← Önce bir disk seçin."
mp_add_dialog_hint = "/ veya /home veya swap" mp_add_dialog_hint = "/ veya /home veya swap"
mp_select_hint = "Seç…"
mp_add_btn = "✓ Ekle" mp_add_btn = "✓ Ekle"
mp_size_hint = "Boyut (MB, 0=kalan):" mp_size_hint = "Boyut (MB, 0=kalan):"
mp_delete_confirm_yes = "✓ Evet, Sil" mp_delete_confirm_yes = "✓ Evet, Sil"
+55
View File
@@ -0,0 +1,55 @@
<?xml version="1.0" ?>
<!DOCTYPE PISI SYSTEM "https://pisilinux.org/projeler/pisi/pisi-spec.dtd">
<PISI>
<Source>
<Name>yali-rs</Name>
<Homepage>https://gitlab.com/erkanisik/yali-rs</Homepage>
<Packager>
<Name>Erkan Işık</Name>
<Email>erkanisik@pisilinux.org</Email>
</Packager>
<License>GPLv2</License>
<IsA>app:gui</IsA>
<Summary xml:lang="en">Yet Another Linux Installer</Summary>
<Summary xml:lang="tr">PiSi Linux için Grafik Kurulum Programı</Summary>
<Description xml:lang="en">YALI is the graphical installer for PiSi Linux distribution which is written using Rust and egui framework.</Description>
<Description xml:lang="tr">YALI, Rust ve egui framework kullanılarak yazılmış olan PiSi Linux dağıtımının grafik kurulum programıdır.</Description>
<Archive sha1sum="" type="targz">
https://gitlab.com/erkanisik/yali-rs/-/archive/v4.0.0/yali-rs-v4.0.0.tar.gz
</Archive>
<BuildDependencies>
<Dependency>rustc</Dependency>
<Dependency>cargo</Dependency>
<Dependency>make</Dependency>
</BuildDependencies>
</Source>
<Package>
<Name>yali-rs</Name>
<RuntimeDependencies>
<Dependency>dbus</Dependency>
</RuntimeDependencies>
<Conflicts>
<Package>yali</Package>
</Conflicts>
<Files>
<Path fileType="executable">/usr/bin/yali-rs</Path>
<Path fileType="data">/usr/share/yali</Path>
<Path fileType="data">/usr/share/applications/yali.desktop</Path>
<Path fileType="data">/usr/share/pixmaps/yali.png</Path>
</Files>
</Package>
<History>
<Update release="1">
<Date>2026-06-01</Date>
<Version>4.0.0</Version>
<Comment>First release.</Comment>
<Name>Erkan IŞIK</Name>
<Email>erkanisik@pisilinux.org</Email>
</Update>
</History>
</PISI>
+24
View File
@@ -39,6 +39,7 @@ impl CheckResult {
pub fn run_all() -> Vec<CheckResult> { pub fn run_all() -> Vec<CheckResult> {
vec![ vec![
check_root(),
check_ram(), check_ram(),
check_disk_space(), check_disk_space(),
check_internet(), check_internet(),
@@ -234,6 +235,29 @@ fn check_live_env() -> CheckResult {
} }
} }
/// Kök kullanıcı (root) yetkisi kontrolü.
/// mount, swapon, parted gibi komutlar root gerektirdiğinden
/// installer'ın root olarak çalıştırılması zorunludur.
fn check_root() -> CheckResult {
let is_root = std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).trim() == "0")
} else {
None
}
})
.unwrap_or(false);
if is_root {
CheckResult::pass(t!("check_root"), t!("check_root_pass"))
} else {
CheckResult::fail(t!("check_root"), t!("check_root_fail"))
}
}
/// NVMe ve diğer modern disk arayüzlerinin çekirdek desteğini kontrol eder. /// NVMe ve diğer modern disk arayüzlerinin çekirdek desteğini kontrol eder.
fn check_nvme_support() -> CheckResult { fn check_nvme_support() -> CheckResult {
// /dev/nvme* cihazları var mı? // /dev/nvme* cihazları var mı?
+45 -13
View File
@@ -2,6 +2,48 @@ use eframe::egui;
use sysinfo::System; use sysinfo::System;
use rust_i18n::t; use rust_i18n::t;
/// Aktif masaüstü ortamını algılar.
/// Önce `XDG_CURRENT_DESKTOP` ortam değişkenine bakar (normal kullanıcı).
/// sudo ile çalışıldığında env var kaybolur, bu durumda `pgrep` ile
/// çalışan desktop session process'lerini kontrol eder.
pub fn detect_desktop_environment() -> String {
// 1. XDG_CURRENT_DESKTOP ortam değişkeni
if let Ok(de) = std::env::var("XDG_CURRENT_DESKTOP") {
let de = de.trim().to_lowercase();
if !de.is_empty() {
return de;
}
}
// 2. sudo ile çalışıldıysa env kaybolur — aktif session process'lerini kontrol et
let de_map: &[(&str, &[&str])] = &[
("plasma", &["plasmashell", "startplasma"]),
("xfce", &["xfce4-session", "xfdesktop"]),
("gnome", &["gnome-session"]),
("cinnamon", &["cinnamon-session"]),
("budgie", &["budgie-wm"]),
("lxqt", &["lxqt-session"]),
("mate", &["mate-session"]),
("lumina", &["lumina-desktop"]),
("lxde", &["lxsession"]),
];
for (de_name, procs) in de_map {
for proc in *procs {
if let Ok(out) = std::process::Command::new("pgrep")
.args(["-x", proc])
.output()
{
if out.status.success() {
return de_name.to_string();
}
}
}
}
// 3. Hiçbiri bulunamazsa varsayılan
"plasma".to_string()
}
/// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur. /// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur.
/// ///
/// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1` /// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1`
@@ -154,6 +196,7 @@ pub struct GlobalState {
pub logical_volumes: Vec<LogicalVolume>, pub logical_volumes: Vec<LogicalVolume>,
pub keyboard_layout: String, pub keyboard_layout: String,
pub keyboard_variant: String, pub keyboard_variant: String,
pub real_name: String,
pub username: String, pub username: String,
pub password: String, pub password: String,
pub password_confirm: String, pub password_confirm: String,
@@ -169,7 +212,6 @@ pub struct GlobalState {
pub bootloader_timeout: u32, pub bootloader_timeout: u32,
pub bootloader_password: String, pub bootloader_password: String,
pub kernel_options: String, pub kernel_options: String,
pub license_accepted: bool,
pub root_password: String, pub root_password: String,
pub root_password_confirm: String, pub root_password_confirm: String,
pub display_manager: String, pub display_manager: String,
@@ -217,6 +259,7 @@ impl Default for GlobalState {
logical_volumes: Vec::new(), logical_volumes: Vec::new(),
keyboard_layout: "tr".to_string(), keyboard_layout: "tr".to_string(),
keyboard_variant: String::new(), keyboard_variant: String::new(),
real_name: String::new(),
username: String::new(), username: String::new(),
password: String::new(), password: String::new(),
password_confirm: String::new(), password_confirm: String::new(),
@@ -232,7 +275,6 @@ impl Default for GlobalState {
bootloader_timeout: 5, bootloader_timeout: 5,
bootloader_password: String::new(), bootloader_password: String::new(),
kernel_options: String::new(), kernel_options: String::new(),
license_accepted: false,
root_password: String::new(), root_password: String::new(),
root_password_confirm: String::new(), root_password_confirm: String::new(),
network_wifi_password: String::new(), network_wifi_password: String::new(),
@@ -251,17 +293,7 @@ impl Default for GlobalState {
selected_package_groups: Vec::new(), selected_package_groups: Vec::new(),
display_manager: "sddm".to_string(), display_manager: "sddm".to_string(),
autologin: true, autologin: true,
desktop_environment: { desktop_environment: detect_desktop_environment(),
let de = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.trim()
.to_lowercase();
if de.is_empty() {
"plasma".to_string()
} else {
de
}
},
branding_icons: std::collections::HashMap::new(), branding_icons: std::collections::HashMap::new(),
branding_pisi_dark: String::new(), branding_pisi_dark: String::new(),
branding_pisi_light: String::new(), branding_pisi_light: String::new(),
+37 -10
View File
@@ -167,7 +167,8 @@ impl Job for MountBindJob {
("/run", format!("{}/run", mt)), // COMAR/dbus için gerekli ("/run", format!("{}/run", mt)), // COMAR/dbus için gerekli
]; ];
for (src, target) in &bind_dirs { for (src, target) in &bind_dirs {
run_cmd("mkdir", &["-p", target]).await?; tokio::fs::create_dir_all(target).await
.map_err(|e| format!("{} oluşturulamadı: {}", target, e))?;
ui.log(format!("mount --bind {} {}", src, target)); ui.log(format!("mount --bind {} {}", src, target));
run_cmd("mount", &["--bind", src, target]).await?; run_cmd("mount", &["--bind", src, target]).await?;
} }
@@ -175,7 +176,8 @@ impl Job for MountBindJob {
// efivarfs — UEFI kurulumunda grub-install için zorunlu // efivarfs — UEFI kurulumunda grub-install için zorunlu
let efivarfs_target = format!("{}/sys/firmware/efi/efivars", mt); let efivarfs_target = format!("{}/sys/firmware/efi/efivars", mt);
if std::path::Path::new("/sys/firmware/efi/efivars").exists() { if std::path::Path::new("/sys/firmware/efi/efivars").exists() {
run_cmd("mkdir", &["-p", &efivarfs_target]).await?; tokio::fs::create_dir_all(&efivarfs_target).await
.map_err(|e| format!("{} oluşturulamadı: {}", efivarfs_target, e))?;
ui.log(format!("mount --bind /sys/firmware/efi/efivars {}", efivarfs_target)); ui.log(format!("mount --bind /sys/firmware/efi/efivars {}", efivarfs_target));
run_cmd("mount", &["--bind", "/sys/firmware/efi/efivars", &efivarfs_target]).await?; run_cmd("mount", &["--bind", "/sys/firmware/efi/efivars", &efivarfs_target]).await?;
} }
@@ -308,6 +310,12 @@ impl Job for InstallGrubJob {
fn name(&self) -> &str { "job_bootloader" } fn name(&self) -> &str { "job_bootloader" }
async fn run(&self, ui: &UiSender) -> Result<(), String> { async fn run(&self, ui: &UiSender) -> Result<(), String> {
// Ana dizinleri oluştur (demo modunda rsync çalışmaz)
let etc_default = format!("{}/etc/default", self.mount);
let _ = tokio::fs::create_dir_all(&etc_default).await;
let grub_d = format!("{}/etc/grub.d", self.mount);
let _ = tokio::fs::create_dir_all(&grub_d).await;
// GRUB şifresi (varsa) — bcrypt hash ile /etc/grub.d/40_custom yazılır // GRUB şifresi (varsa) — bcrypt hash ile /etc/grub.d/40_custom yazılır
if !self.password.is_empty() { if !self.password.is_empty() {
ui.log("GRUB şifresi yapılandırılıyor…"); ui.log("GRUB şifresi yapılandırılıyor…");
@@ -776,7 +784,8 @@ impl Job for MountPartitionsJob {
if self.plan.efi_mb > 0 { if self.plan.efi_mb > 0 {
let efi_dir = format!("{}/boot/efi", mount); let efi_dir = format!("{}/boot/efi", mount);
ui.log(format!("mkdir -p {}", efi_dir)); ui.log(format!("mkdir -p {}", efi_dir));
run_cmd("mkdir", &["-p", &efi_dir]).await?; tokio::fs::create_dir_all(&efi_dir).await
.map_err(|e| format!("{} oluşturulamadı: {}", efi_dir, e))?;
let efi_part = part_path(disk, 1); let efi_part = part_path(disk, 1);
ui.log(format!("mount {} {}", efi_part, efi_dir)); ui.log(format!("mount {} {}", efi_part, efi_dir));
run_cmd("mount", &[&efi_part, &efi_dir]).await?; run_cmd("mount", &[&efi_part, &efi_dir]).await?;
@@ -1175,6 +1184,9 @@ impl Job for ConfigureHostnameJob {
fn name(&self) -> &str { "job_hostname" } fn name(&self) -> &str { "job_hostname" }
async fn run(&self, ui: &UiSender) -> Result<(), String> { async fn run(&self, ui: &UiSender) -> Result<(), String> {
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
// /etc/hostname // /etc/hostname
let hostname_path = format!("{}/etc/hostname", self.mount); let hostname_path = format!("{}/etc/hostname", self.mount);
ui.log(format!("Yazılıyor: {}", hostname_path)); ui.log(format!("Yazılıyor: {}", hostname_path));
@@ -1210,6 +1222,10 @@ impl Job for ConfigureLocaleJob {
fn name(&self) -> &str { "job_locale" } fn name(&self) -> &str { "job_locale" }
async fn run(&self, ui: &UiSender) -> Result<(), String> { async fn run(&self, ui: &UiSender) -> Result<(), String> {
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
let _ = tokio::fs::create_dir_all(format!("{}/etc/env.d", self.mount)).await;
// /etc/locale.conf (systemd-style) // /etc/locale.conf (systemd-style)
let conf_path = format!("{}/etc/locale.conf", self.mount); let conf_path = format!("{}/etc/locale.conf", self.mount);
let conf = format!( let conf = format!(
@@ -1229,7 +1245,7 @@ impl Job for ConfigureLocaleJob {
let _ = tokio::fs::write(&pisi_locale_path, pisi_locale).await; let _ = tokio::fs::write(&pisi_locale_path, pisi_locale).await;
// /etc/locale.gen — satırın başındaki # kaldırılır // /etc/locale.gen — satırın başındaki # kaldırılır
let gen_path = format!("{}/etc/locale.gen", self.mount); let gen_path = format!("{}/etc/locale-gen", self.mount);
ui.log(format!("Güncelleniyor: {}", gen_path)); ui.log(format!("Güncelleniyor: {}", gen_path));
let content = tokio::fs::read_to_string(&gen_path).await.unwrap_or_default(); let content = tokio::fs::read_to_string(&gen_path).await.unwrap_or_default();
let updated = content let updated = content
@@ -1246,11 +1262,11 @@ impl Job for ConfigureLocaleJob {
.join("\n"); .join("\n");
tokio::fs::write(&gen_path, updated + "\n") tokio::fs::write(&gen_path, updated + "\n")
.await .await
.map_err(|e| format!("/etc/locale.gen yazılamadı: {}", e))?; .map_err(|e| format!("/etc/locale-gen yazılamadı: {}", e))?;
// locale-gen çalıştır // locale-gen çalıştır
ui.log("locale-gen çalıştırılıyor…"); ui.log("locale.gen çalıştırılıyor…");
run_chroot(&self.mount, &["locale-gen"]).await?; run_chroot(&self.mount, &["locale.gen"]).await?;
Ok(()) Ok(())
} }
@@ -1361,7 +1377,8 @@ impl Job for ConfigureKeyboardJob {
// /etc/X11/xorg.conf.d/00-keyboard.conf (X11 için) // /etc/X11/xorg.conf.d/00-keyboard.conf (X11 için)
let x11_dir = format!("{}/etc/X11/xorg.conf.d", self.mount); let x11_dir = format!("{}/etc/X11/xorg.conf.d", self.mount);
let x11_path = format!("{}/00-keyboard.conf", x11_dir); let x11_path = format!("{}/00-keyboard.conf", x11_dir);
run_cmd("mkdir", &["-p", &x11_dir]).await?; tokio::fs::create_dir_all(&x11_dir).await
.map_err(|e| format!("{} oluşturulamadı: {}", x11_dir, e))?;
let options_line = if self.variant.is_empty() { let options_line = if self.variant.is_empty() {
String::new() String::new()
@@ -1450,7 +1467,10 @@ impl Job for CreateUserJob {
} }
// sudoers: wheel grubu sudo yetkisi alsın // sudoers: wheel grubu sudo yetkisi alsın
let sudoers_path = format!("{}/etc/sudoers.d/wheel", self.mount); let sudoers_dir = format!("{}/etc/sudoers.d", self.mount);
tokio::fs::create_dir_all(&sudoers_dir).await
.map_err(|e| format!("{} oluşturulamadı: {}", sudoers_dir, e))?;
let sudoers_path = format!("{}/wheel", sudoers_dir);
ui.log(format!("Yazılıyor: {}", sudoers_path)); ui.log(format!("Yazılıyor: {}", sudoers_path));
tokio::fs::write(&sudoers_path, "%wheel ALL=(ALL:ALL) ALL\n") tokio::fs::write(&sudoers_path, "%wheel ALL=(ALL:ALL) ALL\n")
.await .await
@@ -1510,6 +1530,9 @@ impl Job for GenerateFstabJob {
fn name(&self) -> &str { "job_fstab" } fn name(&self) -> &str { "job_fstab" }
async fn run(&self, ui: &UiSender) -> Result<(), String> { async fn run(&self, ui: &UiSender) -> Result<(), String> {
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
let fstab_path = format!("{}/etc/fstab", self.mount); let fstab_path = format!("{}/etc/fstab", self.mount);
ui.log(format!("Yazılıyor: {}", fstab_path)); ui.log(format!("Yazılıyor: {}", fstab_path));
@@ -1872,7 +1895,8 @@ impl Job for MountCustomPartitionsJob {
for item in mount_items { for item in mount_items {
let target_path = format!("{}{}", self.mount, item.mountpoint); let target_path = format!("{}{}", self.mount, item.mountpoint);
ui.log(format!("Bağlama noktası dizini oluşturuluyor: {}", target_path)); ui.log(format!("Bağlama noktası dizini oluşturuluyor: {}", target_path));
run_cmd("mkdir", &["-p", &target_path]).await?; tokio::fs::create_dir_all(&target_path).await
.map_err(|e| format!("{} oluşturulamadı: {}", target_path, e))?;
ui.log(format!("Bağlanıyor: {} ➔ {}", item.device, target_path)); ui.log(format!("Bağlanıyor: {} ➔ {}", item.device, target_path));
run_cmd("mount", &[&item.device, &target_path]).await?; run_cmd("mount", &[&item.device, &target_path]).await?;
} }
@@ -1893,6 +1917,9 @@ impl Job for FstabFromCustomPartitionsJob {
fn name(&self) -> &str { "job_fstab" } fn name(&self) -> &str { "job_fstab" }
async fn run(&self, ui: &UiSender) -> Result<(), String> { async fn run(&self, ui: &UiSender) -> Result<(), String> {
let etc_dir = format!("{}/etc", self.mount);
let _ = tokio::fs::create_dir_all(&etc_dir).await;
let fstab_path = format!("{}/etc/fstab", self.mount); let fstab_path = format!("{}/etc/fstab", self.mount);
ui.log(format!("Yazılıyor: {}", fstab_path)); ui.log(format!("Yazılıyor: {}", fstab_path));
+1 -2
View File
@@ -6,7 +6,7 @@ mod autoinstall;
mod branding; mod branding;
use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep}; use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep};
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, LicenseStep, NetworkStep, DisplayManagerStep}; use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep};
use eframe::egui; use eframe::egui;
use rust_i18n::t; use rust_i18n::t;
@@ -219,7 +219,6 @@ impl Default for YaliApp {
steps: vec![ steps: vec![
Box::new(WelcomeStep::default()), Box::new(WelcomeStep::default()),
Box::new(LocationStep::default()), Box::new(LocationStep::default()),
Box::new(LicenseStep),
Box::new(KeyboardStep::default()), Box::new(KeyboardStep::default()),
Box::new(NetworkStep::default()), Box::new(NetworkStep::default()),
Box::new(PartitionStep::default()), Box::new(PartitionStep::default()),
+18 -11
View File
@@ -3,6 +3,7 @@ use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep}; use crate::installer::{GlobalState, InstallerStep};
const DM_OPTIONS: &[&str] = &["sddm", "lightdm", "gdm"]; const DM_OPTIONS: &[&str] = &["sddm", "lightdm", "gdm"];
const DE_OPTIONS: &[&str] = &["plasma", "xfce", "gnome", "cinnamon", "budgie", "lxde", "lxqt", "mate", "lumina"];
#[derive(Default)] #[derive(Default)]
pub struct DisplayManagerStep; pub struct DisplayManagerStep;
@@ -13,14 +14,9 @@ impl InstallerStep for DisplayManagerStep {
} }
fn on_enter(&mut self, state: &mut GlobalState) { fn on_enter(&mut self, state: &mut GlobalState) {
let desktop = std::env::var("XDG_CURRENT_DESKTOP") let desktop = crate::installer::detect_desktop_environment();
.unwrap_or_default()
.trim()
.to_lowercase();
if !desktop.is_empty() { if !desktop.is_empty() {
state.desktop_environment = desktop; state.desktop_environment = desktop;
} else if state.desktop_environment.is_empty() {
state.desktop_environment = "plasma".to_string();
} }
} }
@@ -48,13 +44,24 @@ impl InstallerStep for DisplayManagerStep {
}); });
ui.end_row(); ui.end_row();
// Masaüstü ortamı (otomatik algılanır) // Masaüstü ortamı (seçilebilir)
ui.label(t!("display_manager_de")); ui.label(t!("display_manager_de"));
if !state.desktop_environment.is_empty() && state.desktop_environment != "default" { let current_de = if state.desktop_environment.is_empty() || state.desktop_environment == "default" {
ui.colored_label(egui::Color32::from_rgb(120, 180, 120), &state.desktop_environment); "plasma"
} else { } else {
ui.colored_label(egui::Color32::from_rgb(200, 160, 40), t!("display_manager_de_not_found")); state.desktop_environment.as_str()
} };
egui::ComboBox::from_id_source("de_combo")
.selected_text(current_de)
.show_ui(ui, |ui| {
for de in DE_OPTIONS {
ui.selectable_value(
&mut state.desktop_environment,
de.to_string(),
*de,
);
}
});
ui.end_row(); ui.end_row();
// Otomatik oturum açma // Otomatik oturum açma
+239 -52
View File
@@ -1,29 +1,138 @@
use eframe::egui; use eframe::egui;
use rust_i18n::t; use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep}; use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
/// Klavye düzeni ve varyantı seçimi. /// Klavye düzeni ve varyantı seçimi.
/// Gerçek uygulamada mevcut düzenler `localectl list-keymaps` /// Mevcut düzenler `/usr/share/X11/xkb/rules/evdev.lst` dosyasından okunur.
/// veya `/usr/share/X11/xkb/rules/evdev.lst` dosyasından okunabilir. /// Dosya okunamazsa sabit bir temel liste kullanılır.
pub struct KeyboardStep { pub struct KeyboardStep {
test_input: String, test_input: String,
layouts: Vec<(&'static str, &'static str)>, // (kod, gösterim adı) layout_filter: String,
variant_filter: String,
layouts: Vec<(String, String)>, // (kod, gösterim adı)
variants: Vec<(String, String, String)>, // (layout, variant, gösterim adı)
}
fn default_keyboard_layouts() -> Vec<(String, String)> {
vec![
("tr".to_string(), "Türkçe (Q)".to_string()),
("us".to_string(), "İngilizce (US)".to_string()),
("de".to_string(), "Almanca".to_string()),
("fr".to_string(), "Fransızca".to_string()),
("es".to_string(), "İspanyolca".to_string()),
("ru".to_string(), "Rusça".to_string()),
("ar".to_string(), "Arapça".to_string()),
]
}
fn default_keyboard_variants() -> Vec<(String, String, String)> {
vec![
("tr".to_string(), "f".to_string(), "Türkçe (F)".to_string()),
]
}
fn load_keyboard_layouts() -> (Vec<(String, String)>, Vec<(String, String, String)>) {
let path = "/usr/share/X11/xkb/rules/evdev.lst";
let contents = std::fs::read_to_string(path).ok();
let mut layouts = Vec::new();
let mut variants = Vec::new();
let mut section: Option<&str> = None;
if let Some(text) = contents {
for line in text.lines() {
let line = line.trim_end();
if line.starts_with('!') {
section = if line.starts_with("! layout") {
Some("layout")
} else if line.starts_with("! variant") {
Some("variant")
} else {
None
};
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(section) = section {
if let Some((code, desc)) = split_code_desc(trimmed) {
match section {
"layout" => layouts.push((code.to_string(), desc.to_string())),
"variant" => {
if let Some((layout, display)) = desc.split_once(':') {
variants.push((layout.trim().to_string(), code.to_string(), display.trim().to_string()));
}
}
_ => {}
}
}
}
}
}
if layouts.is_empty() {
(default_keyboard_layouts(), default_keyboard_variants())
} else {
(layouts, variants)
}
}
fn split_code_desc(line: &str) -> Option<(&str, &str)> {
let mut parts = line.splitn(2, char::is_whitespace);
let code = parts.next()?.trim();
let desc = parts.next()?.trim();
if code.is_empty() || desc.is_empty() {
None
} else {
Some((code, desc))
}
}
fn layout_display_name(layout: &str, layouts: &[(String, String)]) -> String {
layouts
.iter()
.find(|(code, _)| code == layout)
.map(|(_, display)| display.clone())
.unwrap_or_else(|| layout.to_string())
}
fn variant_display_name(
layout: &str,
variant: &str,
variants: &[(String, String, String)],
) -> String {
variants
.iter()
.find(|(layout_code, v_code, _)| layout_code == layout && v_code == variant)
.map(|(_, _, display)| display.clone())
.unwrap_or_else(|| variant.to_string())
}
fn apply_system_keyboard_layout(layout: &str, variant: &str) {
if std::env::var("DISPLAY").is_ok() {
let mut cmd = std::process::Command::new("setxkbmap");
cmd.arg(layout);
if !variant.is_empty() {
cmd.arg("-variant").arg(variant);
}
let _ = cmd.output();
}
} }
impl Default for KeyboardStep { impl Default for KeyboardStep {
fn default() -> Self { fn default() -> Self {
let (layouts, variants) = load_keyboard_layouts();
Self { Self {
test_input: String::new(), test_input: String::new(),
layouts: vec![ layout_filter: String::new(),
("tr", "Türkçe (Q)"), variant_filter: String::new(),
("tr:f", "Türkçe (F)"), layouts,
("us", "İngilizce (US)"), variants,
("de", "Almanca"),
("fr", "Fransızca"),
("es", "İspanyolca"),
("ru", "Rusça"),
("ar", "Arapça"),
],
} }
} }
} }
@@ -42,6 +151,7 @@ impl InstallerStep for KeyboardStep {
"us".to_string() "us".to_string()
}; };
} }
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
} }
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
@@ -50,51 +160,128 @@ impl InstallerStep for KeyboardStep {
ui.add_space(12.0); ui.add_space(12.0);
let available_variants: Vec<_> = self
.variants
.iter()
.filter(|(layout, _, _)| layout == &state.keyboard_layout)
.collect();
ui.columns(2, |cols| { ui.columns(2, |cols| {
// Sol: Düzen listesi cols[0].vertical(|ui| {
cols[0].label(t!("keyboard_layout_label")); egui::Frame::group(ui.style())
egui::ScrollArea::vertical() .fill(theme::c_bg_widget())
.id_source("kbd_layouts") .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200)))
.max_height(260.0) .rounding(egui::Rounding::same(8.0))
.show(&mut cols[0], |ui| { .inner_margin(egui::Margin::same(8.0))
// &(code, display) pattern ile code: &str, display: &str olur .show(ui, |ui| {
for &(code, display) in &self.layouts { ui.label(t!("keyboard_layout_label"));
// tr:f kodunu layout+variant olarak ayır ui.add(
let (layout, variant) = if let Some(pos) = code.find(':') { egui::TextEdit::singleline(&mut self.layout_filter)
(&code[..pos], &code[pos + 1..]) .hint_text(t!("keyboard_layout_search_hint")),
} else { );
(code, "") ui.add_space(4.0);
};
let is_selected = state.keyboard_layout == layout egui::ScrollArea::vertical()
&& state.keyboard_variant == variant; .id_source("kbd_layout_scroll")
.auto_shrink([false, false])
.max_height(240.0)
.show(ui, |ui| {
let filter = self.layout_filter.to_lowercase();
let filtered_layouts: Vec<_> = self.layouts.iter()
.filter(|(code, display)| {
filter.is_empty()
|| code.to_lowercase().contains(&filter)
|| display.to_lowercase().contains(&filter)
})
.collect();
if ui.selectable_label(is_selected, display).clicked() { if filtered_layouts.is_empty() {
state.keyboard_layout = layout.to_string(); ui.label(t!("keyboard_layout_search_no_match"));
state.keyboard_variant = variant.to_string(); }
} for (code, display) in filtered_layouts {
} let is_selected = state.keyboard_layout == *code;
}); if ui.selectable_label(is_selected, display).clicked() {
state.keyboard_layout = code.to_string();
self.variant_filter.clear();
if !self.variants.iter().any(|(layout, _, _)| layout == code) {
state.keyboard_variant.clear();
}
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
}
}
});
});
});
// Sağ: Test alanı cols[1].vertical(|ui| {
cols[1].label(t!("keyboard_test_label")); egui::Frame::group(ui.style())
cols[1].add_space(4.0); .fill(theme::c_bg_widget())
cols[1].add( .stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200)))
egui::TextEdit::multiline(&mut self.test_input) .rounding(egui::Rounding::same(8.0))
.desired_rows(5) .inner_margin(egui::Margin::same(8.0))
.hint_text(t!("keyboard_test_hint")), .show(ui, |ui| {
); ui.label(t!("keyboard_variant_label"));
ui.add(
egui::TextEdit::singleline(&mut self.variant_filter)
.hint_text(t!("keyboard_variant_search_hint")),
);
ui.add_space(4.0);
cols[1].add_space(8.0); egui::ScrollArea::vertical()
cols[1].label(format!("{}: {}", t!("keyboard_selected"), { .id_source("kbd_variant_scroll")
let variant_part = if state.keyboard_variant.is_empty() { .auto_shrink([false, false])
String::new() .max_height(240.0)
} else { .show(ui, |ui| {
format!(" ({})", state.keyboard_variant) let variant_filter = self.variant_filter.to_lowercase();
}; let filtered_variants: Vec<_> = available_variants
format!("{}{}", state.keyboard_layout, variant_part) .into_iter()
})); .filter(|(_, variant, display)| {
variant_filter.is_empty()
|| variant.to_lowercase().contains(&variant_filter)
|| display.to_lowercase().contains(&variant_filter)
})
.collect();
let mut selected_default = state.keyboard_variant.is_empty();
if ui.selectable_value(&mut selected_default, true, t!("keyboard_variant_default")).clicked() {
state.keyboard_variant.clear();
apply_system_keyboard_layout(&state.keyboard_layout, "");
}
if filtered_variants.is_empty() {
ui.label(t!("keyboard_variant_search_no_match"));
}
for (_, variant, display) in filtered_variants {
let is_selected = state.keyboard_variant == *variant;
if ui.selectable_label(is_selected, display).clicked() {
state.keyboard_variant = variant.to_string();
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
}
}
});
});
});
}); });
ui.add_space(12.0);
ui.label(t!("keyboard_test_label"));
ui.add_space(4.0);
ui.add(
egui::TextEdit::multiline(&mut self.test_input)
.desired_rows(5)
.desired_width(ui.available_width())
.hint_text(t!("keyboard_test_hint")),
);
ui.add_space(8.0);
ui.label(format!("{}: {}", t!("keyboard_selected"), {
let layout_name = layout_display_name(&state.keyboard_layout, &self.layouts);
if state.keyboard_variant.is_empty() {
layout_name
} else {
let variant_name = variant_display_name(&state.keyboard_layout, &state.keyboard_variant, &self.variants);
format!("{} ({})", layout_name, variant_name)
}
}));
} }
fn is_complete(&self, state: &GlobalState) -> bool { fn is_complete(&self, state: &GlobalState) -> bool {
-2
View File
@@ -5,7 +5,6 @@ pub mod execution;
pub mod finish; pub mod finish;
pub mod partition; pub mod partition;
pub mod bootloader; pub mod bootloader;
pub mod license;
pub mod network; pub mod network;
pub mod rescue; pub mod rescue;
pub mod display_manager; pub mod display_manager;
@@ -18,7 +17,6 @@ pub use execution::ExecutionStep;
pub use finish::FinishStep; pub use finish::FinishStep;
pub use partition::PartitionStep; pub use partition::PartitionStep;
pub use bootloader::BootloaderStep; pub use bootloader::BootloaderStep;
pub use license::LicenseStep;
pub use network::NetworkStep; pub use network::NetworkStep;
pub use rescue::RescueStep; pub use rescue::RescueStep;
pub use display_manager::DisplayManagerStep; pub use display_manager::DisplayManagerStep;
+56 -10
View File
@@ -172,10 +172,12 @@ use crate::installer::{
}; };
const MIN_DISK_GB: u64 = 10; const MIN_DISK_GB: u64 = 10;
const EFI_MB: u64 = 512; const EFI_MB: u64 = 300;
const SWAP_MB_MIN: u64 = 512; const SWAP_MB_MIN: u64 = 512;
const SWAP_MB_MAX: u64 = 8 * 1024; const SWAP_MB_MAX: u64 = 8 * 1024;
const COMMON_MOUNTPOINTS: &[&str] = &["/", "/boot", "/boot/efi", "/home", "/var", "/tmp", "swap"];
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
// Manuel bölümleme için yerel durum // Manuel bölümleme için yerel durum
// ───────────────────────────────────────────── // ─────────────────────────────────────────────
@@ -597,8 +599,9 @@ impl PartitionStep {
.spacing([12.0, 8.0]) .spacing([12.0, 8.0])
.show(ui, |ui| { .show(ui, |ui| {
// Dosya sistemi // Dosya sistemi (boyut önerisi ile)
ui.label(t!("mp_fstype")); ui.label(t!("mp_fstype"));
let prev_fs = self.manual.add_fstype;
egui::ComboBox::from_id_source("fs_combo") egui::ComboBox::from_id_source("fs_combo")
.selected_text(FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?")) .selected_text(FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?"))
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
@@ -608,6 +611,15 @@ impl PartitionStep {
); );
} }
}); });
// FS tipi değişince akıllı boyut öner
if prev_fs != self.manual.add_fstype && self.manual.add_size_str.is_empty() {
let fs = FS_OPTIONS.get(self.manual.add_fstype).map(|x| &x.1);
match fs {
Some(FsType::Fat32) => self.manual.add_size_str = "300".to_string(),
Some(FsType::Swap) => self.manual.add_size_str = format!("{}", recommended_swap_mb()),
_ => {}
}
}
ui.end_row(); ui.end_row();
// Boyut // Boyut
@@ -615,11 +627,28 @@ impl PartitionStep {
ui.text_edit_singleline(&mut self.manual.add_size_str); ui.text_edit_singleline(&mut self.manual.add_size_str);
ui.end_row(); ui.end_row();
// Bağlama noktası // Bağlama noktası (2 aşamalı: combo + input)
ui.label(t!("mp_mountpoint")); ui.label(t!("mp_mountpoint"));
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_mountpoint) ui.horizontal(|ui| {
.hint_text(t!("mp_add_dialog_hint")); let current_mp = if self.manual.add_mountpoint.is_empty() {
ui.add(mp_field); t!("mp_select_hint").to_string()
} else {
self.manual.add_mountpoint.clone()
};
egui::ComboBox::from_id_source("mp_combo")
.selected_text(&current_mp)
.width(120.0)
.show_ui(ui, |ui| {
for mp in COMMON_MOUNTPOINTS {
if ui.selectable_label(false, *mp).clicked() {
self.manual.add_mountpoint = mp.to_string();
}
}
});
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.end_row();
// LUKS şifreleme // LUKS şifreleme
@@ -858,11 +887,28 @@ impl PartitionStep {
ui.text_edit_singleline(&mut self.manual.add_lv_size_str); ui.text_edit_singleline(&mut self.manual.add_lv_size_str);
ui.end_row(); ui.end_row();
// Bağlama noktası // Bağlama noktası (2 aşamalı: combo + input)
ui.label(t!("mp_mountpoint")); ui.label(t!("mp_mountpoint"));
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_lv_mountpoint) ui.horizontal(|ui| {
.hint_text(t!("mp_add_dialog_hint")); let current_mp = if self.manual.add_lv_mountpoint.is_empty() {
ui.add(mp_field); t!("mp_select_hint").to_string()
} else {
self.manual.add_lv_mountpoint.clone()
};
egui::ComboBox::from_id_source("lv_mp_combo")
.selected_text(&current_mp)
.width(120.0)
.show_ui(ui, |ui| {
for mp in COMMON_MOUNTPOINTS {
if ui.selectable_label(false, *mp).clicked() {
self.manual.add_lv_mountpoint = mp.to_string();
}
}
});
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_lv_mountpoint)
.hint_text(t!("mp_add_dialog_hint"));
ui.add(mp_field);
});
ui.end_row(); ui.end_row();
}); });
+92 -21
View File
@@ -23,6 +23,33 @@ impl Default for UsersStep {
} }
} }
/// Gerçek addan geçerli bir Linux kullanıcı adı türetir.
/// - Küçük harfe çevirir
/// - Türkçe karakterleri ASCII'ye dönüştürür (ş→s, ğ→g, ı→i, ö→o, ü→u, ç→c)
/// - Boşlukları `-`'ye çevirir
/// - Geçersiz karakterleri temizler
fn derive_username(real_name: &str) -> String {
let mut name = real_name.to_lowercase();
name = name
.replace('ş', "s")
.replace('ğ', "g")
.replace('ı', "i")
.replace('ö', "o")
.replace('ü', "u")
.replace('ç', "c")
.replace(' ', "-")
.replace('_', "-");
let valid: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect();
let trimmed = valid.trim_matches('-').to_string();
if trimmed.is_empty() {
"user".to_string()
} else if trimmed.len() > 32 {
trimmed[..32].trim_end_matches('-').to_string()
} else {
trimmed
}
}
/// Kullanıcı adının geçerliliğini kontrol eder. /// Kullanıcı adının geçerliliğini kontrol eder.
/// Linux kuralları: küçük harf veya alt çizgi ile başlamalı, /// Linux kuralları: küçük harf veya alt çizgi ile başlamalı,
/// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli. /// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli.
@@ -50,6 +77,24 @@ fn is_valid_hostname(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
} }
/// Otomatik hostname üretirken nokta, boşluk ve geçersiz karakterleri kaldırır.
fn sanitize_hostname(s: &str) -> String {
let mut hostname: String = s
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect();
if hostname.len() > 63 {
hostname.truncate(63);
}
if hostname.is_empty() {
"host".to_string()
} else {
hostname
}
}
fn password_strength(p: &str) -> (String, egui::Color32) { fn password_strength(p: &str) -> (String, egui::Color32) {
let len = p.len(); let len = p.len();
let has_upper = p.chars().any(|c| c.is_uppercase()); let has_upper = p.chars().any(|c| c.is_uppercase());
@@ -75,9 +120,24 @@ impl InstallerStep for UsersStep {
} }
fn on_enter(&mut self, state: &mut GlobalState) { fn on_enter(&mut self, state: &mut GlobalState) {
// Hostname boşsa username'den öner // Gerçek ad varsa ama kullanıcı adı yoksa türet
if !state.real_name.is_empty() && state.username.is_empty() {
state.username = derive_username(&state.real_name);
}
// Hostname boşsa önce /etc/hostname dene
if state.hostname.is_empty() {
let sys_hostname = std::fs::read_to_string("/etc/hostname")
.unwrap_or_default()
.trim()
.to_string();
let sys_hostname = sanitize_hostname(&sys_hostname);
if !sys_hostname.is_empty() && sys_hostname != "localhost" {
state.hostname = sys_hostname;
}
}
// Hâlâ boşsa ve username varsa username-pc öner
if state.hostname.is_empty() && !state.username.is_empty() { if state.hostname.is_empty() && !state.username.is_empty() {
state.hostname = format!("{}-pc", state.username); state.hostname = sanitize_hostname(&format!("{}pc", state.username));
} }
} }
@@ -99,23 +159,37 @@ impl InstallerStep for UsersStep {
.inner_margin(egui::Margin::same(12.0)) .inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| { .show(ui, |ui| {
ui.set_width(ui.available_width()); ui.set_width(ui.available_width());
egui::Grid::new("users_grid") egui::Grid::new("users_grid")
.num_columns(2) .num_columns(2)
.spacing([12.0, 10.0]) .spacing([12.0, 10.0])
.show(ui, |ui| { .show(ui, |ui| {
// Kullanıcı adı // Gerçek ad
ui.label(egui::RichText::new(t!("username_label")).strong().color(theme::c_text())); ui.label(egui::RichText::new(t!("real_name_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.username); let prev = state.real_name.clone();
if !state.username.is_empty() { ui.text_edit_singleline(&mut state.real_name);
if is_valid_username(&state.username) { if state.real_name != prev && !state.real_name.is_empty() {
ui.colored_label(theme::c_success(), "✓"); state.username = derive_username(&state.real_name);
} else {
ui.colored_label(theme::c_error(), "✗");
} }
} if !state.real_name.is_empty() {
}); ui.colored_label(theme::c_success(), "✓");
ui.end_row(); }
});
ui.end_row();
// Kullanıcı adı (otomatik türetilir, düzenlenebilir)
ui.label(egui::RichText::new(t!("username_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.username);
if !state.username.is_empty() {
if is_valid_username(&state.username) {
ui.colored_label(theme::c_success(), "✓");
} else {
ui.colored_label(theme::c_error(), "✗");
}
}
});
ui.end_row();
// Şifre // Şifre
ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text())); ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text()));
@@ -159,9 +233,6 @@ impl InstallerStep for UsersStep {
// Hostname // Hostname
ui.label(egui::RichText::new(t!("hostname_label")).strong().color(theme::c_text())); ui.label(egui::RichText::new(t!("hostname_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| { ui.horizontal(|ui| {
if state.hostname.is_empty() && !state.username.is_empty() {
state.hostname = format!("{}-pc", state.username);
}
ui.text_edit_singleline(&mut state.hostname); ui.text_edit_singleline(&mut state.hostname);
if !state.hostname.is_empty() { if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) { if is_valid_hostname(&state.hostname) {
+1 -1
View File
@@ -176,7 +176,7 @@ fn build_style() -> egui::Style {
style.spacing.item_spacing = Vec2::new(8.0, 6.0); style.spacing.item_spacing = Vec2::new(8.0, 6.0);
style.spacing.button_padding = Vec2::new(12.0, 6.0); style.spacing.button_padding = Vec2::new(12.0, 6.0);
style.spacing.indent = 16.0; style.spacing.indent = 16.0;
style.spacing.scroll.bar_width = 8.0; style.spacing.scroll.bar_width = 14.0;
// Metin boyutları // Metin boyutları
style.text_styles = [ style.text_styles = [
BIN
View File
Binary file not shown.