feat: enhance keyboard layout and variant selection with search functionality and default options

This commit is contained in:
Erkan IŞIK
2026-06-01 21:32:25 +03:00
parent c2ef2e23fc
commit 7f35d0180b
7 changed files with 335 additions and 57 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")
+6
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 ─────────────────────────────────────────────
+6
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 ─────────────────────────────────────────────
+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>
+4 -4
View File
@@ -1245,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
@@ -1262,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(())
}
+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 {
+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 = [