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 ### Changed
- Sürgü maksimum değeri artık diskteki kullanılabilir alan kadar (diğer bölümler hariç) - 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. 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 ## Ö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`) - **İ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`) - **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 - **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) │ │ ├── partition.rs # Disk bölümleme (otomatik + manuel + LVM)
│ │ ├── users.rs # Kullanıcı hesabı oluşturma │ │ ├── users.rs # Kullanıcı hesabı oluşturma
│ │ ├── bootloader.rs # GRUB yapılandırması │ │ ├── bootloader.rs # GRUB yapılandırması
│ │ ├── display_manager.rs # Görüntü yöneticisi ve masaüstü seçimi
│ │ ├── summary.rs # Kurulum özeti │ │ ├── summary.rs # Kurulum özeti
│ │ ├── execution.rs # Kurulum yürütme (ilerleme çubuğu, log) │ │ ├── execution.rs # Kurulum yürütme (ilerleme çubuğu, log)
│ │ ├── finish.rs # Tamamlanma ekranı │ │ ├── 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) 3. **Klavye** — Düzen/varyant seçimi, arama, canlı önizleme (setxkbmap)
4. **Ağ** — WiFi tarama, bağlantı, proxy 4. **Ağ** — WiFi tarama, bağlantı, proxy
5. **Disk Bölümleme** — Otomatik (rehberli) veya manuel, LVM, LUKS 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 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 8. **Özet** — Tüm seçimlerin gözden geçirilmesi
9. **Özet**Tüm seçimlerin gözden geçirilmesi 9. **Kurulum**Slayt + ilerleme çubuğu + canlı log
10. **Kurulum**Slayt + ilerleme çubuğu + canlı log 10. **Bitir**Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön
11. **Bitir** — Başarılı kurulum, yeniden başlat veya canlı masaüstüne dön
## Bağımlılıklar ## 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" password_confirm_label = "Confirm password"
hostname_label = "Computer name" hostname_label = "Computer name"
username_invalid = "Invalid username. Must start with a lowercase letter or underscore, max 32 characters." 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." hostname_invalid = "Invalid hostname. Only letters, digits, and hyphens are allowed."
passwords_no_match = "Passwords do not match" 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)" password_confirm_label = "Parola (tekrar)"
hostname_label = "Bilgisayar adı" 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_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." hostname_invalid = "Geçersiz bilgisayar adı. Yalnızca harf, rakam ve tire içerebilir."
passwords_no_match = "Parolalar eşleşmiyor" 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( ui.add(
eframe::egui::Image::new(eframe::egui::include_image!("../assets/icons/language-icon.svg")) eframe::egui::Image::new(eframe::egui::include_image!("../assets/icons/language-icon.svg"))
.max_width(40.0) .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 current_step: usize,
pub available_disks: Vec<DiskInfo>, pub available_disks: Vec<DiskInfo>,
pub install_progress: f32, pub install_progress: f32,
pub install_sub_progress: f32,
pub install_log: Vec<String>, pub install_log: Vec<String>,
pub install_current_file: Option<String>, pub install_current_file: Option<String>,
pub demo_mode: bool, pub demo_mode: bool,
@@ -274,10 +275,11 @@ impl Default for GlobalState {
current_step: 0, current_step: 0,
available_disks: Vec::new(), available_disks: Vec::new(),
install_progress: 0.0, install_progress: 0.0,
install_sub_progress: 0.0,
install_log: Vec::new(), install_log: Vec::new(),
install_current_file: None, install_current_file: None,
demo_mode: false, demo_mode: false,
is_dark: false, is_dark: true,
rescue_mode: false, rescue_mode: false,
bootloader_device: String::new(), bootloader_device: String::new(),
bootloader_timeout: 5, bootloader_timeout: 5,
@@ -301,7 +303,7 @@ impl Default for GlobalState {
oem_password_changed: false, oem_password_changed: false,
selected_package_groups: Vec::new(), selected_package_groups: Vec::new(),
display_manager: "sddm".to_string(), display_manager: "sddm".to_string(),
autologin: true, autologin: false,
desktop_environment: detect_desktop_environment(), desktop_environment: detect_desktop_environment(),
branding_icons: std::collections::HashMap::new(), branding_icons: std::collections::HashMap::new(),
branding_pisi_dark: String::new(), branding_pisi_dark: String::new(),
+109 -6
View File
@@ -13,6 +13,7 @@
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc; use std::sync::mpsc;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command; use tokio::process::Command;
use rust_i18n::t; use rust_i18n::t;
@@ -27,6 +28,10 @@ pub enum InstallMessage {
Log(String), Log(String),
/// Genel ilerleme; 0.01.0 arasında. /// Genel ilerleme; 0.01.0 arasında.
Progress(f32), 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ı. /// Tüm işler başarıyla tamamlandı.
Done, Done,
/// Bir iş başarısız oldu; kurulum durdu. /// Bir iş başarısız oldu; kurulum durdu.
@@ -54,6 +59,14 @@ impl UiSender {
pub fn log(&self, s: impl Into<String>) { pub fn log(&self, s: impl Into<String>) {
self.send(InstallMessage::Log(s.into())); 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.source.clone());
args.push(self.target.clone()); args.push(self.target.clone());
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); if DEMO_MODE.load(Ordering::Relaxed) {
run_cmd("rsync", &arg_refs).await?; 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(()) Ok(())
} }
@@ -1186,10 +1265,22 @@ impl Job for ConfigureDisplayManagerJob {
match dm.as_str() { match dm.as_str() {
"sddm" => { "sddm" => {
let conf = format!("{}/usr/lib/sddm/sddm.conf.d/sddm.conf", self.mount); 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 { let content = if self.autologin {
format!( format!(
"[Autologin]\nUser={}\nSession=plasmax11.desktop\n\n[Theme]\nCurrent=pisilinux24\n", "[Autologin]\nUser={}\nSession={}\n\n[Theme]\nCurrent=pisilinux24\n",
self.username self.username, session
) )
} else { } else {
"[Autologin]\nUser=\nSession=\n\n[Theme]\nCurrent=pisilinux24\n".to_string() "[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 dir = format!("{}/etc/gdm", self.mount);
let _ = tokio::fs::create_dir_all(&dir).await; let _ = tokio::fs::create_dir_all(&dir).await;
let conf = format!("{}/etc/gdm/custom.conf", self.mount); 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 { let content = if self.autologin {
format!( format!(
"[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\n", "[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\nDefaultSession={}\n",
self.username self.username, session
) )
} else { } else {
"[daemon]\n".to_string() "[daemon]\n".to_string()
+23 -26
View File
@@ -7,7 +7,7 @@ mod branding;
mod funct; mod funct;
use installer::{GlobalState, InstallerStep, WelcomeStep}; 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 eframe::egui;
use rust_i18n::t; use rust_i18n::t;
@@ -48,8 +48,8 @@ fn run_gui(demo_mode: bool) -> eframe::Result<()> {
Box::new(move |cc| { Box::new(move |cc| {
// Görsel yükleyicileri etkinleştir (PNG desteği) // Görsel yükleyicileri etkinleştir (PNG desteği)
egui_extras::install_image_loaders(&cc.egui_ctx); egui_extras::install_image_loaders(&cc.egui_ctx);
ui::theme::set_theme_mode(false); ui::theme::set_theme_mode(true);
ui::theme::apply(&cc.egui_ctx, false); ui::theme::apply(&cc.egui_ctx, true);
let mut app = YaliApp::default(); let mut app = YaliApp::default();
app.state.demo_mode = demo_mode; app.state.demo_mode = demo_mode;
Box::new(app) 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); print!("\r {}: [{:50}] {:.0}%", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0);
let _ = std::io::Write::flush(&mut std::io::stdout()); let _ = std::io::Write::flush(&mut std::io::stdout());
} }
Ok(InstallMessage::SubProgress(_)) => {}
Ok(InstallMessage::CurrentFile(_)) => {}
Ok(InstallMessage::Done) => { Ok(InstallMessage::Done) => {
println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description")); println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description"));
break; break;
@@ -229,7 +231,6 @@ impl Default for YaliApp {
Box::new(PartitionStep::default()), Box::new(PartitionStep::default()),
Box::new(UsersStep::default()), Box::new(UsersStep::default()),
Box::new(BootloaderStep::default()), Box::new(BootloaderStep::default()),
Box::new(DisplayManagerStep::default()),
Box::new(SummaryStep), Box::new(SummaryStep),
Box::new(ExecutionStep::default()), Box::new(ExecutionStep::default()),
Box::new(FinishStep), Box::new(FinishStep),
@@ -407,24 +408,24 @@ impl eframe::App for YaliApp {
} }
// Alt kısım: tema değiştirme // Alt kısım: tema değiştirme
// ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
// ui.add_space(6.0); ui.add_space(6.0);
// let theme_icon = if self.state.is_dark { "☀" } else { "🌙" }; let theme_icon = if self.state.is_dark { "" } else { "🌙" };
// let theme_label = if self.state.is_dark { "Light" } else { "Dark" }; let theme_label = if self.state.is_dark { "Light" } else { "Dark" };
// if ui.add( if ui.add(
// egui::Button::new( egui::Button::new(
// egui::RichText::new(format!("{} {}", theme_icon, theme_label)) egui::RichText::new(format!("{} {}", theme_icon, theme_label))
// .size(12.0) .size(12.0)
// ) )
// .rounding(egui::Rounding::same(6.0)) .rounding(egui::Rounding::same(6.0))
// .min_size(egui::Vec2::new(100.0, 28.0)) .min_size(egui::Vec2::new(100.0, 28.0))
// ).clicked() { ).clicked() {
// self.state.is_dark = !self.state.is_dark; self.state.is_dark = !self.state.is_dark;
// ui::theme::set_theme_mode(self.state.is_dark); ui::theme::set_theme_mode(self.state.is_dark);
// ui::theme::apply(ctx, self.state.is_dark); ui::theme::apply(ctx, self.state.is_dark);
// } }
// }); });
}); });
// ── Alt panel: navigasyon ──────────────────────────── // ── 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() { // if !is_exec && !is_finish && ui::theme::danger_button(ui, &t!("cancel")).clicked() {
// self.show_cancel_dialog = true; // self.show_cancel_dialog = true;
// }; // };
let next_clicked = if is_valid { let next_clicked = ui::theme::primary_button_enabled(ui, &next_label, is_valid).clicked();
ui::theme::primary_button(ui, &next_label).clicked()
} else {
ui::theme::secondary_button(ui, &next_label).clicked()
};
if next_clicked { if next_clicked {
self.state.current_step += 1; self.state.current_step += 1;
} }
+31 -28
View File
@@ -2,6 +2,7 @@ use eframe::egui;
use rust_i18n::t; use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep}; use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme; use crate::ui::theme;
use crate::funct::{form_label, form_input_text, form_input_password};
#[derive(Default)] #[derive(Default)]
pub struct BootloaderStep { pub struct BootloaderStep {
@@ -26,16 +27,26 @@ impl InstallerStep for BootloaderStep {
} }
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) { fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("bootloader_title")); theme::section_heading(ui, &t!("bootloader_title"));
ui.label(t!("bootloader_description")); ui.label(
egui::RichText::new(t!("bootloader_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(12.0); ui.add_space(12.0);
egui::Grid::new("bootloader_grid") // ─── Önyükleyici Formu ──────────────────────────────────
.num_columns(2) egui::Frame::group(ui.style())
.spacing([12.0, 10.0]) .fill(theme::c_bg_widget())
.rounding(6.0)
.inner_margin(egui::Margin::symmetric(10.0, 10.0))
.show(ui, |ui| { .show(ui, |ui| {
ui.set_width(ui.available_width());
ui.vertical(|ui| {
ui.add_space(10.0);
// Hedef aygıt // Hedef aygıt
ui.label(t!("bootloader_device_label")); form_label(ui, &t!("bootloader_device_label"));
ui.horizontal(|ui| { ui.horizontal(|ui| {
let mut device_list: Vec<String> = state.available_disks.iter() let mut device_list: Vec<String> = state.available_disks.iter()
.map(|d| d.name.clone()) .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); theme::selectable_value(ui, &mut state.bootloader_device, dev.clone(), dev.as_str(), w);
} }
}); });
});
ui.label( ui.label(
egui::RichText::new(t!("bootloader_device_hint")) egui::RichText::new(t!("bootloader_device_hint"))
.size(11.0) .size(11.0)
.color(egui::Color32::from_rgb(120, 120, 140)), .color(theme::c_text_dim()),
); );
}); ui.add_space(6.0);
ui.end_row();
// Timeout // Timeout
ui.label(t!("bootloader_timeout_label")); form_label(ui, &t!("bootloader_timeout_label"));
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.add(egui::Slider::new(&mut state.bootloader_timeout, 0..=60) ui.add(egui::Slider::new(&mut state.bootloader_timeout, 0..=60)
.text(t!("bootloader_timeout_suffix"))); .text(t!("bootloader_timeout_suffix")));
}); });
ui.end_row(); ui.add_space(6.0);
// GRUB şifresi // GRUB şifresi
ui.label(t!("bootloader_password_label")); form_label(ui, &t!("bootloader_password_label"));
ui.horizontal(|ui| { ui.horizontal(|ui| {
if self.show_password { let show_pwd = &mut self.show_password;
ui.text_edit_singleline(&mut state.bootloader_password); form_input_password(ui, &mut state.bootloader_password, show_pwd);
} 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), "");
}
}); });
ui.end_row(); ui.add_space(6.0);
// Kernel parametreleri // Kernel parametreleri
ui.label(t!("bootloader_kernel_label")); form_label(ui, &t!("bootloader_kernel_label"));
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.kernel_options); form_input_text(ui, &mut state.kernel_options);
});
ui.label( ui.label(
egui::RichText::new(t!("bootloader_kernel_hint")) egui::RichText::new(t!("bootloader_kernel_hint"))
.size(11.0) .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())); self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone()));
state.install_progress = 0.0; state.install_progress = 0.0;
state.install_sub_progress = 0.0;
state.install_log.clear(); state.install_log.clear();
state.install_current_file = None; state.install_current_file = None;
state.install_log.push(t!("install_starting").to_string()); state.install_log.push(t!("install_starting").to_string());
@@ -182,6 +183,12 @@ impl InstallerStep for ExecutionStep {
Ok(InstallMessage::Progress(p)) => { Ok(InstallMessage::Progress(p)) => {
state.install_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) => { Ok(InstallMessage::Done) => {
state.install_progress = 1.0; state.install_progress = 1.0;
self.finished = true; self.finished = true;
@@ -305,6 +312,16 @@ impl InstallerStep for ExecutionStep {
.animate(running) .animate(running)
.desired_height(10.0), .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 // Durum etiketi
+139 -38
View File
@@ -1,4 +1,4 @@
use eframe::egui::{self, Color32}; use eframe::egui;
use rust_i18n::t; use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep}; use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme; use crate::ui::theme;
@@ -92,6 +92,7 @@ fn split_code_desc(line: &str) -> Option<(&str, &str)> {
Some((code, desc)) Some((code, desc))
} }
} }
fn layout_display_name(layout: &str, layouts: &[(String, String)]) -> String { fn layout_display_name(layout: &str, layouts: &[(String, String)]) -> String {
layouts layouts
.iter() .iter()
@@ -112,7 +113,6 @@ fn variant_display_name(
.unwrap_or_else(|| variant.to_string()) .unwrap_or_else(|| variant.to_string())
} }
fn apply_system_keyboard_layout(layout: &str, variant: &str) { fn apply_system_keyboard_layout(layout: &str, variant: &str) {
if std::env::var("DISPLAY").is_ok() { if std::env::var("DISPLAY").is_ok() {
let mut cmd = std::process::Command::new("setxkbmap"); let mut cmd = std::process::Command::new("setxkbmap");
@@ -141,7 +141,6 @@ impl InstallerStep for KeyboardStep {
} }
fn on_enter(&mut self, state: &mut GlobalState) { fn on_enter(&mut self, state: &mut GlobalState) {
// Varsayılan düzen sistemi diline göre seçilir
if state.keyboard_layout.is_empty() { if state.keyboard_layout.is_empty() {
state.keyboard_layout = if state.language == "tr" { state.keyboard_layout = if state.language == "tr" {
"tr".to_string() "tr".to_string()
@@ -162,64 +161,166 @@ impl InstallerStep for KeyboardStep {
ui.add_space(12.0); ui.add_space(12.0);
// ── Klavye görseli (xkbcommon ile çizilir, temaya uyumlu) // ── Klavye görseli ───────────────────────────────────
if let Some(kle) = crate::ui::kle_svg::compile_layout(&state.keyboard_layout, &state.keyboard_variant) { 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)); let max = egui::Vec2::new(ui.available_width(), ui.available_height().max(100.0));
crate::ui::kle_svg::draw_layout(ui, &kle, max); crate::ui::kle_svg::draw_layout(ui, &kle, max);
}
ui.add_space(12.0); ui.add_space(12.0);
// ── Layout ve Varyant ComboBox'ları yan yana ────────────── // ── Label genişlik hesaplama ──────────────────────────
let available_variants: Vec<_> = self let layout_label = t!("keyboard_layout_label").to_string();
.variants let variant_label = t!("keyboard_variant_label").to_string();
.iter() let label_w = ui.fonts(|f| {
.filter(|(layout, _, _)| layout == &state.keyboard_layout) let measure = |s: &str| -> f32 {
.collect(); f.layout_no_wrap(s.to_string(), egui::FontId::proportional(14.0), egui::Color32::WHITE)
.size().x
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 }
}; };
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.horizontal(|ui| {
ui.vertical(|ui| {
ui.add_space(6.0); 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); 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 { 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) { if !self.variants.iter().any(|(layout, _, _)| layout == code) {
state.keyboard_variant.clear(); state.keyboard_variant.clear();
} }
apply_system_keyboard_layout(&state.keyboard_layout, &state.keyboard_variant); 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| { if is_default_target {
ui.add_space(6.0); state.keyboard_variant.clear();
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() {
apply_system_keyboard_layout(&state.keyboard_layout, ""); 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); 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.label(t!("keyboard_test_label"));
ui.add_space(4.0); ui.add_space(4.0);
egui::Frame::group(ui.style()) egui::Frame::group(ui.style())
.fill(Color32::from_rgb(255, 255, 255)) .fill(theme::c_keyboard_bg())
.rounding(6.0) .rounding(6.0)
.show(ui, |ui| { .show(ui, |ui| {
ui.add( ui.add(
+1 -2
View File
@@ -7,7 +7,7 @@ pub mod partition;
pub mod bootloader; pub mod bootloader;
pub mod network; pub mod network;
pub mod rescue; pub mod rescue;
pub mod display_manager; //pub mod display_manager;
pub mod netinstall; pub mod netinstall;
pub mod location; pub mod location;
@@ -20,5 +20,4 @@ pub use partition::PartitionStep;
pub use bootloader::BootloaderStep; pub use bootloader::BootloaderStep;
pub use network::NetworkStep; pub use network::NetworkStep;
pub use rescue::RescueStep; pub use rescue::RescueStep;
pub use display_manager::DisplayManagerStep;
pub use location::LocationStep; 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ı. /// Kullanıcı adı, şifre, hostname ve yönetici (root) şifresi ayarları adımı.
pub struct UsersStep { pub struct UsersStep {
use_same_for_admin: bool, use_same_for_admin: bool,
autologin: bool,
show_password: bool, show_password: bool,
show_password_confirm: bool, show_password_confirm: bool,
show_root_password: bool, // <-- Eklendi show_root_password: bool,
show_root_password_confirm: bool, // <-- Eklendi show_root_password_confirm: bool,
} }
impl Default for UsersStep { impl Default for UsersStep {
fn default() -> Self { fn default() -> Self {
Self { Self {
use_same_for_admin: true, use_same_for_admin: true,
autologin: false,
show_password: false, show_password: false,
show_password_confirm: false, show_password_confirm: false,
show_root_password: false, show_root_password: false,
@@ -30,16 +32,22 @@ impl Default for UsersStep {
/// - Boşlukları `-`'ye çevirir /// - Boşlukları `-`'ye çevirir
/// - Geçersiz karakterleri temizler /// - Geçersiz karakterleri temizler
fn derive_username(real_name: &str) -> String { fn derive_username(real_name: &str) -> String {
let mut name = real_name.to_lowercase(); let name = real_name
name = name
.replace('ş', "s") .replace('ş', "s")
.replace('Ş', "s")
.replace('ğ', "g") .replace('ğ', "g")
.replace('Ğ', "g")
.replace('ı', "i") .replace('ı', "i")
.replace('İ', "i")
.replace('ö', "o") .replace('ö', "o")
.replace('Ö', "o")
.replace('ü', "u") .replace('ü', "u")
.replace('Ü', "u")
.replace('ç', "c") .replace('ç', "c")
.replace('Ç', "c")
.replace(' ', "-") .replace(' ', "-")
.replace('_', "-"); .replace('_', "-")
.to_lowercase();
let valid: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect(); let valid: String = name.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_').collect();
let trimmed = valid.trim_matches('-').to_string(); let trimmed = valid.trim_matches('-').to_string();
if trimmed.is_empty() { 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. /// Kullanıcı adının geçerliliğini kontrol eder.
/// Linux kuralları: küçük harf veya alt çizgi ile başlamalı, /// Linux kuralları: küçük harf veya alt çizgi ile başlamalı,
/// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli. /// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli.
fn is_valid_username(s: &str) -> bool { /// Ayrıca sistem kullanıcı adları yasaklıdır.
if s.is_empty() || s.len() > 32 { /// Hata varsa hata mesajını döndürür, yoksa None döndürür.
return false; 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(); let mut chars = s.chars();
// İlk karakter küçük harf veya alt çizgi olmalı // İlk karakter küçük harf veya alt çizgi olmalı
match chars.next() { match chars.next() {
Some(c) if c.is_ascii_lowercase() || c == '_' => {} 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. /// 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) { 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 // Gerçek ad varsa ama kullanıcı adı yoksa türet
if !state.real_name.is_empty() && state.username.is_empty() { if !state.real_name.is_empty() && state.username.is_empty() {
state.username = derive_username(&state.real_name); state.username = derive_username(&state.real_name);
@@ -155,7 +193,7 @@ impl InstallerStep for UsersStep {
// ─── Kullanıcı Bilgileri Formu ────────────────────────────────── // ─── Kullanıcı Bilgileri Formu ──────────────────────────────────
egui::Frame::group(ui.style()) egui::Frame::group(ui.style())
.fill(egui::Color32::from_rgb(240, 243, 198)) .fill(theme::c_bg_widget())
.rounding(6.0) .rounding(6.0)
.inner_margin(egui::Margin::symmetric(10.0, 10.0)) .inner_margin(egui::Margin::symmetric(10.0, 10.0))
.show(ui, |ui| { .show(ui, |ui| {
@@ -168,28 +206,55 @@ impl InstallerStep for UsersStep {
form_label(ui, &t!("real_name_label")); form_label(ui, &t!("real_name_label"));
{ {
let prev = state.real_name.clone(); let prev = state.real_name.clone();
ui.horizontal(|ui| {
form_input_text(ui, &mut state.real_name); 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() { if state.real_name != prev && !state.real_name.is_empty() {
state.username = derive_username(&state.real_name); 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); ui.add_space(6.0);
// Kullanıcı adı (otomatik türetilir, düzenlenebilir) // Kullanıcı adı (otomatik türetilir, düzenlenebilir)
form_label(ui, &t!("username_label")); form_label(ui, &t!("username_label"));
ui.horizontal(|ui| {
form_input_text(ui, &mut state.username); form_input_text(ui, &mut state.username);
if !state.username.is_empty() { if !state.username.is_empty() {
if is_valid_username(&state.username) { if let Some(_err) = validate_username(&state.username) {
ui.colored_label(theme::c_success(), ""); 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 { } 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); ui.add_space(6.0);
// Şifre // Şifre
@@ -221,27 +286,50 @@ impl InstallerStep for UsersStep {
form_label(ui, &t!("password_confirm_label")); form_label(ui, &t!("password_confirm_label"));
{ {
let show_pwd = &mut self.show_password_confirm; let show_pwd = &mut self.show_password_confirm;
ui.horizontal(|ui| {
form_input_password(ui, &mut state.password_confirm, show_pwd); form_input_password(ui, &mut state.password_confirm, show_pwd);
}
if !state.password_confirm.is_empty() { if !state.password_confirm.is_empty() {
if state.password == state.password_confirm { 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 { } 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); ui.add_space(6.0);
// Hostname // Hostname
form_label(ui, &t!("hostname_label")); form_label(ui, &t!("hostname_label"));
ui.horizontal(|ui| {
form_input_text(ui, &mut state.hostname); form_input_text(ui, &mut state.hostname);
if !state.hostname.is_empty() { if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) { if is_valid_hostname(&state.hostname) {
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 { } 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); ui.add_space(10.0);
@@ -250,6 +338,10 @@ impl InstallerStep for UsersStep {
ui.add_space(14.0); 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ı ───────────────────────────────────── // ─── Yönetici (root) Ayarları ─────────────────────────────────────
form_checkbox(ui, &mut self.use_same_for_admin, &t!("use_same_for_admin")); 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 // DÜZELTME: 3. parametre olarak show_root_password_confirm eklendi
let show_root_pwd_conf = &mut self.show_root_password_confirm; 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); 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 { 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 { } 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")); 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 { 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 rounding = Rounding::same(6.0);
let min_size = Vec2::new(100.0, 32.0); let min_size = Vec2::new(100.0, 32.0);
let padding = Vec2::new(12.0, 6.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), 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; let n_strips = 16;
for i in 0..n_strips { for i in 0..n_strips {
let t = i as f32 / n_strips as f32; let t = i as f32 / n_strips as f32;
let t2 = (i + 1) 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 y0 = rect.top() + rect.height() * t;
let y1 = rect.top() + rect.height() * t2; let y1 = rect.top() + rect.height() * t2;
let strip_rect = egui::Rect::from_min_max( 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); ui.painter().rect(strip_rect, Rounding::ZERO, c, Stroke::NONE);
} }
if response.hovered() { ui.painter().rect(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0, actual_border));
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().galley( ui.painter().galley(
egui::pos2(rect.center().x - galley.size().x / 2.0, rect.center().y - galley.size().y / 2.0), egui::pos2(rect.center().x - galley.size().x / 2.0, rect.center().y - galley.size().y / 2.0),
galley, galley,
text_color, actual_text,
); );
if let Some(cursor) = ui.visuals().interact_cursor { 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 { 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, gradient_button(ui, text,
Color32::from_rgb(50, 130, 255), Color32::from_rgb(50, 130, 255),
Color32::from_rgb(13, 110, 253), Color32::from_rgb(13, 110, 253),
Color32::WHITE, Color32::WHITE,
Color32::from_rgb(70, 150, 255), Color32::from_rgb(70, 150, 255),
Color32::from_rgb(30, 130, 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 { 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, gradient_button(ui, text,
Color32::from_rgb(130, 140, 150), // top Color32::from_rgb(130, 140, 150), // top
Color32::from_rgb(100, 110, 120), // bottom Color32::from_rgb(100, 110, 120), // bottom
Color32::from_rgb(255, 255, 255), // text color32 Color32::from_rgb(255, 255, 255), // text color32
Color32::from_rgb(150, 160, 170), // hover top Color32::from_rgb(150, 160, 170), // hover top
Color32::from_rgb(120, 130, 140), // hover bottom 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 { 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, gradient_button(ui, text,
Color32::from_rgb(240, 83, 99), Color32::from_rgb(240, 83, 99),
Color32::from_rgb(200, 40, 60), Color32::from_rgb(200, 40, 60),
Color32::WHITE, Color32::WHITE,
Color32::from_rgb(255, 100, 120), Color32::from_rgb(255, 100, 120),
Color32::from_rgb(220, 60, 80), Color32::from_rgb(220, 60, 80),
Color32::from_rgb(200, 40, 60)) Color32::from_rgb(200, 40, 60),
enabled)
} }
#[allow(dead_code)] #[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(102, 102, 102),
Color32::from_rgb(246, 246, 246), Color32::from_rgb(246, 246, 246),
Color32::from_rgb(255, 255, 255), Color32::from_rgb(255, 255, 255),
Color32::from_rgb(177, 177, 177)) Color32::from_rgb(177, 177, 177),
true)
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn btn_dark_blue(ui: &mut egui::Ui, text: &str) -> egui::Response { 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::WHITE,
Color32::from_rgb(70, 140, 207), Color32::from_rgb(70, 140, 207),
Color32::from_rgb(99, 184, 238), Color32::from_rgb(99, 184, 238),
Color32::from_rgb(56, 102, 163)) Color32::from_rgb(56, 102, 163),
true)
} }
#[allow(dead_code)] #[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(102, 102, 102), // metin: #666666
Color32::from_rgb(233, 233, 233), // hover üst (ters) Color32::from_rgb(233, 233, 233), // hover üst (ters)
Color32::from_rgb(249, 249, 249), // hover alt (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 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). /// Stil uygulanmış ComboBox oluşturur (genişlik ayarı yapılmaz, Grid içinde kullanım için).
pub fn combo_box( pub fn combo_box(
@@ -48,13 +48,15 @@ pub fn selectable_value<T: PartialEq>(
draw_gradient_bg(ui, rect); draw_gradient_bg(ui, rect);
ui.put(rect, |ui: &mut egui::Ui| { let text_color = combobox_stil_text_color(ui);
ui.horizontal(|ui| { let painter = ui.painter();
ui.add_space(6.0); painter.text(
ui.colored_label(DROPDOWN_TEXT_COLOR, label); egui::pos2(rect.left() + 6.0, rect.center().y),
}) egui::Align2::LEFT_CENTER,
.response label,
}); egui::FontId::proportional(14.0),
text_color,
);
if response.clicked() { if response.clicked() {
*current = value; *current = value;
@@ -62,6 +64,15 @@ pub fn selectable_value<T: PartialEq>(
response.on_hover_cursor(egui::CursorIcon::PointingHand) 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). /// ComboBox dropdown öğesi — özel içerik çizmek için (ör. bayrak + metin).
/// `add_contents` callback'i verilen `Rect` içine içerik ekler. /// `add_contents` callback'i verilen `Rect` içine içerik ekler.
pub fn selectable_value_custom<R>( pub fn selectable_value_custom<R>(
+35 -32
View File
@@ -1,64 +1,67 @@
use eframe::egui::{self, Color32, Rect, Stroke}; use eframe::egui::{self, Color32, Rect, Stroke};
use egui::epaint::Mesh; 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. /// Verilen `Visuals` yapısına ComboBox stillerini uygular.
pub fn apply_visuals(v: &mut egui::Visuals) { pub fn apply_visuals(v: &mut egui::Visuals) {
v.widgets.inactive.weak_bg_fill = WEAK_BG_INACTIVE; v.widgets.inactive.weak_bg_fill = Color32::from_rgb(224, 224, 224);
v.widgets.hovered.weak_bg_fill = WEAK_BG_HOVERED; v.widgets.hovered.weak_bg_fill = Color32::from_rgb(200, 200, 200);
v.widgets.active.weak_bg_fill = WEAK_BG_ACTIVE; 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.hovered.fg_stroke = Stroke::new(1.5, Color32::WHITE);
v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE); v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE);
// Kenarlıklar v.widgets.inactive.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
v.widgets.inactive.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT); v.widgets.hovered.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
v.widgets.hovered.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT); v.widgets.active.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
v.widgets.active.bg_stroke = Stroke::new(1.0, COMBO_BORDER_ACCENT);
} }
/// 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) { 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 mut mesh = Mesh::default();
let idx = mesh.vertices.len() as u32; let idx = mesh.vertices.len() as u32;
mesh.vertices.push(egui::epaint::Vertex { mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.left(), rect.top()), pos: egui::pos2(rect.left(), rect.top()),
uv: egui::pos2(0.0, 0.0), uv: egui::pos2(0.0, 0.0),
color: GRADIENT_TOP, color: top,
}); });
mesh.vertices.push(egui::epaint::Vertex { mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.right(), rect.top()), pos: egui::pos2(rect.right(), rect.top()),
uv: egui::pos2(0.0, 0.0), uv: egui::pos2(0.0, 0.0),
color: GRADIENT_TOP, color: top,
}); });
mesh.vertices.push(egui::epaint::Vertex { mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.right(), rect.bottom()), pos: egui::pos2(rect.right(), rect.bottom()),
uv: egui::pos2(0.0, 0.0), uv: egui::pos2(0.0, 0.0),
color: GRADIENT_BOT, color: bot,
}); });
mesh.vertices.push(egui::epaint::Vertex { mesh.vertices.push(egui::epaint::Vertex {
pos: egui::pos2(rect.left(), rect.bottom()), pos: egui::pos2(rect.left(), rect.bottom()),
uv: egui::pos2(0.0, 0.0), 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]); mesh.indices.extend_from_slice(&[idx, idx + 1, idx + 2, idx + 2, idx + 3, idx]);
ui.painter().add(egui::Shape::mesh(mesh)); 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; use xkbcommon::xkb;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -10,36 +10,16 @@ pub struct XkbKey {
#[derive(Debug)] #[derive(Debug)]
pub struct XkbLayout { pub struct XkbLayout {
pub keys: Vec<XkbKey>, pub keys: Vec<XkbKey>,
pub width: f32, pub height: f32,
} }
/// ISO105 tuşlu klavye için XKB adı → (x, y, w, h) birim cinsinden. pub fn empty_layout() -> XkbLayout {
/// x,y ondalıklı olabilir (geniş tuşlar için). XkbLayout { keys: vec![] }
}
fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> { fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
// Satır y offset'leri (birim)
let r = |row| row as f32; let r = |row| row as f32;
// Standart ISO105 düzeni. x değerleri kümülatif.
match name { 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)), "TLDE" => Some((0.0, r(1), 1.0, 1.0)),
"AE01" => Some((1.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)), "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)), "AE10" => Some((10.0, r(1), 1.0, 1.0)),
"AE11" => Some((11.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)), "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)), "TAB" => Some((0.0, r(2), 1.5, 1.0)),
"AD01" => Some((1.5, r(2), 1.0, 1.0)), "AD01" => Some((1.5, r(2), 1.0, 1.0)),
"AD02" => Some((2.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)), "AD10" => Some((10.5, r(2), 1.0, 1.0)),
"AD11" => Some((11.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)), "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)), "CAPS" => Some((0.0, r(3), 1.75, 1.0)),
"AC01" => Some((1.75, r(3), 1.0, 1.0)), "AC01" => Some((1.75, r(3), 1.0, 1.0)),
"AC02" => Some((2.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)), "AC10" => Some((10.75,r(3), 1.0, 1.0)),
"AC11" => Some((11.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)), "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)), "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)), "AB01" => Some((2.25, r(4), 1.0, 1.0)),
"AB02" => Some((3.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)), "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)), "AB08" => Some((9.25, r(4), 1.0, 1.0)),
"AB09" => Some((10.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)), "AB10" => Some((11.25,r(4), 1.0, 1.0)),
"AB11" => Some((12.25,r(4), 1.0, 1.0)), "RTSH" => Some((12.25,r(4), 3.0, 1.0)),
"RTSH" => Some((13.25,r(4), 2.75, 1.0)),
// ── Space bar row ─────────────
"LCTL" => Some((0.0, r(5), 1.25, 1.0)), "LCTL" => Some((0.0, r(5), 1.25, 1.0)),
"LWIN" => Some((1.25, 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)), "LALT" => Some((2.5, r(5), 1.5, 1.0)),
"SPCE" => Some((4.0, r(5), 6.25, 1.0)), "SPCE" => Some((4.0, r(5), 6.25, 1.0)),
"RALT" | "LVL3" => Some((10.25, r(5), 1.5, 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((11.75, r(5), 1.25, 1.0)),
"COMP" => Some((13.0, r(5), 1.25, 1.0)), "RCTL" => Some((13.0, r(5), 2.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)),
_ => None, _ => None,
} }
@@ -162,94 +92,76 @@ fn key_position(name: &str) -> Option<(f32, f32, f32, f32)> {
fn keysym_to_label(sym: xkb::Keysym) -> String { fn keysym_to_label(sym: xkb::Keysym) -> String {
let utf = xkb::keysym_to_utf8(sym); let utf = xkb::keysym_to_utf8(sym);
if !utf.is_empty() { return utf; } if utf.chars().any(|c| !c.is_control()) {
let trimmed = utf.trim();
let name = xkb::keysym_get_name(sym); if !trimmed.is_empty() {
if name == "VoidSymbol" || name == "NoSymbol" { return trimmed.to_uppercase();
return String::new();
} }
}
// 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() { match name.as_str() {
"BackSpace" => "".into(), "BackSpace" => "Bksp".into(),
"Tab" => "".into(), "Tab" => "Tab".into(),
"Return" => "".into(), "Return" => "Enter".into(),
"Caps_Lock" => "".into(), "Caps_Lock" => "Caps".into(),
"Shift_L" => "".into(), "Shift_L" | "Shift_R" => "Shift".into(),
"Shift_R" => "".into(), "Control_L" | "Control_R" => "Ctrl".into(),
"Control_L" => "Ctrl".into(),
"Control_R" => "Ctrl".into(),
"Alt_L" => "Alt".into(), "Alt_L" => "Alt".into(),
"Alt_R" => "AltGr".into(), "Alt_R" | "ISO_Level3_Shift" => "AltGr".into(),
"ISO_Level3_Shift" => "AltGr".into(), "Super_L" | "Super_R" => "Win".into(),
"Super_L" => "Win".into(),
"Super_R" => "Win".into(),
"Menu" => "Menü".into(), "Menu" => "Menü".into(),
"Escape" => "Esc".into(), "Escape" => "Esc".into(),
"Delete" => "Del".into(),
"Insert" => "Ins".into(),
"Prior" => "PgUp".into(),
"Next" => "PgDn".into(),
"space" => "".into(), "space" => "".into(),
"Multi_key" => "Compose".into(),
_ if name.starts_with("XF86") => String::new(),
_ => name, _ => name,
} }
} }
/// XKB keymap'inden klavye düzenini oluşturur.
pub fn compile_layout(layout: &str, variant: &str) -> Option<XkbLayout> { pub fn compile_layout(layout: &str, variant: &str) -> Option<XkbLayout> {
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); let mut context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
let model = if std::path::Path::new("/usr/share/X11/xkb/symbols/").exists() { context.set_log_level(xkb::LogLevel::Critical);
"pc105"
} else {
"pc105"
};
let keymap = xkb::Keymap::new_from_names::<str>( let keymap = xkb::Keymap::new_from_names::<str>(
&context, &context, "", "pc105", layout,
"", // rules if variant.is_empty() { "" } else { variant },
model, // model None::<String>, xkb::COMPILE_NO_FLAGS,
layout, // layout
if variant.is_empty() { "" } else { variant }, // variant
None::<String>, // options
xkb::COMPILE_NO_FLAGS,
)?; )?;
let min = keymap.min_keycode(); let min = keymap.min_keycode();
let max = keymap.max_keycode(); let max = keymap.max_keycode();
let mut keys = Vec::new(); let mut keys = Vec::new();
let mut min_x = f32::MAX;
let mut min_y = 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) { for code_u32 in (min.raw() as u32)..=(max.raw() as u32) {
let code = xkb::Keycode::new(code_u32); let code = xkb::Keycode::new(code_u32);
let Some(name) = keymap.key_get_name(code) else { continue }; 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 }; 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(); let mut raw_labels = Vec::new();
for level in 0..4 { for level in 0..4 {
let syms = keymap.key_get_syms_by_level(code, 0, level); let syms = keymap.key_get_syms_by_level(code, 0, level);
let label = syms.iter() let label = syms.iter().map(|&s| keysym_to_label(s)).collect::<Vec<_>>().join("");
.map(|&s| keysym_to_label(s))
.collect::<Vec<_>>()
.join("");
if !label.is_empty() && !raw_labels.contains(&label) { if !label.is_empty() && !raw_labels.contains(&label) {
raw_labels.push(label); raw_labels.push(label);
} }
} }
// Her tuşta sadece 1 karakter göster (büyük harf tercihli)
let labels: Vec<String> = { let labels: Vec<String> = {
let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect(); let single: Vec<_> = raw_labels.iter().filter(|l| l.chars().count() == 1).collect();
if single.is_empty() { if single.is_empty() {
// Hiç tek karakter yoksa ilk label'ı göster (F1, Esc, …)
raw_labels.into_iter().take(1).collect() raw_labels.into_iter().take(1).collect()
} else { } 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())); 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() }] 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 }); keys.push(XkbKey { x, y, w, h, labels });
if x < min_x { min_x = x; }
if y < min_y { min_y = y; } 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 { for key in &mut keys {
key.x -= min_x;
key.y -= min_y; key.y -= min_y;
} }
Some(XkbLayout { Some(XkbLayout {
keys, 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) { pub fn draw_layout(ui: &mut egui::Ui, layout: &XkbLayout, max_size: Vec2) {
let margin = 6.0; let margin = 6.0;
let spacing = 0.08;
let corner = 3.0;
let uw = (max_size.x - margin * 2.0) / layout.width; let svg_size = Vec2::new(840.0, 300.0);
let uh = (max_size.y - margin * 2.0) / layout.height; let img_scale = ((max_size.x - margin * 2.0) / svg_size.x)
let unit = uw.min(uh).max(12.0); .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 (_id, resp) = ui.allocate_exact_size(total_size, egui::Sense::hover());
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 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 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 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 img_rect = egui::Rect::from_min_size(origin, draw_size);
let small_size = (unit * 0.22).max(5.0); 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 { for key in &layout.keys {
let kx = origin.x + margin + key.x * unit + sp; if let Some(lbl) = key.labels.first() {
let ky = origin.y + margin + key.y * unit + sp; if lbl == "_RTRN_BOT" { continue; }
let kw = key.w * unit - sp * 2.0; if key.y == 1.0 && (key.x - 13.5).abs() < 0.1 {
let kh = key.h * unit - sp * 2.0; 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)); let ex = origin.x + (svg_cx - svg_w / 2.0) * img_scale;
painter.rect(key_rect, Rounding::same(corner), key_fill, Stroke::new(key_stroke.width.max(0.5), key_stroke.color)); 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() { if let Some(lbl) = key.labels.first() {
let lbl = label.trim(); let trimmed = lbl.trim();
if lbl.is_empty() { continue; } if trimmed.is_empty() { continue; }
let (lx, ly, sz) = match i {
0 => (kx + kw * 0.5, ky + kh * 0.3, font_size), let text_pos = egui::pos2(ex + ew / 2.0, ey + eh / 2.0);
1 => (kx + kw * 0.5, ky + kh * 0.65, small_size),
_ => (kx + kw * 0.5, ky + kh * 0.9, small_size), painter.text(text_pos, egui::Align2::CENTER_CENTER, trimmed, egui::FontId::proportional(font_size), text_color);
}; }
}
if !enter_text.is_empty() {
painter.text( painter.text(
egui::pos2(lx, ly), egui::Align2::CENTER_CENTER, egui::pos2(origin.x + ENTER_VBX * img_scale, origin.y + ENTER_VBY * img_scale),
lbl, egui::FontId::proportional(sz), text_color, 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_DARK: AtomicU32 = AtomicU32::new(0x121214);
static BG_PANEL: AtomicU32 = AtomicU32::new(0x1A1A1E); static BG_PANEL: AtomicU32 = AtomicU32::new(0x1A1A1E);
static BG_WIDGET: AtomicU32 = AtomicU32::new(0x242428); static BG_WIDGET: AtomicU32 = AtomicU32::new(0x242428);
static BG_FOOTER: AtomicU32 = AtomicU32::new(0x121214);
static BORDER: AtomicU32 = AtomicU32::new(0x323238); static BORDER: AtomicU32 = AtomicU32::new(0x323238);
@@ -26,6 +27,8 @@ static ERROR: AtomicU32 = AtomicU32::new(0xF44336);
static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D); static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D);
static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b) static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b)
static SIDEBAR_TEXT: AtomicU32 = AtomicU32::new(0x000000); 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_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() -> 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_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering::Relaxed)) }
pub fn c_footer_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(0xf7f7f7) } 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) { pub fn set_theme_mode(is_dark: bool) {
if is_dark { if is_dark {
BG_FOOTER.store(0x121214, Ordering::Relaxed);
BG_DARK.store(0x121214, Ordering::Relaxed); BG_DARK.store(0x121214, Ordering::Relaxed);
BG_PANEL.store(0x1A1A1E, Ordering::Relaxed); BG_PANEL.store(0x1A1A1E, Ordering::Relaxed);
BG_WIDGET.store(0x242428, Ordering::Relaxed); BG_WIDGET.store(0x242428, Ordering::Relaxed);
BORDER.store(0x323238, Ordering::Relaxed); BORDER.store(0x323238, Ordering::Relaxed);
TEXT.store(0xF3F3F5, Ordering::Relaxed); TEXT.store(0xF3F3F5, Ordering::Relaxed); // #F3F3F5
TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed); TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed); // #9E9EAF
SIDEBAR.store(0x0B0B0D, Ordering::Relaxed); SIDEBAR.store(0x0B0B0D, Ordering::Relaxed);
SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed);
KEYBOARD_BG.store(0x000000, Ordering::Relaxed);
BG_ROOT.store(0x1A1A1E, Ordering::Relaxed);
} else { } else {
BG_FOOTER.store(0xf7f7f7, Ordering::Relaxed);
BG_DARK.store(0xf7f7f7, Ordering::Relaxed); BG_DARK.store(0xf7f7f7, Ordering::Relaxed);
SIDEBAR.store(0x292f34, Ordering::Relaxed); SIDEBAR.store(0x292f34, Ordering::Relaxed);
TEXT.store(0x000000, Ordering::Relaxed); TEXT.store(0x000000, Ordering::Relaxed); // #000000
TEXT_DIM.store(0x000000, Ordering::Relaxed); TEXT_DIM.store(0x000000, Ordering::Relaxed); // #000000
SIDEBAR_TEXT.store(0xffffff, Ordering::Relaxed); 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_DARK.store(0xF0F0F5, Ordering::Relaxed);
// BG_PANEL.store(0xFFFFFF, Ordering::Relaxed); BG_PANEL.store(0xFFFFFF, Ordering::Relaxed);
// BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed); BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed);
// BORDER.store(0xD0D0DA, Ordering::Relaxed); BORDER.store(0xD0D0DA, Ordering::Relaxed);
// TEXT.store(0x20202A, Ordering::Relaxed); //TEXT.store(0x20202A, Ordering::Relaxed); // #20202A
// TEXT_DIM.store(0x60606A, Ordering::Relaxed); //TEXT_DIM.store(0x60606A, Ordering::Relaxed); // #60606A
//SIDEBAR.store(0xE4E4EA, Ordering::Relaxed); //SIDEBAR.store(0xE4E4EA, Ordering::Relaxed);
} }
} }
@@ -183,13 +193,13 @@ fn build_visuals(is_dark: bool) -> Visuals {
v.widgets.noninteractive.bg_fill = c_bg_widget(); v.widgets.noninteractive.bg_fill = c_bg_widget();
// Pasif Hali: Widget arka planı (TextEdit, ComboBox giriş alanı) // 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 // Ü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 // 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) // ComboBox stilleri (combobox_stil.rs)
crate::ui::combobox_stil::apply_visuals(&mut v); crate::ui::combobox_stil::apply_visuals(&mut v);