Merge branch 'main' into 'main'

feat: keyboard layout/variant search with custom combobox, hex color constants, various UI fixes

See merge request ayhanyalcinsoy/yali-rs!8
This commit is contained in:
2026-07-07 12:39:04 +00:00
23 changed files with 3547 additions and 443 deletions
+46 -2
View File
@@ -1,6 +1,50 @@
# Changelog
# Changelog — [English](CHANGELOG_en.md)
## [4.0.2] — 2026-06-21
## [4.0.4] — 2026-06-25
### Added
- Kullanıcı adı doğrulama fonksiyonu `validate_username` (detaylı hata mesajları döndürür)
- Yasaklı sistem kullanıcı adları listesi (root, daemon, bin, sys, vb.)
- Türkçe karakter dönüşümleri büyük harfleri de kapsayacak şekilde genişletildi (Ş, Ğ, İ, Ö, Ü, Ç)
- Locale dosyalarına yeni kullanıcı adı doğrulama anahtarları (username_too_long, username_forbidden, username_invalid_start, username_invalid_chars)
- UsersStep struct'ına `autologin` alanı eklendi
### Changed
- Kullanıcı adı, şifre doğrulama ve hostname input'ları için check/fail iconları input'un yanına alındı
- Hata mesajları inputların altında gösteriliyor (username doğrulama, şifre eşleşme)
- Önyükleyici (bootloader) sayfası form düzeni users sayfasındaki gibi güncellendi (Frame, form_label, form_input_text/password)
- Display manager adımı adım listesinden kaldırıldı (ISO'lar tek masaüstü ile geliyor)
- Autologin özelliği display manager adımından users adımına taşındı
- Display manager yapılandırması otomatik hale getirildi (desktop_environment otomatik tespit ediliyor)
- SDDM/GDM yapılandırmasında masaüstü ortamı seçimi kullanılıyor (xfce.desktop, gnome.desktop vb.)
- Autologin varsayılan değeri false olarak değiştirildi
### Fixed
- Dark modda sidebar metin renginin kaybolması sorunu (SIDEBAR_TEXT beyaz olarak ayarlandı)
- derive_username fonksiyonunda yanlış karakter (ā → ğ) düzeltildi
- unused_mut warning (name değişkeni)
- unused variable warning (err değişkeni)
- unused import warning (DisplayManagerStep)
---
## [4.0.3] — 2026-06-23
### Added
- Aramalı klavye düzeni/varyant seçici: egui `TextEdit` + `Area` popup ile özel implementasyon
- `color_from_hex(hex: u32)` yardımcısı (`combobox_stil.rs`)
- `layout_buf`, `variant_buf` alanları (`KeyboardStep`)
### Changed
- `egui-dropdown` bağımlılığı kaldırıldı (artık kullanılmıyor)
- Düzen/Varyant seçiciler yan yana → alt alta, etiket genişliği en uzun etikete göre eşitlendi
- Popup öğe yüksekliği 28px → 32px, gradient arkaplan (`draw_gradient_bg`) ile `combobox.rs` stilinde
- `DROPDOWN_TEXT_COLOR` kırık beyaz → tam beyaz (`color_from_hex(0xFFFFFF)`)
- Tüm renk sabitleri hex formatına çevrildi (`color_from_hex`)
### Fixed
- Popup ilk açılışta eski seçim filtresiyle tüm öğelerin görünmemesi — odaklanınca tampon temizleniyor
- `t!()` dönüş tipi `Cow<str>``String` uyumsuzluğu (`keyboard.rs`)
### Changed
- Sürgü maksimum değeri artık diskteki kullanılabilir alan kadar (diğer bölümler hariç)
+176
View File
@@ -0,0 +1,176 @@
# Changelog — [Türkçe](CHANGELOG.md)
## [4.0.4] — 2026-06-25
### Added
- Username validation function `validate_username` (returns detailed error messages)
- Forbidden system usernames list (root, daemon, bin, sys, etc.)
- Turkish character conversions extended to include uppercase letters (Ş, Ğ, İ, Ö, Ü, Ç)
- New username validation keys in locale files (username_too_long, username_forbidden, username_invalid_start, username_invalid_chars)
- `autologin` field added to UsersStep struct
### Changed
- Check/fail icons for username, password validation, and hostname inputs moved next to input fields
- Error messages shown below inputs (username validation, password mismatch)
- Bootloader page form layout updated to match users page (Frame, form_label, form_input_text/password)
- Display manager step removed from step list (ISOs ship with single desktop environment)
- Autologin feature moved from display manager step to users step
- Display manager configuration made automatic (desktop_environment auto-detected)
- Desktop environment selection used in SDDM/GDM configuration (xfce.desktop, gnome.desktop, etc.)
- Autologin default value changed to false
### Fixed
- Sidebar text color disappearing in dark mode (SIDEBAR_TEXT set to white)
- Incorrect character in derive_username function (ā → ğ)
- unused_mut warning (name variable)
- unused variable warning (err variable)
- unused import warning (DisplayManagerStep)
---
## [4.0.3] — 2026-06-23
### Added
- Searchable keyboard layout/variant selector: egui `TextEdit` + `Area` popup custom implementation
- `color_from_hex(hex: u32)` helper (`combobox_stil.rs`)
- `layout_buf`, `variant_buf` fields (`KeyboardStep`)
### Changed
- `egui-dropdown` dependency removed (no longer used)
- Layout/Variant selectors side-by-side → stacked, label width equalized to longest label
- Popup item height 28px → 32px, gradient background (`draw_gradient_bg`) in `combobox.rs` style
- `DROPDOWN_TEXT_COLOR` off-white → pure white (`color_from_hex(0xFFFFFF)`)
- All color constants converted to hex format (`color_from_hex`)
### Fixed
- Popup not showing all items on first open due to old selection filter — buffer cleared on focus
- `t!()` return type `Cow<str>``String` mismatch (`keyboard.rs`)
### Changed
- Slider maximum value now equals available disk space (excluding other partitions)
- Selected partition capacity included in slider range in edit mode
- `form_input_text` size field directly connected to `add_size_str`, formatted MB display added
- Size label now in `"123.456 MB"` format instead of `"123 MB"` (for parseable values)
### Fixed
- Value written to size input field not written back to `add_size_str` — text edit remained read-only
---
## [4.0.1] — 2026-06-19
### Added
- `c_root_bg()` theme function
- `show_root_password`, `show_root_password_confirm` fields (`UsersStep`)
- `form_input_text` / `form_input_password` wrapped with `Frame` and `.frame(false)`
### Changed
- `form_input_text`: Wrapped in Frame, added stroke/fill/rounding, height 32→10
- `form_input_password`: SVG eye icons (instead of emoji), Frame wrapped, `override_text_color`
- Disk grid selection colors `theme::c_*()` → constant `(237,237,224,1)`
- Disk list header and column headers: `Align::Center``Align::LEFT`, `vertical_centered` removed
- Root password fields raw `TextEdit``form_input_password` (with eye icon + frame)
- User form Frame color `theme::c_bg_widget()``(240,243,198)`
- Manual disk list brand/model label ordering changed
### Fixed
- `users.rs:154``Color32` import error (missing `egui::` prefix)
---
## [4.0.0] — 2026-06-18
### Added
- `CHANGELOG.md`
- `src/ui/combobox.rs` — ComboBox helpers (`show_combo_box`, `selectable_value`, `selectable_value_custom`, `draw_gradient_bg`)
- `src/ui/combobox_stil.rs` — ComboBox visual constants and `apply_visuals()`
- `src/steps/location.rs` — Region/timezone selection step
- `src/steps/network.rs` — Network configuration step
- `src/steps/rescue.rs` — Rescue mode
- `xkbcommon` dependency
- Keyboard visual: XKB keymap → egui drawing (`compile_layout`, `draw_layout`, `keysym_to_label`)
- ISO105 key position mapping (function keys, numpad, navigation included)
- Single character display per key (uppercase preferred)
### Changed
- Keyboard visual: KLE JSON download/parse → direct keymap compilation with xkbcommon
- `Cargo.toml`: `ureq`, `serde_json` removed; `xkbcommon` added
- ComboBox styles moved from `theme.rs``combobox_stil.rs`
- Comment lines added to `secondary_button` gradient colors
- `draw_gradient_bg` moved to `combobox_stil.rs`
- All ComboBox usages updated with `theme::show_combo_box` / `theme::selectable_value`
- Keyboard step (`keyboard.rs`): `ScrollArea`+`selectable_label` → ComboBox
- `TextEdit::multiline` background made with per-widget `Frame::group`
- `locale/` files updated
- Font sizes increased (label 9→12, secondary 7→9)
### Removed
- `ureq`, `serde_json` dependencies
- `assets/keyboards/KLE_*.json` cache files
- `assets/keyboards/KB_*.svg` files
- Old keyboard SVG loading code
- KLE JSON download/cache logic
- Old button examples in demo page
### Fixed
- `set_style`/`set_visuals` order: `set_style` called first
- Region/timezone labels vertically aligned
- Keyboard step `on_enter` not called on first render — layout also initialized in `show()`
- `xkbcommon::Keycode` iteration (conversion with `raw()`)
- `None::<&str>``None::<String>` type mismatch
---
## [4.0.0] — 2026-06-06
### Added
- Rescue mode (rescue step)
- Network configuration step (NetworkManager/Wicd)
- Netinstall step
- LVM and LUKS encryption support
- Demo mode (`--demo`)
- Automatic installation (`--auto-install answer.toml`)
- Multi-language support with `rust-i18n` (Turkish, English)
- Gradient buttons (`gradient_button`, `secondary_button`)
- Language selector (flag + label)
- SVG loading support (`egui_extras` + `image`)
- Slideshow (configurable with branding.toml)
- Step list in sidebar (processed steps green ✔️)
- Real-time installation file tracking
- Advanced partitioning UI (usage bar, LUKS indicator, mount-point selector)
### Changed
- `pspec.xml`/`actions.py` → Cargo-based build
- `comar`/`mudur` calls → `zbus` D-Bus and direct command execution
- Shell-based password management → secure stdin piping (LUKS/GRUB)
- Automatic username derivation (real name → system username)
- Hostname validation/sanitization
- Cleanup operations: user deletion, pool management, boot directory cleanup
- Rsync command with dynamic exclusion
- Version 4.0.1 → 4.0.0 (Cargo.toml compatibility)
### Fixed
- GRUB password writing security
- Dracut/Mkinitcpio LVM module addition
- EFI partition detection and mounting
- Password strength indicator colors
- Automatic partitioning (LVM/LUKS ordering)
---
## [0.1.0] — 2026-05-20
### Added
- First stable release
- Project skeleton (egui + wgpu)
- Step-based installation wizard
- Automatic disk partitioning (GPT/MBR)
- User creation (with password strength check)
- Bootloader configuration (GRUB)
- Display manager selection
- Summary screen
- Basic color theme (light/dark)
- Customizable branding (branding.toml)
- Error screen for rescue purposes
- Job queue (JobQueue / async task management)
- YALI serialization (installer state save/load)
+8 -7
View File
@@ -2,9 +2,12 @@
Rust ve egui ile yeniden yazılmış **Pisi GNU/Linux** kurulum aracı. YALI, canlı ortamdan sistemi kurar, paketleri kurar ve temel yapılandırmayı gerçekleştirir. Orijinal Python/Qt sürümünün modern, hızlı ve bakımı kolay bir yeniden yazımıdır.
- [Changelog](CHANGELOG.md)
- [English README](README_en.md)
## Özellikler
- **11 adımlı kurulum sihirbazı**: Hoş Geldiniz → Konum → Klavye → Ağ → Disk Bölümleme → Kullanıcılar → Bootloader → Görüntü Yöneticisi → Özet → Kurulum → Bitir
- **10 adımlı kurulum sihirbazı**: Hoş Geldiniz → Konum → Klavye → Ağ → Disk Bölümleme → Kullanıcılar → Bootloader → Özet → Kurulum → Bitir
- **İki mod**: Grafik arayüz (varsayılan) ve cevap dosyasıyla otomatik kurulum (`--auto-install answer.toml`)
- **Demo modu**: Sistem kontrollerini atlar, kurulumu simüle eder (`--demo`)
- **Disk bölümleme**: Otomatik (GPT/MBR) veya manuel, LVM ve LUKS şifreleme desteği
@@ -56,7 +59,6 @@ sudo cargo run --release -- --auto-install answer.toml
│ │ ├── partition.rs # Disk bölümleme (otomatik + manuel + LVM)
│ │ ├── users.rs # Kullanıcı hesabı oluşturma
│ │ ├── bootloader.rs # GRUB yapılandırması
│ │ ├── display_manager.rs # Görüntü yöneticisi ve masaüstü seçimi
│ │ ├── summary.rs # Kurulum özeti
│ │ ├── execution.rs # Kurulum yürütme (ilerleme çubuğu, log)
│ │ ├── finish.rs # Tamamlanma ekranı
@@ -86,12 +88,11 @@ sudo cargo run --release -- --auto-install answer.toml
3. **Klavye** — Düzen/varyant seçimi, arama, canlı önizleme (setxkbmap)
4. **Ağ** — WiFi tarama, bağlantı, proxy
5. **Disk Bölümleme** — Otomatik (rehberli) veya manuel, LVM, LUKS
6. **Kullanıcılar** — Hesap oluşturma, parola gücü göstergesi, root parolası
6. **Kullanıcılar** — Hesap oluşturma, parola gücü göstergesi, root parolası, otomatik oturum açma
7. **Bootloader** — GRUB aygıtı, zaman aşımı, parola, çekirdek parametreleri
8. **Görüntü Yöneticisi** — SDDM/LightDM/GDM, masaüstü ortamı, otomatik oturum
9. **Özet**Tüm seçimlerin gözden geçirilmesi
10. **Kurulum**Slayt + ilerleme çubuğu + canlı log
11. **Bitir** — Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön
8. **Özet** — Tüm seçimlerin gözden geçirilmesi
9. **Kurulum**Slayt + ilerleme çubuğu + canlı log
10. **Bitir**Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön
## Bağımlılıklar
+114
View File
@@ -0,0 +1,114 @@
# YALI (Yet Another Linux Installer) — Rust / egui
Rust ve egui ile yeniden yazılmış **Pisi GNU/Linux** kurulum aracı. YALI, canlı ortamdan sistemi kurar, paketleri kurar ve temel yapılandırmayı gerçekleştirir. Orijinal Python/Qt sürümünün modern, hızlı ve bakımı kolay bir yeniden yazımıdır.
## Özellikler
- **11 adımlı kurulum sihirbazı**: Hoş Geldiniz → Konum → Klavye → Ağ → Disk Bölümleme → Kullanıcılar → Bootloader → Görüntü Yöneticisi → Özet → Kurulum → Bitir
- **İki mod**: Grafik arayüz (varsayılan) ve cevap dosyasıyla otomatik kurulum (`--auto-install answer.toml`)
- **Demo modu**: Sistem kontrollerini atlar, kurulumu simüle eder (`--demo`)
- **Disk bölümleme**: Otomatik (GPT/MBR) veya manuel, LVM ve LUKS şifreleme desteği
- **Ağ yapılandırması**: WiFi tarama (nmcli), proxy ayarları
- **Önyükleyici**: GRUB aygıt/parola/zaman aşımı ayarları, çekirdek parametreleri
- **Kurtarma modu**: Chroot, GRUB yeniden kurulumu, Pisi geçmişe dönme
- **OEM modu**: Üretici ön kurulumu için ön tanımlı ayarlar
- **Uluslararasılaştırma**: Türkçe ve İngilizce (rust-i18n ile TOML locale dosyaları)
- **Markalama sistemi**: Çalışma zamanında yüklenebilir `branding.toml` ile logo, slayt, renk ayarları
- **Tema**: Koyu/açık tema, atomik renk yönetimi, özel gradient butonlar
- **Asenkron iş kuyruğu**: Tüm kurulum adımları ayrı `tokio` runtime'ında çalışır, `mpsc` kanalı ile UI güncellenir
- **Sistem kontrolleri**: RAM, disk alanı, internet, UEFI, CPU, NVMe gibi 8 ön kontrol
- **Slayt gösterisi**: 9 masaüstü ortamı için özel markalı slaytlar
- **Demo oturum temizliği**: Canlı ortam artifact'lerini kaldırır, üretim deposu ekler
## Kullanım
```bash
# Normal kurulum (root yetkisi gerekir)
sudo cargo run --release
# Demo modu (root gerekmez)
cargo run --release -- --demo
# Otomatik kurulum (cevap dosyası ile)
sudo cargo run --release -- --auto-install answer.toml
```
## Proje Yapısı
```
├── src/
│ ├── main.rs # Giriş noktası, eframe App, GUI düzeni
│ ├── installer.rs # InstallerStep trait'i, GlobalState, WelcomeStep
│ ├── funct.rs # Paylaşılan UI yardımcıları (form alanları, dil seçici)
│ ├── branding.rs # Branding.toml yükleyici (logo, slayt, tema, OEM)
│ ├── ui/
│ │ ├── theme.rs # Renkler, fontlar, Visuals, section_heading, error/warning/success kutuları
│ │ ├── buttons.rs # Gradient butonlar (primary, secondary, danger, vb.)
│ │ ├── combobox.rs # ComboBox yardımcıları (show_combo_box, selectable_value)
│ │ ├── combobox_stil.rs # ComboBox görsel sabitleri (weak_bg, border, text renkleri)
│ │ ├── slideshow.rs # Otomatik ilerleyen slayt gösterisi
│ │ └── error_screen.rs # Hata ekranı (log görüntüleyici, kopyala, yeniden dene)
│ ├── steps/
│ │ ├── location.rs # Konum/saat dilimi + tarih/saat
│ │ ├── license.rs # Lisans sözleşmesi
│ │ ├── keyboard.rs # Klavye düzeni/varyant seçimi
│ │ ├── network.rs # Ağ yapılandırması (WiFi, proxy)
│ │ ├── partition.rs # Disk bölümleme (otomatik + manuel + LVM)
│ │ ├── users.rs # Kullanıcı hesabı oluşturma
│ │ ├── bootloader.rs # GRUB yapılandırması
│ │ ├── display_manager.rs # Görüntü yöneticisi ve masaüstü seçimi
│ │ ├── summary.rs # Kurulum özeti
│ │ ├── execution.rs # Kurulum yürütme (ilerleme çubuğu, log)
│ │ ├── finish.rs # Tamamlanma ekranı
│ │ ├── netinstall.rs # Ek paket grubu seçimi
│ │ └── rescue.rs # Kurtarma modu (chroot, GRUB, Pisi takeback)
│ ├── jobs/mod.rs # Asenkron iş kuyruğu ve tüm kurulum işleri
│ └── autoinstall/
│ ├── mod.rs # Cevap dosyası yükleme ve state uygulama
│ └── checker.rs # Sistem ön kontrolleri
├── locales/
│ ├── tr.toml # Türkçe çeviriler
│ └── en.toml # İngilizce çeviriler
├── assets/
│ ├── icons/ # UI ikonları (YALI_icon_LIGHT.svg, language-icon.svg, vb.)
│ ├── flags/ # Ülke bayrakları (~250 SVG)
│ └── *.png # Slayt ve logo görselleri
├── branding.toml # Varsayılan markalama yapılandırması
├── branding_en.toml # İngilizce markalama
├── branding_tr.toml # Türkçe markalama
└── answer-example.toml # Örnek cevap dosyası
```
## Kurulum İş Akışı
1. **Hoş Geldiniz** — Dil seçimi, sistem kontrolleri, kurtarma modu
2. **Konum / Zaman** — Saat dilimi seçimi (bölge/alan çift ComboBox), GeoIP, NTP veya manuel tarih/saat
3. **Klavye** — Düzen/varyant seçimi, arama, canlı önizleme (setxkbmap)
4. **Ağ** — WiFi tarama, bağlantı, proxy
5. **Disk Bölümleme** — Otomatik (rehberli) veya manuel, LVM, LUKS
6. **Kullanıcılar** — Hesap oluşturma, parola gücü göstergesi, root parolası
7. **Bootloader** — GRUB aygıtı, zaman aşımı, parola, çekirdek parametreleri
8. **Görüntü Yöneticisi** — SDDM/LightDM/GDM, masaüstü ortamı, otomatik oturum
9. **Özet** — Tüm seçimlerin gözden geçirilmesi
10. **Kurulum** — Slayt + ilerleme çubuğu + canlı log
11. **Bitir** — Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön
## Bağımlılıklar
| Bağımlılık | Sürüm | Amaç |
|---|---|---|
| `eframe` | 0.27 (wgpu) | egui çatısı, WebGPU çıktısı |
| `egui_extras` | 0.27 (image, file, svg) | Görsel yükleme, dosya diyalogları, SVG |
| `image` | 0.24 | PNG kod çözme |
| `serde` / `toml` | 1.0 / 0.8 | Branding ve cevap dosyası serileştirme |
| `zbus` | 4.0 | D-Bus iletişimi (COMAR) |
| `tokio` | 1.0 (full) | Asenkron iş kuyruğu |
| `sysinfo` | 0.30 | Sistem bilgisi (RAM, disk, OS) |
| `rust-i18n` | 3.1 | Uluslararasılaştırma |
| `chrono` | 0.4 | Tarih/saat işlemleri |
## Lisans
GNU General Public License v3.0
Bu proje Pisi GNU/Linux ekibi tarafından geliştirilmektedir.
+112
View File
@@ -0,0 +1,112 @@
# YALI (Yet Another Linux Installer) — Rust / egui
A rewritten **Pisi GNU/Linux** installation tool in Rust and egui. YALI installs the system from the live environment, installs packages, and performs basic configuration. It is a modern, fast, and maintainable rewrite of the original Python/Qt version.
## Features
- **10-step installation wizard**: Welcome → Location → Keyboard → Network → Disk Partitioning → Users → Bootloader → Summary → Installation → Finish
- **Two modes**: Graphical interface (default) and automatic installation with answer file (`--auto-install answer.toml`)
- **Demo mode**: Skips system checks, simulates installation (`--demo`)
- **Disk partitioning**: Automatic (GPT/MBR) or manual, LVM and LUKS encryption support
- **Network configuration**: WiFi scanning (nmcli), proxy settings
- **Bootloader**: GRUB device/password/timeout settings, kernel parameters
- **Rescue mode**: Chroot, GRUB reinstallation, Pisi rollback
- **OEM mode**: Predefined settings for manufacturer pre-installation
- **Internationalization**: Turkish and English (with rust-i18n via TOML locale files)
- **Branding system**: Runtime-loadable `branding.toml` for logo, slides, color settings
- **Theme**: Dark/light theme, atomic color management, custom gradient buttons
- **Async job queue**: All installation steps run in separate `tokio` runtime, UI updated via `mpsc` channel
- **System checks**: 8 pre-checks including RAM, disk space, internet, UEFI, CPU, NVMe
- **Slideshow**: Custom branded slides for 9 desktop environments
- **Demo session cleanup**: Removes live environment artifacts, adds production repository
## Usage
```bash
# Normal installation (requires root)
sudo cargo run --release
# Demo mode (no root required)
cargo run --release -- --demo
# Automatic installation (with answer file)
sudo cargo run --release -- --auto-install answer.toml
```
## Project Structure
```
├── src/
│ ├── main.rs # Entry point, eframe App, GUI layout
│ ├── installer.rs # InstallerStep trait, GlobalState, WelcomeStep
│ ├── funct.rs # Shared UI helpers (form fields, language selector)
│ ├── branding.rs # Branding.toml loader (logo, slides, theme, OEM)
│ ├── ui/
│ │ ├── theme.rs # Colors, fonts, Visuals, section_heading, error/warning/success boxes
│ │ ├── buttons.rs # Gradient buttons (primary, secondary, danger, etc.)
│ │ ├── combobox.rs # ComboBox helpers (show_combo_box, selectable_value)
│ │ ├── combobox_stil.rs # ComboBox visual constants (weak_bg, border, text colors)
│ │ ├── slideshow.rs # Auto-advancing slideshow
│ │ └── error_screen.rs # Error screen (log viewer, copy, retry)
│ ├── steps/
│ │ ├── location.rs # Location/timezone + date/time
│ │ ├── license.rs # License agreement
│ │ ├── keyboard.rs # Keyboard layout/variant selection
│ │ ├── network.rs # Network configuration (WiFi, proxy)
│ │ ├── partition.rs # Disk partitioning (automatic + manual + LVM)
│ │ ├── users.rs # User account creation
│ │ ├── bootloader.rs # GRUB configuration
│ │ ├── summary.rs # Installation summary
│ │ ├── execution.rs # Installation execution (progress bar, log)
│ │ ├── finish.rs # Completion screen
│ │ ├── netinstall.rs # Extra package group selection
│ │ └── rescue.rs # Rescue mode (chroot, GRUB, Pisi takeback)
│ ├── jobs/mod.rs # Async job queue and all installation jobs
│ └── autoinstall/
│ ├── mod.rs # Answer file loading and state application
│ └── checker.rs # System pre-checks
├── locales/
│ ├── tr.toml # Turkish translations
│ └── en.toml # English translations
├── assets/
│ ├── icons/ # UI icons (YALI_icon_LIGHT.svg, language-icon.svg, etc.)
│ ├── flags/ # Country flags (~250 SVG)
│ └── *.png # Slide and logo images
├── branding.toml # Default branding configuration
├── branding_en.toml # English branding
├── branding_tr.toml # Turkish branding
└── answer-example.toml # Example answer file
```
## Installation Workflow
1. **Welcome** — Language selection, system checks, rescue mode
2. **Location / Time** — Timezone selection (region/area dual ComboBox), GeoIP, NTP or manual date/time
3. **Keyboard** — Layout/variant selection, search, live preview (setxkbmap)
4. **Network** — WiFi scanning, connection, proxy
5. **Disk Partitioning** — Automatic (guided) or manual, LVM, LUKS
6. **Users** — Account creation, password strength indicator, root password, autologin
7. **Bootloader** — GRUB device, timeout, password, kernel parameters
8. **Summary** — Review all selections
9. **Installation** — Slideshow + progress bar + live log
10. **Finish** — Successful installation, reboot or return to live desktop
## Dependencies
| Dependency | Version | Purpose |
|---|---|---|
| `eframe` | 0.27 (wgpu) | egui framework, WebGPU backend |
| `egui_extras` | 0.27 (image, file, svg) | Image loading, file dialogs, SVG |
| `image` | 0.24 | PNG decoding |
| `serde` / `toml` | 1.0 / 0.8 | Branding and answer file serialization |
| `zbus` | 4.0 | D-Bus communication (COMAR) |
| `tokio` | 1.0 (full) | Async job queue |
| `sysinfo` | 0.30 | System information (RAM, disk, OS) |
| `rust-i18n` | 3.1 | Internationalization |
| `chrono` | 0.4 | Date/time operations |
## License
GNU General Public License v3.0
This project is developed by the Pisi GNU/Linux team.
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 40 KiB

+4
View File
@@ -61,6 +61,10 @@ password_label = "Password (at least 8 characters...)"
password_confirm_label = "Confirm password"
hostname_label = "Computer name"
username_invalid = "Invalid username. Must start with a lowercase letter or underscore, max 32 characters."
username_too_long = "Username too long (maximum 32 characters)"
username_forbidden = "This username is reserved by the system and is forbidden"
username_invalid_start = "Username must start with a lowercase letter or underscore"
username_invalid_chars = "Only lowercase letters, digits, underscore and hyphen are allowed"
hostname_invalid = "Invalid hostname. Only letters, digits, and hyphens are allowed."
passwords_no_match = "Passwords do not match"
+4
View File
@@ -61,6 +61,10 @@ password_label = "Parola (en az 8 karakter olmalıdır...)"
password_confirm_label = "Parola (tekrar)"
hostname_label = "Bilgisayar adı"
username_invalid = "Geçersiz kullanıcı adı. Küçük harf veya alt çizgi ile başlamalı, en fazla 32 karakter."
username_too_long = "Kullanıcı adı çok uzun (maksimum 32 karakter)"
username_forbidden = "Bu kullanıcı adı sistem tarafından kullanılıyor ve yasaklı"
username_invalid_start = "Kullanıcı adı küçük harf veya alt çizgi ile başlamalı"
username_invalid_chars = "Sadece küçük harf, rakam, alt çizgi ve tire kullanılabilir"
hostname_invalid = "Geçersiz bilgisayar adı. Yalnızca harf, rakam ve tire içerebilir."
passwords_no_match = "Parolalar eşleşmiyor"
+1
View File
@@ -127,6 +127,7 @@ pub fn language_selector_ui(ui: &mut eframe::egui::Ui, state: &mut crate::instal
ui.add(
eframe::egui::Image::new(eframe::egui::include_image!("../assets/icons/language-icon.svg"))
.max_width(40.0)
.tint(crate::ui::theme::c_text())
);
});
+4 -2
View File
@@ -207,6 +207,7 @@ pub struct GlobalState {
pub current_step: usize,
pub available_disks: Vec<DiskInfo>,
pub install_progress: f32,
pub install_sub_progress: f32,
pub install_log: Vec<String>,
pub install_current_file: Option<String>,
pub demo_mode: bool,
@@ -274,10 +275,11 @@ impl Default for GlobalState {
current_step: 0,
available_disks: Vec::new(),
install_progress: 0.0,
install_sub_progress: 0.0,
install_log: Vec::new(),
install_current_file: None,
demo_mode: false,
is_dark: false,
is_dark: true,
rescue_mode: false,
bootloader_device: String::new(),
bootloader_timeout: 5,
@@ -301,7 +303,7 @@ impl Default for GlobalState {
oem_password_changed: false,
selected_package_groups: Vec::new(),
display_manager: "sddm".to_string(),
autologin: true,
autologin: false,
desktop_environment: detect_desktop_environment(),
branding_icons: std::collections::HashMap::new(),
branding_pisi_dark: String::new(),
+109 -6
View File
@@ -13,6 +13,7 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use rust_i18n::t;
@@ -27,6 +28,10 @@ pub enum InstallMessage {
Log(String),
/// Genel ilerleme; 0.01.0 arasında.
Progress(f32),
/// Alt ilerleme (ör: rsync içindeki dosya kopyalama); 0.01.0 arasında.
SubProgress(f32),
/// Kopyalanmakta olan dosya adı.
CurrentFile(String),
/// Tüm işler başarıyla tamamlandı.
Done,
/// Bir iş başarısız oldu; kurulum durdu.
@@ -54,6 +59,14 @@ impl UiSender {
pub fn log(&self, s: impl Into<String>) {
self.send(InstallMessage::Log(s.into()));
}
pub fn sub_progress(&self, p: f32) {
self.send(InstallMessage::SubProgress(p));
}
pub fn current_file(&self, f: impl Into<String>) {
self.send(InstallMessage::CurrentFile(f.into()));
}
}
// ─────────────────────────────────────────────
@@ -179,8 +192,74 @@ impl Job for CopyFilesJob {
args.push(self.source.clone());
args.push(self.target.clone());
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
run_cmd("rsync", &arg_refs).await?;
if DEMO_MODE.load(Ordering::Relaxed) {
ui.log("rsync /run/livecd/squashfs-root/ → /mnt (DEMO)");
let steps = 20;
for i in 0..steps {
let p = (i + 1) as f32 / steps as f32;
ui.current_file(format!("/usr/share/doc/{}/README.md", (i * 137) % 50));
ui.sub_progress(p);
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
}
return Ok(());
}
let mut child = Command::new("rsync")
.args(&args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| format!("rsync çalıştırılamadı: {}", e))?;
let stderr = child.stderr.take()
.ok_or_else(|| "rsync stderr alınamadı".to_string())?;
let reader = BufReader::new(stderr);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await
.map_err(|e| format!("rsync çıktısı okunamadı: {}", e))?
{
let trimmed = line.trim();
// rsync --info=progress2 format (newlines when piped):
// 1,024 100% 512.00kB/s 0:00:00 (xfr#5, to-chk=99/105)
// Extract xfr# and to-chk for sub-progress
if let Some(xfr_pos) = trimmed.find("xfr#") {
let after_xfr = &trimmed[xfr_pos + 4..];
let xfr_end = after_xfr.find(|c: char| !c.is_ascii_digit()).unwrap_or(after_xfr.len());
let xfr: u64 = after_xfr[..xfr_end].parse().unwrap_or(0);
if let Some(to_chk_pos) = trimmed.find("to-chk=") {
let after_to = &trimmed[to_chk_pos + 7..];
let slash_pos = after_to.find('/').unwrap_or(after_to.len());
let total_end = after_to[slash_pos + 1..]
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(after_to[slash_pos + 1..].len());
let total: u64 = after_to[slash_pos + 1..][..total_end].parse().unwrap_or(1);
let p = if total > 0 {
(xfr as f32) / (total as f32)
} else {
0.0
};
ui.sub_progress(p.min(1.0));
}
}
// Extract filename: lines starting with a path (not a number/space)
let first_char = trimmed.chars().next();
if let Some('/') = first_char {
ui.current_file(trimmed.to_string());
} else if trimmed.starts_with("./") || trimmed.starts_with("..") {
ui.current_file(trimmed.to_string());
}
}
let status = child.wait().await
.map_err(|e| format!("rsync beklenemedi: {}", e))?;
if !status.success() {
return Err(format!("rsync çıkış kodu: {:?}", status.code()));
}
Ok(())
}
@@ -1186,10 +1265,22 @@ impl Job for ConfigureDisplayManagerJob {
match dm.as_str() {
"sddm" => {
let conf = format!("{}/usr/lib/sddm/sddm.conf.d/sddm.conf", self.mount);
let session = match self.desktop_environment.as_str() {
"plasma" => "plasmax11.desktop",
"xfce" => "xfce.desktop",
"gnome" => "gnome.desktop",
"cinnamon" => "cinnamon.desktop",
"budgie" => "budgie-desktop.desktop",
"lxde" => "LXDE.desktop",
"lxqt" => "lxqt.desktop",
"mate" => "mate.desktop",
"lumina" => "lumina-desktop.desktop",
_ => "plasmax11.desktop",
};
let content = if self.autologin {
format!(
"[Autologin]\nUser={}\nSession=plasmax11.desktop\n\n[Theme]\nCurrent=pisilinux24\n",
self.username
"[Autologin]\nUser={}\nSession={}\n\n[Theme]\nCurrent=pisilinux24\n",
self.username, session
)
} else {
"[Autologin]\nUser=\nSession=\n\n[Theme]\nCurrent=pisilinux24\n".to_string()
@@ -1220,10 +1311,22 @@ impl Job for ConfigureDisplayManagerJob {
let dir = format!("{}/etc/gdm", self.mount);
let _ = tokio::fs::create_dir_all(&dir).await;
let conf = format!("{}/etc/gdm/custom.conf", self.mount);
let session = match self.desktop_environment.as_str() {
"plasma" => "plasmax11.desktop",
"xfce" => "xfce.desktop",
"gnome" => "gnome.desktop",
"cinnamon" => "cinnamon.desktop",
"budgie" => "budgie-desktop.desktop",
"lxde" => "LXDE.desktop",
"lxqt" => "lxqt.desktop",
"mate" => "mate.desktop",
"lumina" => "lumina-desktop.desktop",
_ => "gnome.desktop",
};
let content = if self.autologin {
format!(
"[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\n",
self.username
"[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\nDefaultSession={}\n",
self.username, session
)
} else {
"[daemon]\n".to_string()
+23 -26
View File
@@ -7,7 +7,7 @@ mod branding;
mod funct;
use installer::{GlobalState, InstallerStep, WelcomeStep};
use steps::{LocationStep, KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep, DisplayManagerStep};
use steps::{LocationStep, KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep, BootloaderStep, NetworkStep};
use eframe::egui;
use rust_i18n::t;
@@ -48,8 +48,8 @@ fn run_gui(demo_mode: bool) -> eframe::Result<()> {
Box::new(move |cc| {
// Görsel yükleyicileri etkinleştir (PNG desteği)
egui_extras::install_image_loaders(&cc.egui_ctx);
ui::theme::set_theme_mode(false);
ui::theme::apply(&cc.egui_ctx, false);
ui::theme::set_theme_mode(true);
ui::theme::apply(&cc.egui_ctx, true);
let mut app = YaliApp::default();
app.state.demo_mode = demo_mode;
Box::new(app)
@@ -188,6 +188,8 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
print!("\r {}: [{:50}] {:.0}%", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
Ok(InstallMessage::SubProgress(_)) => {}
Ok(InstallMessage::CurrentFile(_)) => {}
Ok(InstallMessage::Done) => {
println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description"));
break;
@@ -229,7 +231,6 @@ impl Default for YaliApp {
Box::new(PartitionStep::default()),
Box::new(UsersStep::default()),
Box::new(BootloaderStep::default()),
Box::new(DisplayManagerStep::default()),
Box::new(SummaryStep),
Box::new(ExecutionStep::default()),
Box::new(FinishStep),
@@ -407,24 +408,24 @@ impl eframe::App for YaliApp {
}
// Alt kısım: tema değiştirme
// ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
// ui.add_space(6.0);
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.add_space(6.0);
// let theme_icon = if self.state.is_dark { "☀" } else { "🌙" };
// let theme_label = if self.state.is_dark { "Light" } else { "Dark" };
// if ui.add(
// egui::Button::new(
// egui::RichText::new(format!("{} {}", theme_icon, theme_label))
// .size(12.0)
// )
// .rounding(egui::Rounding::same(6.0))
// .min_size(egui::Vec2::new(100.0, 28.0))
// ).clicked() {
// self.state.is_dark = !self.state.is_dark;
// ui::theme::set_theme_mode(self.state.is_dark);
// ui::theme::apply(ctx, self.state.is_dark);
// }
// });
let theme_icon = if self.state.is_dark { "" } else { "🌙" };
let theme_label = if self.state.is_dark { "Light" } else { "Dark" };
if ui.add(
egui::Button::new(
egui::RichText::new(format!("{} {}", theme_icon, theme_label))
.size(12.0)
)
.rounding(egui::Rounding::same(6.0))
.min_size(egui::Vec2::new(100.0, 28.0))
).clicked() {
self.state.is_dark = !self.state.is_dark;
ui::theme::set_theme_mode(self.state.is_dark);
ui::theme::apply(ctx, self.state.is_dark);
}
});
});
// ── Alt panel: navigasyon ────────────────────────────
@@ -455,11 +456,7 @@ impl eframe::App for YaliApp {
// if !is_exec && !is_finish && ui::theme::danger_button(ui, &t!("cancel")).clicked() {
// self.show_cancel_dialog = true;
// };
let next_clicked = if is_valid {
ui::theme::primary_button(ui, &next_label).clicked()
} else {
ui::theme::secondary_button(ui, &next_label).clicked()
};
let next_clicked = ui::theme::primary_button_enabled(ui, &next_label, is_valid).clicked();
if next_clicked {
self.state.current_step += 1;
}
+31 -28
View File
@@ -2,6 +2,7 @@ use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
use crate::funct::{form_label, form_input_text, form_input_password};
#[derive(Default)]
pub struct BootloaderStep {
@@ -26,16 +27,26 @@ impl InstallerStep for BootloaderStep {
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("bootloader_title"));
ui.label(t!("bootloader_description"));
theme::section_heading(ui, &t!("bootloader_title"));
ui.label(
egui::RichText::new(t!("bootloader_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(12.0);
egui::Grid::new("bootloader_grid")
.num_columns(2)
.spacing([12.0, 10.0])
// ─── Önyükleyici Formu ──────────────────────────────────
egui::Frame::group(ui.style())
.fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::symmetric(10.0, 10.0))
.show(ui, |ui| {
ui.set_width(ui.available_width());
ui.vertical(|ui| {
ui.add_space(10.0);
// Hedef aygıt
ui.label(t!("bootloader_device_label"));
form_label(ui, &t!("bootloader_device_label"));
ui.horizontal(|ui| {
let mut device_list: Vec<String> = state.available_disks.iter()
.map(|d| d.name.clone())
@@ -53,50 +64,42 @@ impl InstallerStep for BootloaderStep {
theme::selectable_value(ui, &mut state.bootloader_device, dev.clone(), dev.as_str(), w);
}
});
});
ui.label(
egui::RichText::new(t!("bootloader_device_hint"))
.size(11.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
.color(theme::c_text_dim()),
);
});
ui.end_row();
ui.add_space(6.0);
// Timeout
ui.label(t!("bootloader_timeout_label"));
form_label(ui, &t!("bootloader_timeout_label"));
ui.horizontal(|ui| {
ui.add(egui::Slider::new(&mut state.bootloader_timeout, 0..=60)
.text(t!("bootloader_timeout_suffix")));
});
ui.end_row();
ui.add_space(6.0);
// GRUB şifresi
ui.label(t!("bootloader_password_label"));
form_label(ui, &t!("bootloader_password_label"));
ui.horizontal(|ui| {
if self.show_password {
ui.text_edit_singleline(&mut state.bootloader_password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.bootloader_password).password(true));
}
if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() {
self.show_password = !self.show_password;
}
if !state.bootloader_password.is_empty() {
ui.colored_label(egui::Color32::from_rgb(200, 180, 40), "");
}
let show_pwd = &mut self.show_password;
form_input_password(ui, &mut state.bootloader_password, show_pwd);
});
ui.end_row();
ui.add_space(6.0);
// Kernel parametreleri
ui.label(t!("bootloader_kernel_label"));
form_label(ui, &t!("bootloader_kernel_label"));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.kernel_options);
form_input_text(ui, &mut state.kernel_options);
});
ui.label(
egui::RichText::new(t!("bootloader_kernel_hint"))
.size(11.0)
.color(egui::Color32::from_rgb(120, 120, 140)),
.color(theme::c_text_dim()),
);
});
ui.end_row();
});
}
+17
View File
@@ -40,6 +40,7 @@ impl InstallerStep for ExecutionStep {
self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone()));
state.install_progress = 0.0;
state.install_sub_progress = 0.0;
state.install_log.clear();
state.install_current_file = None;
state.install_log.push(t!("install_starting").to_string());
@@ -182,6 +183,12 @@ impl InstallerStep for ExecutionStep {
Ok(InstallMessage::Progress(p)) => {
state.install_progress = p;
}
Ok(InstallMessage::SubProgress(p)) => {
state.install_sub_progress = p;
}
Ok(InstallMessage::CurrentFile(f)) => {
state.install_current_file = Some(f);
}
Ok(InstallMessage::Done) => {
state.install_progress = 1.0;
self.finished = true;
@@ -305,6 +312,16 @@ impl InstallerStep for ExecutionStep {
.animate(running)
.desired_height(10.0),
);
if state.install_sub_progress > 0.0 {
ui.add_space(4.0);
ui.add(
egui::ProgressBar::new(state.install_sub_progress)
.animate(running)
.desired_height(6.0)
.fill(theme::c_accent_dim()),
);
}
});
// Durum etiketi
+139 -38
View File
@@ -1,4 +1,4 @@
use eframe::egui::{self, Color32};
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
@@ -92,6 +92,7 @@ fn split_code_desc(line: &str) -> Option<(&str, &str)> {
Some((code, desc))
}
}
fn layout_display_name(layout: &str, layouts: &[(String, String)]) -> String {
layouts
.iter()
@@ -112,7 +113,6 @@ fn variant_display_name(
.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");
@@ -141,7 +141,6 @@ impl InstallerStep for KeyboardStep {
}
fn on_enter(&mut self, state: &mut GlobalState) {
// Varsayılan düzen sistemi diline göre seçilir
if state.keyboard_layout.is_empty() {
state.keyboard_layout = if state.language == "tr" {
"tr".to_string()
@@ -162,64 +161,166 @@ impl InstallerStep for KeyboardStep {
ui.add_space(12.0);
// ── Klavye görseli (xkbcommon ile çizilir, temaya uyumlu)
if let Some(kle) = crate::ui::kle_svg::compile_layout(&state.keyboard_layout, &state.keyboard_variant) {
// ── Klavye görseli ───────────────────────────────────
let kle = crate::ui::kle_svg::compile_layout(&state.keyboard_layout, &state.keyboard_variant)
.unwrap_or_else(|| crate::ui::kle_svg::empty_layout());
let max = egui::Vec2::new(ui.available_width(), ui.available_height().max(100.0));
crate::ui::kle_svg::draw_layout(ui, &kle, max);
}
ui.add_space(12.0);
// ── Layout ve Varyant ComboBox'ları yan yana ──────────────
let available_variants: Vec<_> = self
.variants
.iter()
.filter(|(layout, _, _)| layout == &state.keyboard_layout)
.collect();
let current_variant_name = if state.keyboard_variant.is_empty() {
t!("keyboard_variant_default").to_string()
} else {
let vn = variant_display_name(&state.keyboard_layout, &state.keyboard_variant, &self.variants);
if vn.is_empty() { state.keyboard_variant.clone() } else { vn }
// ── Label genişlik hesaplama ──────────────────────────
let layout_label = t!("keyboard_layout_label").to_string();
let variant_label = t!("keyboard_variant_label").to_string();
let label_w = ui.fonts(|f| {
let measure = |s: &str| -> f32 {
f.layout_no_wrap(s.to_string(), egui::FontId::proportional(14.0), egui::Color32::WHITE)
.size().x
};
let current_layout_name = layout_display_name(&state.keyboard_layout, &self.layouts);
measure(&layout_label).max(measure(&variant_label)).ceil()
}) + 16.0;
// ── Düzen (Layout) ────────────────────────────────────
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.add_space(6.0);
ui.label(egui::RichText::new(t!("keyboard_layout_label")).strong().color(theme::c_text()));
});
ui.allocate_ui_with_layout(
egui::Vec2::new(label_w, ui.available_height()),
egui::Layout::left_to_right(egui::Align::Center),
|ui| {
ui.label(egui::RichText::new(&layout_label).strong().color(theme::c_text()));
},
);
ui.add_space(8.0);
let lw = ui.available_width().min(200.0);
theme::show_combo_box(ui, "kbd_layout", current_layout_name.as_str(), lw, |ui, w| {
let lw = ui.available_width();
let current_layout = layout_display_name(&state.keyboard_layout, &self.layouts);
theme::show_combo_box(ui, "kbd_layout", &current_layout, lw, |ui, w| {
let mut pressed_char = None;
ui.input(|i| {
for event in &i.events {
if let egui::Event::Text(text) = event {
pressed_char = Some(text.to_lowercase());
}
}
});
for (code, display) in &self.layouts {
if theme::selectable_value(ui, &mut state.keyboard_layout, code.clone(), display.as_str(), w).clicked() {
let is_selected = state.keyboard_layout == *code;
let is_target = if let Some(ref c) = pressed_char {
display.to_lowercase().starts_with(c)
} else {
false
};
if is_target {
state.keyboard_layout = code.clone();
if !self.variants.iter().any(|(layout, _, _)| layout == code) {
state.keyboard_variant.clear();
}
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
}
let response = theme::selectable_value(ui, &mut state.keyboard_layout, code.clone(), display.as_str(), w);
if response.clicked() {
if !self.variants.iter().any(|(layout, _, _)| layout == code) {
state.keyboard_variant.clear();
}
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
}
if is_target || (is_selected && pressed_char.is_none()) {
response.scroll_to_me(None);
}
}
});
});
ui.add_space(8.0);
// ── Varyant ───────────────────────────────────────────
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.allocate_ui_with_layout(
egui::Vec2::new(label_w, ui.available_height()),
egui::Layout::left_to_right(egui::Align::Center),
|ui| {
ui.label(egui::RichText::new(&variant_label).strong().color(theme::c_text()));
},
);
ui.add_space(8.0);
let vw = ui.available_width();
let available_variants: Vec<(String, String)> = self
.variants
.iter()
.filter(|(layout, _, _)| layout == &state.keyboard_layout)
.map(|(_, v_code, display)| (v_code.clone(), display.clone()))
.collect();
let current_variant = if state.keyboard_variant.is_empty() {
t!("keyboard_variant_default").to_string()
} else {
let vn = variant_display_name(&state.keyboard_layout, &state.keyboard_variant, &self.variants);
if vn.is_empty() { state.keyboard_variant.clone() } else { vn }
};
let default_label = t!("keyboard_variant_default").to_string();
theme::show_combo_box(ui, "kbd_variant", &current_variant, vw, |ui, w| {
let mut pressed_char = None;
ui.input(|i| {
for event in &i.events {
if let egui::Event::Text(text) = event {
pressed_char = Some(text.to_lowercase());
}
}
});
ui.add_space(16.0);
// Varsayılan Varyant Elemanı
let is_default_selected = state.keyboard_variant.is_empty();
let is_default_target = if let Some(ref c) = pressed_char {
default_label.to_lowercase().starts_with(c)
} else {
false
};
ui.vertical(|ui| {
ui.add_space(6.0);
ui.label(egui::RichText::new(t!("keyboard_variant_label")).strong().color(theme::c_text()));
});
ui.add_space(8.0);
let vw = ui.available_width().min(200.0);
theme::show_combo_box(ui, "kbd_variant", current_variant_name.as_str(), vw, |ui, w| {
let default_variant = String::new();
if theme::selectable_value(ui, &mut state.keyboard_variant, default_variant, &t!("keyboard_variant_default"), w).clicked() {
if is_default_target {
state.keyboard_variant.clear();
apply_system_keyboard_layout(&state.keyboard_layout, "");
}
for (_, variant, display) in &available_variants {
if theme::selectable_value(ui, &mut state.keyboard_variant, variant.clone(), display.as_str(), w).clicked() {
let def_response = theme::selectable_value(ui, &mut state.keyboard_variant, String::new(), &default_label, w);
if def_response.clicked() {
apply_system_keyboard_layout(&state.keyboard_layout, "");
}
if is_default_target || (is_default_selected && pressed_char.is_none()) {
def_response.scroll_to_me(None);
}
// Diğer Varyantlar
for (v_code, display) in &available_variants {
let is_selected = state.keyboard_variant == *v_code;
let is_target = if let Some(ref c) = pressed_char {
display.to_lowercase().starts_with(c)
} else {
false
};
if is_target {
state.keyboard_variant = v_code.clone();
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
}
let response = theme::selectable_value(ui, &mut state.keyboard_variant, v_code.clone(), display.as_str(), w);
if response.clicked() {
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant);
}
if is_target || (is_selected && pressed_char.is_none()) {
response.scroll_to_me(None);
}
}
});
});
@@ -228,7 +329,7 @@ impl InstallerStep for KeyboardStep {
ui.label(t!("keyboard_test_label"));
ui.add_space(4.0);
egui::Frame::group(ui.style())
.fill(Color32::from_rgb(255, 255, 255))
.fill(theme::c_keyboard_bg())
.rounding(6.0)
.show(ui, |ui| {
ui.add(
+1 -2
View File
@@ -7,7 +7,7 @@ pub mod partition;
pub mod bootloader;
pub mod network;
pub mod rescue;
pub mod display_manager;
//pub mod display_manager;
pub mod netinstall;
pub mod location;
@@ -20,5 +20,4 @@ pub use partition::PartitionStep;
pub use bootloader::BootloaderStep;
pub use network::NetworkStep;
pub use rescue::RescueStep;
pub use display_manager::DisplayManagerStep;
pub use location::LocationStep;
+135 -26
View File
@@ -6,16 +6,18 @@ use crate::funct::{ form_input_text, form_input_password, form_label, form_check
/// Kullanıcı adı, şifre, hostname ve yönetici (root) şifresi ayarları adımı.
pub struct UsersStep {
use_same_for_admin: bool,
autologin: bool,
show_password: bool,
show_password_confirm: bool,
show_root_password: bool, // <-- Eklendi
show_root_password_confirm: bool, // <-- Eklendi
show_root_password: bool,
show_root_password_confirm: bool,
}
impl Default for UsersStep {
fn default() -> Self {
Self {
use_same_for_admin: true,
autologin: false,
show_password: false,
show_password_confirm: false,
show_root_password: false,
@@ -30,16 +32,22 @@ impl Default for UsersStep {
/// - Boşlukları `-`'ye çevirir
/// - Geçersiz karakterleri temizler
fn derive_username(real_name: &str) -> String {
let mut name = real_name.to_lowercase();
name = name
let name = real_name
.replace('ş', "s")
.replace('Ş', "s")
.replace('ğ', "g")
.replace('Ğ', "g")
.replace('ı', "i")
.replace('İ', "i")
.replace('ö', "o")
.replace('Ö', "o")
.replace('ü', "u")
.replace('Ü', "u")
.replace('ç', "c")
.replace('Ç', "c")
.replace(' ', "-")
.replace('_', "-");
.replace('_', "-")
.to_lowercase();
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() {
@@ -54,17 +62,44 @@ fn derive_username(real_name: &str) -> String {
/// Kullanıcı adının geçerliliğini kontrol eder.
/// Linux kuralları: küçük harf veya alt çizgi ile başlamalı,
/// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli.
fn is_valid_username(s: &str) -> bool {
if s.is_empty() || s.len() > 32 {
return false;
/// Ayrıca sistem kullanıcı adları yasaklıdır.
/// Hata varsa hata mesajını döndürür, yoksa None döndürür.
fn validate_username(s: &str) -> Option<String> {
if s.is_empty() {
return None;
}
if s.len() > 32 {
return Some(t!("username_too_long").to_string());
}
// Yasaklı sistem kullanıcı adları
let forbidden = [
"root", "daemon", "bin", "sys", "sync", "games", "man", "lp",
"mail", "news", "uucp", "proxy", "www-data", "backup", "list",
"irc", "gnats", "nobody", "systemd-timesync", "systemd-network",
"systemd-resolve", "systemd-bus-proxy", "messagebus", "sshd",
"ftp", "apache", "nginx", "mysql", "postgres", "docker",
];
if forbidden.contains(&s) {
return Some(t!("username_forbidden").to_string());
}
let mut chars = s.chars();
// İlk karakter küçük harf veya alt çizgi olmalı
match chars.next() {
Some(c) if c.is_ascii_lowercase() || c == '_' => {}
_ => return false,
_ => return Some(t!("username_invalid_start").to_string()),
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
if !chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-') {
return Some(t!("username_invalid_chars").to_string());
}
None
}
/// Kullanıcı adının geçerliliğini kontrol eder (basit bool versiyonu).
fn is_valid_username(s: &str) -> bool {
validate_username(s).is_none()
}
/// Hostname kuralları: 1-63 karakter, yalnızca [a-zA-Z0-9-], tire ile başlayamaz/bitemez.
@@ -121,6 +156,9 @@ impl InstallerStep for UsersStep {
}
fn on_enter(&mut self, state: &mut GlobalState) {
// Autologin senkronizasyonu
self.autologin = state.autologin;
// 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);
@@ -155,7 +193,7 @@ impl InstallerStep for UsersStep {
// ─── Kullanıcı Bilgileri Formu ──────────────────────────────────
egui::Frame::group(ui.style())
.fill(egui::Color32::from_rgb(240, 243, 198))
.fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::symmetric(10.0, 10.0))
.show(ui, |ui| {
@@ -168,28 +206,55 @@ impl InstallerStep for UsersStep {
form_label(ui, &t!("real_name_label"));
{
let prev = state.real_name.clone();
ui.horizontal(|ui| {
form_input_text(ui, &mut state.real_name);
if !state.real_name.is_empty() {
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_success())
);
}else{
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_error())
);
}
});
if state.real_name != prev && !state.real_name.is_empty() {
state.username = derive_username(&state.real_name);
}
}
if !state.real_name.is_empty() {
ui.colored_label(theme::c_success(), "");
} else {
ui.colored_label(theme::c_error(), "");
}
ui.add_space(6.0);
// Kullanıcı adı (otomatik türetilir, düzenlenebilir)
form_label(ui, &t!("username_label"));
ui.horizontal(|ui| {
form_input_text(ui, &mut state.username);
if !state.username.is_empty() {
if is_valid_username(&state.username) {
ui.colored_label(theme::c_success(), "");
if let Some(_err) = validate_username(&state.username) {
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_error())
);
} else {
ui.colored_label(theme::c_warning(), t!("username_invalid"));
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_success())
);
}
}
});
if let Some(err) = validate_username(&state.username) {
ui.colored_label(theme::c_error(), err);
}
ui.add_space(6.0);
// Şifre
@@ -221,27 +286,50 @@ impl InstallerStep for UsersStep {
form_label(ui, &t!("password_confirm_label"));
{
let show_pwd = &mut self.show_password_confirm;
ui.horizontal(|ui| {
form_input_password(ui, &mut state.password_confirm, show_pwd);
}
if !state.password_confirm.is_empty() {
if state.password == state.password_confirm {
ui.colored_label(theme::c_success(), "");
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_success())
);
} else {
ui.colored_label(theme::c_warning(), t!("passwords_no_match"));
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_error())
);
}
}
});
}
if !state.password_confirm.is_empty() && state.password != state.password_confirm {
ui.colored_label(theme::c_warning(), t!("passwords_no_match"));
}
ui.add_space(6.0);
// Hostname
form_label(ui, &t!("hostname_label"));
ui.horizontal(|ui| {
form_input_text(ui, &mut state.hostname);
if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) {
ui.colored_label(theme::c_success(), "");
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_success())
);
} else {
ui.colored_label(theme::c_warning(), t!("hostname_invalid"));
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_error())
);
}
}
});
});
ui.add_space(10.0);
@@ -250,6 +338,10 @@ impl InstallerStep for UsersStep {
ui.add_space(14.0);
// ─── Otomatik Oturum Açma ───────────────────────────────────────
form_checkbox(ui, &mut self.autologin, &t!("display_manager_autologin"));
ui.add_space(8.0);
// ─── Yönetici (root) Ayarları ─────────────────────────────────────
form_checkbox(ui, &mut self.use_same_for_admin, &t!("use_same_for_admin"));
@@ -281,16 +373,33 @@ impl InstallerStep for UsersStep {
// DÜZELTME: 3. parametre olarak show_root_password_confirm eklendi
let show_root_pwd_conf = &mut self.show_root_password_confirm;
ui.horizontal(|ui| {
form_input_password(ui, &mut state.root_password_confirm, show_root_pwd_conf);
if !state.root_password_confirm.is_empty() {
if state.root_password == state.root_password_confirm {
ui.colored_label(theme::c_success(), "");
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_success())
);
} else {
ui.add(
egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg"))
.max_width(18.0).max_height(18.0)
.tint(theme::c_error())
);
}
}
});
if !state.root_password_confirm.is_empty() && state.root_password != state.root_password_confirm {
ui.colored_label(theme::c_error(), t!("passwords_no_match"));
}
});
}
// Autologin değerini state'e kaydet
state.autologin = self.autologin;
}
fn is_complete(&self, state: &GlobalState) -> bool {
+42 -27
View File
@@ -9,7 +9,7 @@ fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 {
)
}
fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_color: Color32, text_color: Color32, hover_top: Color32, hover_bottom: Color32, border_color: Color32) -> egui::Response {
fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_color: Color32, text_color: Color32, hover_top: Color32, hover_bottom: Color32, border_color: Color32, enabled: bool) -> egui::Response {
let rounding = Rounding::same(6.0);
let min_size = Vec2::new(100.0, 32.0);
let padding = Vec2::new(12.0, 6.0);
@@ -27,13 +27,26 @@ fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_col
min_size.y.max(galley.size().y + 2.0 * padding.y),
);
let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click());
let sense = if enabled { egui::Sense::click() } else { egui::Sense::hover() };
let (rect, response) = ui.allocate_exact_size(size, sense);
let (actual_top, actual_bottom, actual_text, actual_border) = if enabled {
let hover_gradient = if response.hovered() {
(hover_top, hover_bottom)
} else {
(top_color, bottom_color)
};
(hover_gradient.0, hover_gradient.1, text_color, border_color)
} else {
let disabled = lerp_color(top_color, Color32::from_rgb(80, 80, 80), 0.5);
(disabled, disabled, Color32::from_gray(160), Color32::from_gray(100))
};
let n_strips = 16;
for i in 0..n_strips {
let t = i as f32 / n_strips as f32;
let t2 = (i + 1) as f32 / n_strips as f32;
let c = lerp_color(top_color, bottom_color, (t + t2) * 0.5);
let c = lerp_color(actual_top, actual_bottom, (t + t2) * 0.5);
let y0 = rect.top() + rect.height() * t;
let y1 = rect.top() + rect.height() * t2;
let strip_rect = egui::Rect::from_min_max(
@@ -43,27 +56,12 @@ fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_col
ui.painter().rect(strip_rect, Rounding::ZERO, c, Stroke::NONE);
}
if response.hovered() {
for i in 0..n_strips {
let t = i as f32 / n_strips as f32;
let t2 = (i + 1) as f32 / n_strips as f32;
let c = lerp_color(hover_top, hover_bottom, (t + t2) * 0.5);
let y0 = rect.top() + rect.height() * t;
let y1 = rect.top() + rect.height() * t2;
let strip_rect = egui::Rect::from_min_max(
egui::pos2(rect.left(), y0),
egui::pos2(rect.right(), y1),
);
ui.painter().rect(strip_rect, Rounding::ZERO, c, Stroke::NONE);
}
}
ui.painter().rect(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0, border_color));
ui.painter().rect(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0, actual_border));
ui.painter().galley(
egui::pos2(rect.center().x - galley.size().x / 2.0, rect.center().y - galley.size().y / 2.0),
galley,
text_color,
actual_text,
);
if let Some(cursor) = ui.visuals().interact_cursor {
@@ -76,34 +74,48 @@ fn gradient_button(ui: &mut egui::Ui, text: &str, top_color: Color32, bottom_col
}
pub fn primary_button(ui: &mut egui::Ui, text: &str) -> egui::Response {
primary_button_enabled(ui, text, true)
}
pub fn primary_button_enabled(ui: &mut egui::Ui, text: &str, enabled: bool) -> egui::Response {
gradient_button(ui, text,
Color32::from_rgb(50, 130, 255),
Color32::from_rgb(13, 110, 253),
Color32::WHITE,
Color32::from_rgb(70, 150, 255),
Color32::from_rgb(30, 130, 255),
Color32::from_rgb(13, 110, 253))
Color32::from_rgb(13, 110, 253),
enabled)
}
pub fn secondary_button(ui: &mut egui::Ui, text: &str) -> egui::Response {
secondary_button_enabled(ui, text, true)
}
pub fn secondary_button_enabled(ui: &mut egui::Ui, text: &str, enabled: bool) -> egui::Response {
gradient_button(ui, text,
Color32::from_rgb(130, 140, 150), // top
Color32::from_rgb(100, 110, 120), // bottom
Color32::from_rgb(255, 255, 255), // text color32
Color32::from_rgb(150, 160, 170), // hover top
Color32::from_rgb(120, 130, 140), // hover bottom
Color32::from_rgb(100, 110, 120) // border
)
Color32::from_rgb(100, 110, 120), // border
enabled)
}
pub fn danger_button(ui: &mut egui::Ui, text: &str) -> egui::Response {
danger_button_enabled(ui, text, true)
}
pub fn danger_button_enabled(ui: &mut egui::Ui, text: &str, enabled: bool) -> egui::Response {
gradient_button(ui, text,
Color32::from_rgb(240, 83, 99),
Color32::from_rgb(200, 40, 60),
Color32::WHITE,
Color32::from_rgb(255, 100, 120),
Color32::from_rgb(220, 60, 80),
Color32::from_rgb(200, 40, 60))
Color32::from_rgb(200, 40, 60),
enabled)
}
#[allow(dead_code)]
@@ -114,7 +126,8 @@ pub fn white_grd_btn(ui: &mut egui::Ui, text: &str) -> egui::Response {
Color32::from_rgb(102, 102, 102),
Color32::from_rgb(246, 246, 246),
Color32::from_rgb(255, 255, 255),
Color32::from_rgb(177, 177, 177))
Color32::from_rgb(177, 177, 177),
true)
}
#[allow(dead_code)]
pub fn btn_dark_blue(ui: &mut egui::Ui, text: &str) -> egui::Response {
@@ -124,7 +137,8 @@ pub fn btn_dark_blue(ui: &mut egui::Ui, text: &str) -> egui::Response {
Color32::WHITE,
Color32::from_rgb(70, 140, 207),
Color32::from_rgb(99, 184, 238),
Color32::from_rgb(56, 102, 163))
Color32::from_rgb(56, 102, 163),
true)
}
#[allow(dead_code)]
@@ -135,5 +149,6 @@ pub fn light_gray_btn(ui: &mut egui::Ui, text: &str) -> egui::Response {
Color32::from_rgb(102, 102, 102), // metin: #666666
Color32::from_rgb(233, 233, 233), // hover üst (ters)
Color32::from_rgb(249, 249, 249), // hover alt (ters)
Color32::from_rgb(227, 195, 227)) // border: #e3c7e3
Color32::from_rgb(227, 195, 227), // border: #e3c7e3
true)
}
+19 -8
View File
@@ -1,5 +1,5 @@
use eframe::egui::{self, Rect, Vec2};
use super::combobox_stil::{draw_gradient_bg, DROPDOWN_TEXT_COLOR};
use super::combobox_stil::draw_gradient_bg;
/// Stil uygulanmış ComboBox oluşturur (genişlik ayarı yapılmaz, Grid içinde kullanım için).
pub fn combo_box(
@@ -48,13 +48,15 @@ pub fn selectable_value<T: PartialEq>(
draw_gradient_bg(ui, rect);
ui.put(rect, |ui: &mut egui::Ui| {
ui.horizontal(|ui| {
ui.add_space(6.0);
ui.colored_label(DROPDOWN_TEXT_COLOR, label);
})
.response
});
let text_color = combobox_stil_text_color(ui);
let painter = ui.painter();
painter.text(
egui::pos2(rect.left() + 6.0, rect.center().y),
egui::Align2::LEFT_CENTER,
label,
egui::FontId::proportional(14.0),
text_color,
);
if response.clicked() {
*current = value;
@@ -62,6 +64,15 @@ pub fn selectable_value<T: PartialEq>(
response.on_hover_cursor(egui::CursorIcon::PointingHand)
}
fn combobox_stil_text_color(ui: &egui::Ui) -> egui::Color32 {
let bg = ui.visuals().panel_fill;
if bg.r() < 128 && bg.g() < 128 && bg.b() < 128 {
egui::Color32::from_rgb(230, 230, 240)
} else {
egui::Color32::from_rgb(50, 50, 50)
}
}
/// ComboBox dropdown öğesi — özel içerik çizmek için (ör. bayrak + metin).
/// `add_contents` callback'i verilen `Rect` içine içerik ekler.
pub fn selectable_value_custom<R>(
+35 -32
View File
@@ -1,64 +1,67 @@
use eframe::egui::{self, Color32, Rect, Stroke};
use egui::epaint::Mesh;
// ─── ComboBox Popup Renk Sabitleri ──────────────────────────
pub const WEAK_BG_INACTIVE: Color32 = Color32::from_rgb(224, 224, 224);
pub const WEAK_BG_HOVERED: Color32 = Color32::from_rgb(200, 200, 200);
pub const WEAK_BG_ACTIVE: Color32 = Color32::from_rgb(180, 180, 180);
// ─── Gradient Dropdown Öğesi Renkleri ───────────────────────
pub const GRADIENT_TOP: Color32 = Color32::from_rgb(249, 249, 249);
pub const GRADIENT_BOT: Color32 = Color32::from_rgb(233, 233, 233);
// ─── ComboBox Metin Renkleri ────────────────────────────────
/// Seçili metin rengi (ComboBox kapalıyken widget üzerinde görünen metin)
pub const COMBO_TEXT_COLOR: Color32 = Color32::from_rgb(50, 50, 50);
/// Açılır listedeki öğelerin metin rengi
pub const DROPDOWN_TEXT_COLOR: Color32 = Color32::from_rgb(50, 50, 50);
// ─── ComboBox Kenarlık (border) Renkleri ────────────────────
/// Üzerine gelindiğinde / tıklanınca kenarlık rengi
pub const COMBO_BORDER_ACCENT: Color32 = Color32::from_rgb(155, 155, 155);
/// Verilen `Visuals` yapısına ComboBox stillerini uygular.
pub fn apply_visuals(v: &mut egui::Visuals) {
v.widgets.inactive.weak_bg_fill = WEAK_BG_INACTIVE;
v.widgets.hovered.weak_bg_fill = WEAK_BG_HOVERED;
v.widgets.active.weak_bg_fill = WEAK_BG_ACTIVE;
v.widgets.inactive.weak_bg_fill = Color32::from_rgb(224, 224, 224);
v.widgets.hovered.weak_bg_fill = Color32::from_rgb(200, 200, 200);
v.widgets.active.weak_bg_fill = Color32::from_rgb(180, 180, 180);
v.widgets.inactive.fg_stroke = Stroke::new(1.0, COMBO_TEXT_COLOR);
v.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(50, 50, 50));
v.widgets.hovered.fg_stroke = Stroke::new(1.5, Color32::WHITE);
v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE);
// Kenarlıklar
v.widgets.inactive.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT);
v.widgets.hovered.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT);
v.widgets.active.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT);
v.widgets.inactive.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
v.widgets.hovered.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
v.widgets.active.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
}
/// Açık gri gradyan mesh arkaplanı çizer (dropdown öğeleri için).
fn is_dark_ui(ui: &egui::Ui) -> bool {
let bg = ui.visuals().panel_fill;
bg.r() < 128 && bg.g() < 128 && bg.b() < 128
}
fn gradient_top(ui: &egui::Ui) -> Color32 {
if is_dark_ui(ui) {
Color32::from_rgb(40, 40, 45)
} else {
Color32::from_rgb(249, 249, 249)
}
}
fn gradient_bot(ui: &egui::Ui) -> Color32 {
if is_dark_ui(ui) {
Color32::from_rgb(30, 30, 35)
} else {
Color32::from_rgb(233, 233, 233)
}
}
/// Açık/koyu temaya uygun gradyan mesh arkaplanı çizer (dropdown öğeleri için).
pub fn draw_gradient_bg(ui: &mut egui::Ui, rect: Rect) {
let top = gradient_top(ui);
let bot = gradient_bot(ui);
let mut mesh = Mesh::default();
let idx = mesh.vertices.len() as u32;
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.left(), rect.top()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_TOP,
color: top,
});
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.right(), rect.top()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_TOP,
color: top,
});
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.right(), rect.bottom()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_BOT,
color: bot,
});
mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.left(), rect.bottom()),
uv: egui::pos2(0.0, 0.0),
color: GRADIENT_BOT,
color: bot,
});
mesh.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx + 2, idx + 3, idx]);
ui.painter().add(egui::Shape::mesh(mesh));
+302
View File
@@ -0,0 +1,302 @@
use eframe::egui::{self, Rect, Rounding, Stroke, Vec2};
use xkbcommon::xkb;
#[derive(Debug, Clone)]
pub struct XkbKey {
pub x: f32, pub y: f32, pub w: f32, pub h: f32,
pub labels: Vec<String>,
}
#[derive(Debug)]
pub struct XkbLayout {
pub keys: Vec<XkbKey>,
pub width: f32, pub height: f32,
}
/// Boş bir klavye düzeni (layout derlenemediğinde gösterilir).
pub fn empty_layout() -> XkbLayout {
XkbLayout { keys: vec![], width: 1.0, height: 1.0 }
}
/// ISO105 tuşlu klavye için XKB adı → (x, y, w, h) birim cinsinden.
/// x,y ondalıklı olabilir (geniş tuşlar için).
fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
// Satır y offset'leri (birim)
let r = |row| row as f32;
// Standart ISO105 düzeni. x değerleri kümülatif.
match name {
// ── (function row tamamen kaldırıldı) ──
// ── Number row ─────────────────
"TLDE" => Some((0.0, r(1), 1.0, 1.0)),
"AE01" => Some((1.0, r(1), 1.0, 1.0)),
"AE02" => Some((2.0, r(1), 1.0, 1.0)),
"AE03" => Some((3.0, r(1), 1.0, 1.0)),
"AE04" => Some((4.0, r(1), 1.0, 1.0)),
"AE05" => Some((5.0, r(1), 1.0, 1.0)),
"AE06" => Some((6.0, r(1), 1.0, 1.0)),
"AE07" => Some((7.0, r(1), 1.0, 1.0)),
"AE08" => Some((8.0, r(1), 1.0, 1.0)),
"AE09" => Some((9.0, r(1), 1.0, 1.0)),
"AE10" => Some((10.0, r(1), 1.0, 1.0)),
"AE11" => Some((11.0, r(1), 1.0, 1.0)),
"AE12" => Some((12.0, r(1), 1.0, 1.0)),
"BKSP" => Some((13.0, r(1), 2.0, 1.0)),
// ── Top alpha row (QP) ────────
"TAB" => Some((0.0, r(2), 1.5, 1.0)),
"AD01" => Some((1.5, r(2), 1.0, 1.0)),
"AD02" => Some((2.5, r(2), 1.0, 1.0)),
"AD03" => Some((3.5, r(2), 1.0, 1.0)),
"AD04" => Some((4.5, r(2), 1.0, 1.0)),
"AD05" => Some((5.5, r(2), 1.0, 1.0)),
"AD06" => Some((6.5, r(2), 1.0, 1.0)),
"AD07" => Some((7.5, r(2), 1.0, 1.0)),
"AD08" => Some((8.5, r(2), 1.0, 1.0)),
"AD09" => Some((9.5, r(2), 1.0, 1.0)),
"AD10" => Some((10.5, r(2), 1.0, 1.0)),
"AD11" => Some((11.5, r(2), 1.0, 1.0)),
"AD12" => Some((12.5, r(2), 1.0, 1.0)),
"BKSL" => Some((13.5, r(2), 1.0, 1.0)), // ISO: solundaki tuş (\|)
"RTRN" => Some((14.5, r(2), 1.25, 2.0)), // ISO Enter (2 satır)
// ── Home row (A–L) ────────────
"CAPS" => Some((0.0, r(3), 1.75, 1.0)),
"AC01" => Some((1.75, r(3), 1.0, 1.0)),
"AC02" => Some((2.75, r(3), 1.0, 1.0)),
"AC03" => Some((3.75, r(3), 1.0, 1.0)),
"AC04" => Some((4.75, r(3), 1.0, 1.0)),
"AC05" => Some((5.75, r(3), 1.0, 1.0)),
"AC06" => Some((6.75, r(3), 1.0, 1.0)),
"AC07" => Some((7.75, r(3), 1.0, 1.0)),
"AC08" => Some((8.75, r(3), 1.0, 1.0)),
"AC09" => Some((9.75, r(3), 1.0, 1.0)),
"AC10" => Some((10.75,r(3), 1.0, 1.0)),
"AC11" => Some((11.75,r(3), 1.0, 1.0)),
"AC12" => Some((12.75,r(3), 1.0, 1.0)),
// ── Bottom row (ZM) ──────────
"LFSH" => Some((0.0, r(4), 1.25, 1.0)),
"LSGT" => Some((1.25, r(4), 1.0, 1.0)), // ISO extra (< > |)
"AB01" => Some((2.25, r(4), 1.0, 1.0)),
"AB02" => Some((3.25, r(4), 1.0, 1.0)),
"AB03" => Some((4.25, r(4), 1.0, 1.0)),
"AB04" => Some((5.25, r(4), 1.0, 1.0)),
"AB05" => Some((6.25, r(4), 1.0, 1.0)),
"AB06" => Some((7.25, r(4), 1.0, 1.0)),
"AB07" => Some((8.25, r(4), 1.0, 1.0)),
"AB08" => Some((9.25, r(4), 1.0, 1.0)),
"AB09" => Some((10.25,r(4), 1.0, 1.0)),
"AB10" => Some((11.25,r(4), 1.0, 1.0)),
"RTSH" => Some((12.25,r(4), 3.75, 1.0)),
// ── Space bar row ─────────────
"LCTL" => Some((0.0, r(5), 1.25, 1.0)),
"LWIN" => Some((1.25, r(5), 1.25, 1.0)),
"LALT" => Some((2.5, r(5), 1.5, 1.0)),
"SPCE" => Some((4.0, r(5), 6.25, 1.0)),
"RALT" | "LVL3" => Some((10.25,r(5), 1.5, 1.0)),
"COMP" => Some((11.75,r(5), 1.25, 1.0)),
"RCTL" => Some((13.0, r(5), 1.25, 1.0)),
// ── (nav cluster tamamen kaldırıldı) ──
_ => None,
}
}
fn keysym_to_label(sym: xkb::Keysym) -> String {
let utf = xkb::keysym_to_utf8(sym);
if utf.chars().any(|c| !c.is_control()) {
let trimmed = utf.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
let name = xkb::keysym_get_name(sym);
if name == "VoidSymbol" || name == "NoSymbol" {
return String::new();
}
// Yaygın XKB sembol adlarını kısa etiketlere çevir
match name.as_str() {
"BackSpace" => "Bksp".into(),
"Tab" => "Tab".into(),
"Return" => "Enter".into(),
"Caps_Lock" => "Caps".into(),
"Shift_L" => "Shift".into(),
"Shift_R" => "Shift".into(),
"Control_L" => "Ctrl".into(),
"Control_R" => "Ctrl".into(),
"Alt_L" => "Alt".into(),
"Alt_R" => "AltGr".into(),
"ISO_Level3_Shift" => "AltGr".into(),
"Super_L" => "Win".into(),
"Super_R" => "Win".into(),
"Menu" => "Menü".into(),
"Escape" => "Esc".into(),
"Delete" => "Del".into(),
"Insert" => "Ins".into(),
"Prior" => "PgUp".into(),
"Next" => "PgDn".into(),
"space" => "".into(),
"Multi_key" => "Compose".into(),
_ if name.starts_with("XF86") => String::new(),
_ => name,
}
}
/// XKB keymap'inden klavye düzenini oluşturur.
pub fn compile_layout(layout: &str, variant: &str) -> Option<XkbLayout> {
let mut context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
context.set_log_level(xkb::LogLevel::Critical);
let model = if std::path::Path::new("/usr/share/X11/xkb/symbols/").exists() {
"pc105"
} else {
"pc105"
};
let keymap = xkb::Keymap::new_from_names::<str>(
&context,
"", // rules
model, // model
layout, // layout
if variant.is_empty() { "" } else { variant }, // variant
None::<String>, // options
xkb::COMPILE_NO_FLAGS,
)?;
let min = keymap.min_keycode();
let max = keymap.max_keycode();
let mut keys = Vec::new();
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
let mut moved_comma = false;
for code_u32 in (min.raw() as u32)..=(max.raw() as u32) {
let code = xkb::Keycode::new(code_u32);
let Some(name) = keymap.key_get_name(code) else { continue };
let Some((x, y, w, h)) = key_position(name) else { continue };
// Sembolleri al (base, shift, altgr, shift+altgr), boş olanları atla
let mut raw_labels = Vec::new();
for level in 0..4 {
let syms = keymap.key_get_syms_by_level(code, 0, level);
let label = syms.iter()
.map(|&s| keysym_to_label(s))
.collect::<Vec<_>>()
.join("");
if !label.is_empty() && !raw_labels.contains(&label) {
raw_labels.push(label);
}
}
// Her tuşta sadece 1 karakter göster (büyük harf tercihli)
let mut labels: Vec<String> = {
let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect();
if single.is_empty() {
// Hiç tek karakter yoksa ilk label'ı göster (F1, Esc, …)
raw_labels.into_iter().take(1).collect()
} else {
// Büyük harf varsa onu göster, yoksa ilk tek karakteri
let uc = single.iter().find(|l| l.chars().next().map_or(false, |c| c.is_uppercase()));
vec![if let Some(s) = uc { (*s).clone() } else { (*single[0]).clone() }]
}
};
// , tuşu BKSL'deyse AC12'ye taşı (Türkçe Q düzeni için)
if name == "BKSL" && labels.contains(&",".to_string()) {
moved_comma = true;
continue;
}
if name == "AC12" {
labels = vec![",".to_string()];
}
keys.push(XkbKey { x, y, w, h, labels });
if x < min_x { min_x = x; }
if y < min_y { min_y = y; }
if x + w > max_x { max_x = x + w; }
if y + h > max_y { max_y = y + h; }
}
// AC12 keymap'te yoksa manuel ekle
if moved_comma && !keys.iter().any(|k| (k.x - 12.75).abs() < 0.1 && (k.y - 3.0).abs() < 0.1) {
keys.push(XkbKey { x: 12.75, y: 3.0, w: 1.0, h: 1.0, labels: vec![",".to_string()] });
}
// normalize x,y — tüm tuşlar (0,0) bazlı olsun
for key in &mut keys {
key.x -= min_x;
key.y -= min_y;
}
Some(XkbLayout {
keys,
width: max_x - min_x,
height: max_y - min_y,
})
}
/// Klavyeyi egui ile çizer.
pub fn draw_layout(ui: &mut egui::Ui, layout: &XkbLayout, max_size: Vec2) {
let margin = 6.0;
let spacing = 0.08;
let corner = 3.0;
let uw = (max_size.x - margin * 2.0) / layout.width;
let uh = (max_size.y - margin * 2.0) / layout.height;
let unit = uw.min(uh).max(12.0);
let draw_w = layout.width * unit;
let draw_h = layout.height * unit;
let (_id, resp) = ui.allocate_exact_size(Vec2::new(draw_w + margin * 2.0, draw_h + margin * 2.0), egui::Sense::hover());
let painter = ui.painter_at(resp.rect);
let origin = resp.rect.min;
let visuals = ui.visuals();
let key_fill = visuals.widgets.inactive.bg_fill;
let key_stroke = visuals.widgets.noninteractive.bg_stroke;
let text_color = egui::Color32::BLACK;
let bg = visuals.window_fill();
painter.rect_filled(resp.rect, Rounding::same(6.0), bg);
let font_size = (unit * 0.28).max(6.0);
let small_size = (unit * 0.22).max(5.0);
let sp = unit * spacing;
for key in &layout.keys {
let kx = origin.x + margin + key.x * unit + sp;
let ky = origin.y + margin + key.y * unit + sp;
let kw = key.w * unit - sp * 2.0;
let kh = key.h * unit - sp * 2.0;
if kw <= 0.0 || kh <= 0.0 { continue; }
let key_rect = Rect::from_min_size(egui::pos2(kx, ky), Vec2::new(kw, kh));
painter.rect(key_rect, Rounding::same(corner), key_fill, Stroke::new(key_stroke.width.max(0.5), key_stroke.color));
for (i, label) in key.labels.iter().enumerate() {
let lbl = label.trim();
if lbl.is_empty() { continue; }
let (lx, ly, sz) = match i {
0 => (kx + kw * 0.5, ky + kh * 0.5, font_size),
1 => (kx + kw * 0.5, ky + kh * 0.75, small_size),
_ => (kx + kw * 0.5, ky + kh * 0.9, small_size),
};
painter.text(
egui::pos2(lx, ly), egui::Align2::CENTER_CENTER,
lbl, egui::FontId::proportional(sz), text_color,
);
}
}
}
+110 -174
View File
@@ -1,4 +1,4 @@
use eframe::egui::{self, Rect, Rounding, Stroke, Vec2};
use eframe::egui::{self, Vec2};
use xkbcommon::xkb;
#[derive(Debug, Clone)]
@@ -10,36 +10,16 @@ pub struct XkbKey {
#[derive(Debug)]
pub struct XkbLayout {
pub keys: Vec<XkbKey>,
pub width: f32, pub height: f32,
}
/// ISO105 tuşlu klavye için XKB adı → (x, y, w, h) birim cinsinden.
/// x,y ondalıklı olabilir (geniş tuşlar için).
pub fn empty_layout() -> XkbLayout {
XkbLayout { keys: vec![] }
}
fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
// Satır y offset'leri (birim)
let r = |row| row as f32;
// Standart ISO105 düzeni. x değerleri kümülatif.
match name {
// ── Function row ────────────────
"ESC" => Some((0.0, r(0), 1.0, 1.0)),
"FK01" => Some((2.0, r(0), 1.0, 1.0)),
"FK02" => Some((3.0, r(0), 1.0, 1.0)),
"FK03" => Some((4.0, r(0), 1.0, 1.0)),
"FK04" => Some((5.0, r(0), 1.0, 1.0)),
"FK05" => Some((6.0, r(0), 1.0, 1.0)),
"FK06" => Some((7.0, r(0), 1.0, 1.0)),
"FK07" => Some((8.0, r(0), 1.0, 1.0)),
"FK08" => Some((9.0, r(0), 1.0, 1.0)),
"FK09" => Some((10.0, r(0), 1.0, 1.0)),
"FK10" => Some((11.0, r(0), 1.0, 1.0)),
"FK11" => Some((12.0, r(0), 1.0, 1.0)),
"FK12" => Some((13.0, r(0), 1.0, 1.0)),
"PRSC" => Some((15.0, r(0), 1.0, 1.0)),
"SCLK" => Some((16.0, r(0), 1.0, 1.0)),
"PAUS" => Some((17.0, r(0), 1.0, 1.0)),
// ── Number row ─────────────────
"TLDE" => Some((0.0, r(1), 1.0, 1.0)),
"AE01" => Some((1.0, r(1), 1.0, 1.0)),
"AE02" => Some((2.0, r(1), 1.0, 1.0)),
@@ -53,9 +33,8 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
"AE10" => Some((10.0, r(1), 1.0, 1.0)),
"AE11" => Some((11.0, r(1), 1.0, 1.0)),
"AE12" => Some((12.0, r(1), 1.0, 1.0)),
"BKSP" => Some((13.0, r(1), 2.0, 1.0)),
"BKSP" => Some((13.0, r(1), 2.25, 1.0)),
// ── Top alpha row (QP) ────────
"TAB" => Some((0.0, r(2), 1.5, 1.0)),
"AD01" => Some((1.5, r(2), 1.0, 1.0)),
"AD02" => Some((2.5, r(2), 1.0, 1.0)),
@@ -69,10 +48,7 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
"AD10" => Some((10.5, r(2), 1.0, 1.0)),
"AD11" => Some((11.5, r(2), 1.0, 1.0)),
"AD12" => Some((12.5, r(2), 1.0, 1.0)),
"BKSL" => Some((14.0, r(2), 1.5, 1.0)), // ISO: solundaki tuş (\|)
"RTRN" => Some((14.0, r(2), 1.5, 2.0)), // ISO Enter (2 satır)
// ── Home row (A–L) ────────────
"CAPS" => Some((0.0, r(3), 1.75, 1.0)),
"AC01" => Some((1.75, r(3), 1.0, 1.0)),
"AC02" => Some((2.75, r(3), 1.0, 1.0)),
@@ -86,10 +62,10 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
"AC10" => Some((10.75,r(3), 1.0, 1.0)),
"AC11" => Some((11.75,r(3), 1.0, 1.0)),
"AC12" => Some((12.75,r(3), 1.0, 1.0)),
"BKSL" => Some((12.50,r(3), 1.0, 1.0)),
// ── Bottom row (ZM) ──────────
"LFSH" => Some((0.0, r(4), 1.25, 1.0)),
"LSGT" => Some((1.25, r(4), 1.0, 1.0)), // ISO extra (< > |)
"LSGT" => Some((1.25, r(4), 1.0, 1.0)),
"AB01" => Some((2.25, r(4), 1.0, 1.0)),
"AB02" => Some((3.25, r(4), 1.0, 1.0)),
"AB03" => Some((4.25, r(4), 1.0, 1.0)),
@@ -100,61 +76,15 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
"AB08" => Some((9.25, r(4), 1.0, 1.0)),
"AB09" => Some((10.25,r(4), 1.0, 1.0)),
"AB10" => Some((11.25,r(4), 1.0, 1.0)),
"AB11" => Some((12.25,r(4), 1.0, 1.0)),
"RTSH" => Some((13.25,r(4), 2.75, 1.0)),
"RTSH" => Some((12.25,r(4), 3.0, 1.0)),
// ── Space bar row ─────────────
"LCTL" => Some((0.0, r(5), 1.25, 1.0)),
"LWIN" => Some((1.25, r(5), 1.25, 1.0)),
"LALT" => Some((2.5, r(5), 1.5, 1.0)),
"SPCE" => Some((4.0, r(5), 6.25, 1.0)),
"RALT" | "LVL3" => Some((10.25, r(5), 1.5, 1.0)),
"RWIN" => Some((11.75,r(5), 1.25, 1.0)),
"COMP" => Some((13.0, r(5), 1.25, 1.0)),
"RCTL" => Some((14.25,r(5), 1.25, 1.0)),
// ── Nav cluster ───────────────
"INS" => Some((15.5, r(2), 1.0, 1.0)),
"HOME" => Some((16.5, r(2), 1.0, 1.0)),
"PGUP" => Some((17.5, r(2), 1.0, 1.0)),
"DELE" => Some((15.5, r(3), 1.0, 1.0)),
"END" => Some((16.5, r(3), 1.0, 1.0)),
"PGDN" => Some((17.5, r(3), 1.0, 1.0)),
"UP" => Some((16.5, r(4), 1.0, 1.0)),
"LEFT" => Some((15.5, r(5), 1.0, 1.0)),
"DOWN" => Some((16.5, r(5), 1.0, 1.0)),
"RGHT" => Some((17.5, r(5), 1.0, 1.0)),
// ── Numpad ────────────────────
"NMLK" => Some((19.0, r(2), 1.0, 1.0)),
"KSLU" => Some((20.0, r(2), 1.0, 1.0)), // /
"KAST" => Some((21.0, r(2), 1.0, 1.0)), // *
"KMIN" => Some((22.0, r(2), 1.0, 1.0)), // -
"KP7" => Some((19.0, r(3), 1.0, 1.0)),
"KP8" => Some((20.0, r(3), 1.0, 1.0)),
"KP9" => Some((21.0, r(3), 1.0, 1.0)),
"KPLU" => Some((22.0, r(3), 1.0, 2.0)), // +
"KP4" => Some((19.0, r(4), 1.0, 1.0)),
"KP5" => Some((20.0, r(4), 1.0, 1.0)),
"KP6" => Some((21.0, r(4), 1.0, 1.0)),
"KP1" => Some((19.0, r(5), 1.0, 1.0)),
"KP2" => Some((20.0, r(5), 1.0, 1.0)),
"KP3" => Some((21.0, r(5), 1.0, 1.0)),
"KP0" => Some((19.0, r(6), 2.0, 1.0)),
"KPDL" => Some((21.0, r(6), 1.0, 1.0)), // .
"KPEN" => Some((22.0, r(5), 1.0, 2.0)), // Enter
"K7" => Some((19.0, r(3), 1.0, 1.0)), // old alias
"K8" => Some((20.0, r(3), 1.0, 1.0)),
"K9" => Some((21.0, r(3), 1.0, 1.0)),
"K4" => Some((19.0, r(4), 1.0, 1.0)),
"K5" => Some((20.0, r(4), 1.0, 1.0)),
"K6" => Some((21.0, r(4), 1.0, 1.0)),
"K1" => Some((19.0, r(5), 1.0, 1.0)),
"K2" => Some((20.0, r(5), 1.0, 1.0)),
"K3" => Some((21.0, r(5), 1.0, 1.0)),
"K0" => Some((19.0, r(6), 2.0, 1.0)),
"KDOT" => Some((21.0, r(6), 1.0, 1.0)),
"KENT" => Some((22.0, r(5), 1.0, 2.0)),
"COMP" => Some((11.75, r(5), 1.25, 1.0)),
"RCTL" => Some((13.0, r(5), 2.25, 1.0)),
_ => None,
}
@@ -162,94 +92,76 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
fn keysym_to_label(sym: xkb::Keysym) -> String {
let utf = xkb::keysym_to_utf8(sym);
if !utf.is_empty() { return utf; }
let name = xkb::keysym_get_name(sym);
if name == "VoidSymbol" || name == "NoSymbol" {
return String::new();
if utf.chars().any(|c| !c.is_control()) {
let trimmed = utf.trim();
if !trimmed.is_empty() {
return trimmed.to_uppercase();
}
// Yaygın XKB sembol adlarını kısa etiketlere çevir
}
let name = xkb::keysym_get_name(sym);
if name == "VoidSymbol" || name == "NoSymbol" { return String::new(); }
match name.as_str() {
"BackSpace" => "".into(),
"Tab" => "".into(),
"Return" => "".into(),
"Caps_Lock" => "".into(),
"Shift_L" => "".into(),
"Shift_R" => "".into(),
"Control_L" => "Ctrl".into(),
"Control_R" => "Ctrl".into(),
"BackSpace" => "Bksp".into(),
"Tab" => "Tab".into(),
"Return" => "Enter".into(),
"Caps_Lock" => "Caps".into(),
"Shift_L" | "Shift_R" => "Shift".into(),
"Control_L" | "Control_R" => "Ctrl".into(),
"Alt_L" => "Alt".into(),
"Alt_R" => "AltGr".into(),
"ISO_Level3_Shift" => "AltGr".into(),
"Super_L" => "Win".into(),
"Super_R" => "Win".into(),
"Alt_R" | "ISO_Level3_Shift" => "AltGr".into(),
"Super_L" | "Super_R" => "Win".into(),
"Menu" => "Menü".into(),
"Escape" => "Esc".into(),
"Delete" => "Del".into(),
"Insert" => "Ins".into(),
"Prior" => "PgUp".into(),
"Next" => "PgDn".into(),
"space" => "".into(),
"Multi_key" => "Compose".into(),
_ if name.starts_with("XF86") => String::new(),
_ => name,
}
}
/// XKB keymap'inden klavye düzenini oluşturur.
pub fn compile_layout(layout: &str, variant: &str) -> Option<XkbLayout> {
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let model = if std::path::Path::new("/usr/share/X11/xkb/symbols/").exists() {
"pc105"
} else {
"pc105"
};
let mut context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
context.set_log_level(xkb::LogLevel::Critical);
let keymap = xkb::Keymap::new_from_names::<str>(
&context,
"", // rules
model, // model
layout, // layout
if variant.is_empty() { "" } else { variant }, // variant
None::<String>, // options
xkb::COMPILE_NO_FLAGS,
&context, "", "pc105", layout,
if variant.is_empty() { "" } else { variant },
None::<String>, xkb::COMPILE_NO_FLAGS,
)?;
let min = keymap.min_keycode();
let max = keymap.max_keycode();
let mut keys = Vec::new();
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
let mut enter_label = "Enter".to_string();
for code_u32 in (min.raw() as u32)..=(max.raw() as u32) {
let code = xkb::Keycode::new(code_u32);
let Some(name) = keymap.key_get_name(code) else { continue };
if name == "RTRN" {
let syms = keymap.key_get_syms_by_level(code, 0, 0);
let label = syms.iter().map(|&s| keysym_to_label(s)).collect::<Vec<_>>().join("");
if !label.is_empty() { enter_label = label; }
continue;
}
let Some((x, y, w, h)) = key_position(name) else { continue };
// Sembolleri al (base, shift, altgr, shift+altgr), boş olanları atla
let mut raw_labels = Vec::new();
for level in 0..4 {
let syms = keymap.key_get_syms_by_level(code, 0, level);
let label = syms.iter()
.map(|&s| keysym_to_label(s))
.collect::<Vec<_>>()
.join("");
let label = syms.iter().map(|&s| keysym_to_label(s)).collect::<Vec<_>>().join("");
if !label.is_empty() && !raw_labels.contains(&label) {
raw_labels.push(label);
}
}
// Her tuşta sadece 1 karakter göster (büyük harf tercihli)
let labels: Vec<String> = {
let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect();
if single.is_empty() {
// Hiç tek karakter yoksa ilk label'ı göster (F1, Esc, …)
raw_labels.into_iter().take(1).collect()
} else {
// Büyük harf varsa onu göster, yoksa ilk tek karakteri
let uc = single.iter().find(|l| l.chars().next().map_or(false, |c| c.is_uppercase()));
vec![if let Some(s) = uc { (*s).clone() } else { (*single[0]).clone() }]
}
@@ -257,78 +169,102 @@ pub fn compile_layout(layout: &str, variant: &str) -> Option<XkbLayout> {
keys.push(XkbKey { x, y, w, h, labels });
if x < min_x { min_x = x; }
if y < min_y { min_y = y; }
if x + w > max_x { max_x = x + w; }
if y + h > max_y { max_y = y + h; }
}
// normalize x,y — tüm tuşlar (0,0) bazlı olsun
keys.push(XkbKey { x: 13.5, y: 2.0, w: 1.75, h: 1.0, labels: vec![enter_label.clone()] });
keys.push(XkbKey { x: 14.75, y: 3.0, w: 0.5, h: 1.0, labels: vec!["_RTRN_BOT".to_string()] });
for key in &mut keys {
key.x -= min_x;
key.y -= min_y;
}
Some(XkbLayout {
keys,
width: max_x - min_x,
height: max_y - min_y,
})
}
/// Klavyeyi egui ile çizer.
pub fn draw_layout(ui: &mut egui::Ui, layout: &XkbLayout, max_size: Vec2) {
let margin = 6.0;
let spacing = 0.08;
let corner = 3.0;
let uw = (max_size.x - margin * 2.0) / layout.width;
let uh = (max_size.y - margin * 2.0) / layout.height;
let unit = uw.min(uh).max(12.0);
let svg_size = Vec2::new(840.0, 300.0);
let img_scale = ((max_size.x - margin * 2.0) / svg_size.x)
.min((max_size.y - margin * 2.0) / svg_size.y)
.max(0.1);
let draw_size = svg_size * img_scale;
let total_size = Vec2::new(draw_size.x + margin * 2.0, draw_size.y + margin * 2.0);
let draw_w = layout.width * unit;
let draw_h = layout.height * unit;
let (_id, resp) = ui.allocate_exact_size(Vec2::new(draw_w + margin * 2.0, draw_h + margin * 2.0), egui::Sense::hover());
let (_id, resp) = ui.allocate_exact_size(total_size, egui::Sense::hover());
let painter = ui.painter_at(resp.rect);
let origin = resp.rect.min;
let origin = egui::pos2(resp.rect.min.x + margin, resp.rect.min.y + margin);
let visuals = ui.visuals();
let key_fill = visuals.widgets.inactive.bg_fill;
let key_stroke = visuals.widgets.noninteractive.bg_stroke;
let text_color = visuals.text_color();
let bg = visuals.window_fill();
let is_dark = visuals.dark_mode;
painter.rect_filled(resp.rect, Rounding::same(6.0), bg);
painter.rect_filled(resp.rect, egui::Rounding::same(6.0), bg);
let font_size = (unit * 0.28).max(6.0);
let small_size = (unit * 0.22).max(5.0);
let img_rect = egui::Rect::from_min_size(origin, draw_size);
egui::Image::from_bytes(
"keyboard.svg",
&include_bytes!("../../assets/keyboard-layout.svg")[..],
).paint_at(ui, img_rect);
let sp = unit * spacing;
// Dark mode: SVG'yi karart
if is_dark {
painter.rect_filled(img_rect, egui::Rounding::same(4.0), egui::Color32::from_black_alpha(170));
}
let font_size = (draw_size.x * 0.015).max(6.0);
let text_color = if is_dark { egui::Color32::from_gray(230) } else { egui::Color32::from_gray(30) };
const PITCH: f32 = 54.0;
const KEY_PX: f32 = 52.0;
const CX0: f32 = 42.0;
const CY0: f32 = 42.0;
const ENTER_VBX: f32 = 791.0;
const ENTER_VBY: f32 = 123.0;
let mut enter_text = String::new();
for key in &layout.keys {
let kx = origin.x + margin + key.x * unit + sp;
let ky = origin.y + margin + key.y * unit + sp;
let kw = key.w * unit - sp * 2.0;
let kh = key.h * unit - sp * 2.0;
if let Some(lbl) = key.labels.first() {
if lbl == "_RTRN_BOT" { continue; }
if key.y == 1.0 && (key.x - 13.5).abs() < 0.1 {
enter_text = lbl.clone();
continue;
}
}
if kw <= 0.0 || kh <= 0.0 { continue; }
let svg_cx = CX0 + key.x * PITCH;
let svg_cy = CY0 + key.y * PITCH;
let svg_w = KEY_PX + (key.w - 1.0) * PITCH;
let svg_h = KEY_PX + (key.h - 1.0) * PITCH;
let key_rect = Rect::from_min_size(egui::pos2(kx, ky), Vec2::new(kw, kh));
painter.rect(key_rect, Rounding::same(corner), key_fill, Stroke::new(key_stroke.width.max(0.5), key_stroke.color));
let ex = origin.x + (svg_cx - svg_w / 2.0) * img_scale;
let ey = origin.y + (svg_cy - svg_h / 2.0) * img_scale;
let ew = svg_w * img_scale;
let eh = svg_h * img_scale;
for (i, label) in key.labels.iter().enumerate() {
let lbl = label.trim();
if lbl.is_empty() { continue; }
let (lx, ly, sz) = match i {
0 => (kx + kw * 0.5, ky + kh * 0.3, font_size),
1 => (kx + kw * 0.5, ky + kh * 0.65, small_size),
_ => (kx + kw * 0.5, ky + kh * 0.9, small_size),
};
if let Some(lbl) = key.labels.first() {
let trimmed = lbl.trim();
if trimmed.is_empty() { continue; }
let text_pos = egui::pos2(ex + ew / 2.0, ey + eh / 2.0);
painter.text(text_pos, egui::Align2::CENTER_CENTER, trimmed, egui::FontId::proportional(font_size), text_color);
}
}
if !enter_text.is_empty() {
painter.text(
egui::pos2(lx, ly), egui::Align2::CENTER_CENTER,
lbl, egui::FontId::proportional(sz), text_color,
egui::pos2(origin.x + ENTER_VBX * img_scale, origin.y + ENTER_VBY * img_scale),
egui::Align2::CENTER_CENTER,
&enter_text,
egui::FontId::proportional(font_size),
text_color,
);
}
}
}
+25 -15
View File
@@ -15,6 +15,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
static BG_DARK: AtomicU32 = AtomicU32::new(0x121214);
static BG_PANEL: AtomicU32 = AtomicU32::new(0x1A1A1E);
static BG_WIDGET: AtomicU32 = AtomicU32::new(0x242428);
static BG_FOOTER: AtomicU32 = AtomicU32::new(0x121214);
static BORDER: AtomicU32 = AtomicU32::new(0x323238);
@@ -26,6 +27,8 @@ static ERROR: AtomicU32 = AtomicU32::new(0xF44336);
static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D);
static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b)
static SIDEBAR_TEXT: AtomicU32 = AtomicU32::new(0x000000);
static KEYBOARD_BG: AtomicU32 = AtomicU32::new(0xFFFFFF);
static BG_ROOT: AtomicU32 = AtomicU32::new(0x1A1A1E);
@@ -46,32 +49,39 @@ pub fn c_warning() -> Color32 { color_from_hex(WARNING.load(Ordering::Relaxed)
pub fn c_error() -> Color32 { color_from_hex(ERROR.load(Ordering::Relaxed)) }
pub fn c_sidebar() -> Color32 { color_from_hex(SIDEBAR.load(Ordering::Relaxed)) }
pub fn c_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering::Relaxed)) }
pub fn c_footer_bg() -> Color32 { color_from_hex(0xf7f7f7) }
pub fn c_root_bg() -> Color32 { color_from_hex(0xf7f7f7) }
pub fn c_footer_bg() -> Color32 { color_from_hex(BG_FOOTER.load(Ordering::Relaxed)) }
pub fn c_root_bg() -> Color32 { color_from_hex(BG_ROOT.load(Ordering::Relaxed)) }
pub fn c_keyboard_bg() -> Color32 { color_from_hex(KEYBOARD_BG.load(Ordering::Relaxed)) }
pub fn set_theme_mode(is_dark: bool) {
if is_dark {
BG_FOOTER.store(0x121214, Ordering::Relaxed);
BG_DARK.store(0x121214, Ordering::Relaxed);
BG_PANEL.store(0x1A1A1E, Ordering::Relaxed);
BG_WIDGET.store(0x242428, Ordering::Relaxed);
BORDER.store(0x323238, Ordering::Relaxed);
TEXT.store(0xF3F3F5, Ordering::Relaxed);
TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed);
TEXT.store(0xF3F3F5, Ordering::Relaxed); // #F3F3F5
TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed); // #9E9EAF
SIDEBAR.store(0x0B0B0D, Ordering::Relaxed);
SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed);
KEYBOARD_BG.store(0x000000, Ordering::Relaxed);
BG_ROOT.store(0x1A1A1E, Ordering::Relaxed);
} else {
BG_FOOTER.store(0xf7f7f7, Ordering::Relaxed);
BG_DARK.store(0xf7f7f7, Ordering::Relaxed);
SIDEBAR.store(0x292f34, Ordering::Relaxed);
TEXT.store(0x000000, Ordering::Relaxed);
TEXT_DIM.store(0x000000, Ordering::Relaxed);
TEXT.store(0x000000, Ordering::Relaxed); // #000000
TEXT_DIM.store(0x000000, Ordering::Relaxed); // #000000
SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed);
KEYBOARD_BG.store(0xffffff, Ordering::Relaxed);
BG_ROOT.store(0xf7f7f7, Ordering::Relaxed);
//BG_DARK.store(0xF0F0F5, Ordering::Relaxed);
// BG_PANEL.store(0xFFFFFF, Ordering::Relaxed);
// BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed);
// BORDER.store(0xD0D0DA, Ordering::Relaxed);
// TEXT.store(0x20202A, Ordering::Relaxed);
// TEXT_DIM.store(0x60606A, Ordering::Relaxed);
BG_PANEL.store(0xFFFFFF, Ordering::Relaxed);
BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed);
BORDER.store(0xD0D0DA, Ordering::Relaxed);
//TEXT.store(0x20202A, Ordering::Relaxed); // #20202A
//TEXT_DIM.store(0x60606A, Ordering::Relaxed); // #60606A
//SIDEBAR.store(0xE4E4EA, Ordering::Relaxed);
}
}
@@ -183,13 +193,13 @@ fn build_visuals(is_dark: bool) -> Visuals {
v.widgets.noninteractive.bg_fill = c_bg_widget();
// Pasif Hali: Widget arka planı (TextEdit, ComboBox giriş alanı)
v.widgets.inactive.bg_fill = Color32::from_rgb(255, 255, 255);
v.widgets.inactive.bg_fill = if is_dark { Color32::from_rgb(255, 255, 255) } else { Color32::from_rgb(245, 245, 245) };
// Üzerine Gelme Hali: Hafif açık
v.widgets.hovered.bg_fill = Color32::from_rgb(55, 58, 68);
v.widgets.hovered.bg_fill = if is_dark { Color32::from_rgb(55, 58, 68) } else { Color32::from_rgb(235, 235, 235) };
// Tıklanma/Açık Kalma Hali: Daha açık
v.widgets.active.bg_fill = Color32::WHITE;
v.widgets.active.bg_fill = if is_dark { Color32::WHITE } else { Color32::from_rgb(230, 230, 230) };
// ComboBox stilleri (combobox_stil.rs)
crate::ui::combobox_stil::apply_visuals(&mut v);