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"
# Title of the installation window
title = "PiSi Linux Installation"
title = "YALI Installation Wizard"
# Subtitle displayed in the left sidebar
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"
# 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
subtitle = "YALI"
+12
View File
@@ -25,8 +25,14 @@ keyboard = "Keyboard"
keyboard_title = "Keyboard Layout"
keyboard_description = "Select your keyboard layout and test it below."
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_hint = "Type here to test your keyboard…"
keyboard_variant_default = "Default"
keyboard_selected = "Selected"
# ── Partition ─────────────────────────────────────────────
@@ -45,6 +51,7 @@ erase_warning = "WARNING: All data on the selected disk will be erased"
users = "Users"
users_title = "User Account"
users_description = "Create a user account for your system."
real_name_label = "Full Name"
username_label = "Username"
password_label = "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"
# ── 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_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."
@@ -248,6 +259,7 @@ mp_error_empty_mountpoint = "Mount point cannot be empty."
mp_plan_valid = "✓ Partition plan is valid. You can proceed."
mp_select_disk_first = "← Select a disk first."
mp_add_dialog_hint = "/ or /home or swap"
mp_select_hint = "Select…"
mp_add_btn = "✓ Add"
mp_size_hint = "Size (MB, 0=remaining):"
mp_delete_confirm_yes = "✓ Yes, Delete"
+13 -1
View File
@@ -25,8 +25,14 @@ keyboard = "Klavye"
keyboard_title = "Klavye Düzeni"
keyboard_description = "Kullanmak istediğiniz klavye düzenini seçin ve aşağıdan test edin."
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_hint = "Buraya yazarak klavyenizi test edin…"
keyboard_variant_default = "Varsayılan"
keyboard_selected = "Seçili"
# ── Partition ─────────────────────────────────────────────
@@ -45,6 +51,7 @@ erase_warning = "UYARI: Seçilen diskteki tüm veriler silinecektir"
users = "Kullanıcı"
users_title = "Kullanıcı Bilgileri"
users_description = "Sisteminiz için bir kullanıcı hesabı oluşturun."
real_name_label = "Ad Soyad"
username_label = "Kullanıcı adı"
password_label = "Şifre"
password_confirm_label = "Şifre (tekrar)"
@@ -86,7 +93,7 @@ retry_install = "Yeniden Dene"
finish_next = "Tamamlandı →"
# ── 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_live = "Canlı Masaüstüne Dön"
install_in_progress = "Kurulum devam ediyor, lütfen bekleyin…"
@@ -131,6 +138,10 @@ pwd_strong = "Güçlü"
pwd_very_strong = "Çok güçlü"
# ── 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_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."
@@ -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_select_disk_first = "← Önce bir disk seçin."
mp_add_dialog_hint = "/ veya /home veya swap"
mp_select_hint = "Seç…"
mp_add_btn = "✓ Ekle"
mp_size_hint = "Boyut (MB, 0=kalan):"
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> {
vec![
check_root(),
check_ram(),
check_disk_space(),
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.
fn check_nvme_support() -> CheckResult {
// /dev/nvme* cihazları var mı?
+45 -13
View File
@@ -2,6 +2,48 @@ use eframe::egui;
use sysinfo::System;
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.
///
/// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1`
@@ -154,6 +196,7 @@ pub struct GlobalState {
pub logical_volumes: Vec<LogicalVolume>,
pub keyboard_layout: String,
pub keyboard_variant: String,
pub real_name: String,
pub username: String,
pub password: String,
pub password_confirm: String,
@@ -169,7 +212,6 @@ pub struct GlobalState {
pub bootloader_timeout: u32,
pub bootloader_password: String,
pub kernel_options: String,
pub license_accepted: bool,
pub root_password: String,
pub root_password_confirm: String,
pub display_manager: String,
@@ -217,6 +259,7 @@ impl Default for GlobalState {
logical_volumes: Vec::new(),
keyboard_layout: "tr".to_string(),
keyboard_variant: String::new(),
real_name: String::new(),
username: String::new(),
password: String::new(),
password_confirm: String::new(),
@@ -232,7 +275,6 @@ impl Default for GlobalState {
bootloader_timeout: 5,
bootloader_password: String::new(),
kernel_options: String::new(),
license_accepted: false,
root_password: String::new(),
root_password_confirm: String::new(),
network_wifi_password: String::new(),
@@ -251,17 +293,7 @@ impl Default for GlobalState {
selected_package_groups: Vec::new(),
display_manager: "sddm".to_string(),
autologin: true,
desktop_environment: {
let de = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.trim()
.to_lowercase();
if de.is_empty() {
"plasma".to_string()
} else {
de
}
},
desktop_environment: detect_desktop_environment(),
branding_icons: std::collections::HashMap::new(),
branding_pisi_dark: 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
];
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));
run_cmd("mount", &["--bind", src, target]).await?;
}
@@ -175,7 +176,8 @@ impl Job for MountBindJob {
// efivarfs — UEFI kurulumunda grub-install için zorunlu
let efivarfs_target = format!("{}/sys/firmware/efi/efivars", mt);
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));
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" }
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
if !self.password.is_empty() {
ui.log("GRUB şifresi yapılandırılıyor…");
@@ -776,7 +784,8 @@ impl Job for MountPartitionsJob {
if self.plan.efi_mb > 0 {
let efi_dir = format!("{}/boot/efi", mount);
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);
ui.log(format!("mount {} {}", efi_part, efi_dir));
run_cmd("mount", &[&efi_part, &efi_dir]).await?;
@@ -1175,6 +1184,9 @@ impl Job for ConfigureHostnameJob {
fn name(&self) -> &str { "job_hostname" }
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
let hostname_path = format!("{}/etc/hostname", self.mount);
ui.log(format!("Yazılıyor: {}", hostname_path));
@@ -1210,6 +1222,10 @@ impl Job for ConfigureLocaleJob {
fn name(&self) -> &str { "job_locale" }
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)
let conf_path = format!("{}/etc/locale.conf", self.mount);
let conf = format!(
@@ -1229,7 +1245,7 @@ impl Job for ConfigureLocaleJob {
let _ = tokio::fs::write(&pisi_locale_path, pisi_locale).await;
// /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));
let content = tokio::fs::read_to_string(&gen_path).await.unwrap_or_default();
let updated = content
@@ -1246,11 +1262,11 @@ impl Job for ConfigureLocaleJob {
.join("\n");
tokio::fs::write(&gen_path, updated + "\n")
.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
ui.log("locale-gen çalıştırılıyor…");
run_chroot(&self.mount, &["locale-gen"]).await?;
ui.log("locale.gen çalıştırılıyor…");
run_chroot(&self.mount, &["locale.gen"]).await?;
Ok(())
}
@@ -1361,7 +1377,8 @@ impl Job for ConfigureKeyboardJob {
// /etc/X11/xorg.conf.d/00-keyboard.conf (X11 için)
let x11_dir = format!("{}/etc/X11/xorg.conf.d", self.mount);
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() {
String::new()
@@ -1450,7 +1467,10 @@ impl Job for CreateUserJob {
}
// 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));
tokio::fs::write(&sudoers_path, "%wheel ALL=(ALL:ALL) ALL\n")
.await
@@ -1510,6 +1530,9 @@ impl Job for GenerateFstabJob {
fn name(&self) -> &str { "job_fstab" }
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);
ui.log(format!("Yazılıyor: {}", fstab_path));
@@ -1872,7 +1895,8 @@ impl Job for MountCustomPartitionsJob {
for item in mount_items {
let target_path = format!("{}{}", self.mount, item.mountpoint);
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));
run_cmd("mount", &[&item.device, &target_path]).await?;
}
@@ -1893,6 +1917,9 @@ impl Job for FstabFromCustomPartitionsJob {
fn name(&self) -> &str { "job_fstab" }
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);
ui.log(format!("Yazılıyor: {}", fstab_path));
+1 -2
View File
@@ -6,7 +6,7 @@ mod autoinstall;
mod branding;
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 rust_i18n::t;
@@ -219,7 +219,6 @@ impl Default for YaliApp {
steps: vec![
Box::new(WelcomeStep::default()),
Box::new(LocationStep::default()),
Box::new(LicenseStep),
Box::new(KeyboardStep::default()),
Box::new(NetworkStep::default()),
Box::new(PartitionStep::default()),
+18 -11
View File
@@ -3,6 +3,7 @@ use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
const DM_OPTIONS: &[&str] = &["sddm", "lightdm", "gdm"];
const DE_OPTIONS: &[&str] = &["plasma", "xfce", "gnome", "cinnamon", "budgie", "lxde", "lxqt", "mate", "lumina"];
#[derive(Default)]
pub struct DisplayManagerStep;
@@ -13,14 +14,9 @@ impl InstallerStep for DisplayManagerStep {
}
fn on_enter(&mut self, state: &mut GlobalState) {
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_default()
.trim()
.to_lowercase();
let desktop = crate::installer::detect_desktop_environment();
if !desktop.is_empty() {
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();
// Masaüstü ortamı (otomatik algılanır)
// Masaüstü ortamı (seçilebilir)
ui.label(t!("display_manager_de"));
if !state.desktop_environment.is_empty() && state.desktop_environment != "default" {
ui.colored_label(egui::Color32::from_rgb(120, 180, 120), &state.desktop_environment);
let current_de = if state.desktop_environment.is_empty() || state.desktop_environment == "default" {
"plasma"
} 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();
// Otomatik oturum açma
+239 -52
View File
@@ -1,29 +1,138 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
/// 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.
/// Mevcut düzenler `/usr/share/X11/xkb/rules/evdev.lst` dosyasından okunur.
/// Dosya okunamazsa sabit bir temel liste kullanılır.
pub struct KeyboardStep {
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 {
fn default() -> Self {
let (layouts, variants) = load_keyboard_layouts();
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"),
],
layout_filter: String::new(),
variant_filter: String::new(),
layouts,
variants,
}
}
}
@@ -42,6 +151,7 @@ impl InstallerStep for KeyboardStep {
"us".to_string()
};
}
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
@@ -50,51 +160,128 @@ impl InstallerStep for KeyboardStep {
ui.add_space(12.0);
let available_variants: Vec<_> = self
.variants
.iter()
.filter(|(layout, _, _)| layout == &state.keyboard_layout)
.collect();
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, "")
};
cols[0].vertical(|ui| {
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200)))
.rounding(egui::Rounding::same(8.0))
.inner_margin(egui::Margin::same(8.0))
.show(ui, |ui| {
ui.label(t!("keyboard_layout_label"));
ui.add(
egui::TextEdit::singleline(&mut self.layout_filter)
.hint_text(t!("keyboard_layout_search_hint")),
);
ui.add_space(4.0);
let is_selected = state.keyboard_layout == layout
&& state.keyboard_variant == variant;
egui::ScrollArea::vertical()
.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() {
state.keyboard_layout = layout.to_string();
state.keyboard_variant = variant.to_string();
}
}
});
if filtered_layouts.is_empty() {
ui.label(t!("keyboard_layout_search_no_match"));
}
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].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].vertical(|ui| {
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200)))
.rounding(egui::Rounding::same(8.0))
.inner_margin(egui::Margin::same(8.0))
.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);
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)
}));
egui::ScrollArea::vertical()
.id_source("kbd_variant_scroll")
.auto_shrink([false, false])
.max_height(240.0)
.show(ui, |ui| {
let variant_filter = self.variant_filter.to_lowercase();
let filtered_variants: Vec<_> = available_variants
.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 {
-2
View File
@@ -5,7 +5,6 @@ pub mod execution;
pub mod finish;
pub mod partition;
pub mod bootloader;
pub mod license;
pub mod network;
pub mod rescue;
pub mod display_manager;
@@ -18,7 +17,6 @@ pub use execution::ExecutionStep;
pub use finish::FinishStep;
pub use partition::PartitionStep;
pub use bootloader::BootloaderStep;
pub use license::LicenseStep;
pub use network::NetworkStep;
pub use rescue::RescueStep;
pub use display_manager::DisplayManagerStep;
+56 -10
View File
@@ -172,10 +172,12 @@ use crate::installer::{
};
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_MAX: u64 = 8 * 1024;
const COMMON_MOUNTPOINTS: &[&str] = &["/", "/boot", "/boot/efi", "/home", "/var", "/tmp", "swap"];
// ─────────────────────────────────────────────
// Manuel bölümleme için yerel durum
// ─────────────────────────────────────────────
@@ -597,8 +599,9 @@ impl PartitionStep {
.spacing([12.0, 8.0])
.show(ui, |ui| {
// Dosya sistemi
// Dosya sistemi (boyut önerisi ile)
ui.label(t!("mp_fstype"));
let prev_fs = self.manual.add_fstype;
egui::ComboBox::from_id_source("fs_combo")
.selected_text(FS_OPTIONS.get(self.manual.add_fstype).map(|x| x.0).unwrap_or("?"))
.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();
// Boyut
@@ -615,11 +627,28 @@ impl PartitionStep {
ui.text_edit_singleline(&mut self.manual.add_size_str);
ui.end_row();
// Bağlama noktası
// Bağlama noktası (2 aşamalı: combo + input)
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.horizontal(|ui| {
let current_mp = if self.manual.add_mountpoint.is_empty() {
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();
// LUKS şifreleme
@@ -858,11 +887,28 @@ impl PartitionStep {
ui.text_edit_singleline(&mut self.manual.add_lv_size_str);
ui.end_row();
// Bağlama noktası
// Bağlama noktası (2 aşamalı: combo + input)
ui.label(t!("mp_mountpoint"));
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_lv_mountpoint)
.hint_text(t!("mp_add_dialog_hint"));
ui.add(mp_field);
ui.horizontal(|ui| {
let current_mp = if self.manual.add_lv_mountpoint.is_empty() {
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();
});
+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.
/// Linux kuralları: küçük harf veya alt çizgi ile başlamalı,
/// 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 == '-')
}
/// 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) {
let len = p.len();
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) {
// 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() {
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))
.show(ui, |ui| {
ui.set_width(ui.available_width());
egui::Grid::new("users_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Kullanıcı adı
ui.label(egui::RichText::new(t!("username_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.username);
if !state.username.is_empty() {
if is_valid_username(&state.username) {
ui.colored_label(theme::c_success(), "");
} else {
ui.colored_label(theme::c_error(), "");
egui::Grid::new("users_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Gerçek ad
ui.label(egui::RichText::new(t!("real_name_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
let prev = state.real_name.clone();
ui.text_edit_singleline(&mut state.real_name);
if state.real_name != prev && !state.real_name.is_empty() {
state.username = derive_username(&state.real_name);
}
}
});
ui.end_row();
if !state.real_name.is_empty() {
ui.colored_label(theme::c_success(), "");
}
});
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
ui.label(egui::RichText::new(t!("password_label")).strong().color(theme::c_text()));
@@ -159,9 +233,6 @@ impl InstallerStep for UsersStep {
// Hostname
ui.label(egui::RichText::new(t!("hostname_label")).strong().color(theme::c_text()));
ui.horizontal(|ui| {
if state.hostname.is_empty() && !state.username.is_empty() {
state.hostname = format!("{}-pc", state.username);
}
ui.text_edit_singleline(&mut state.hostname);
if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) {
+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.button_padding = Vec2::new(12.0, 6.0);
style.spacing.indent = 16.0;
style.spacing.scroll.bar_width = 8.0;
style.spacing.scroll.bar_width = 14.0;
// Metin boyutları
style.text_styles = [
BIN
View File
Binary file not shown.