Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af33334b2c | |||
| 546bb79be7 | |||
| 5f78df981a | |||
| 03cc46098c | |||
| 4f12334dd6 | |||
| 5063d9d9dd | |||
| 20f48bdf99 | |||
| cedbb6a741 | |||
| bd407adab3 | |||
| c6a1d0fb6f | |||
| f150ab0002 | |||
| 70f3737f01 | |||
| 008c7a57b8 | |||
| ad8bc0c506 | |||
| 7ff5c4b783 | |||
| 948e1d548d | |||
| a47c6e52bc | |||
| 3104adbc41 | |||
| d87664d191 | |||
| 332d5a787f | |||
| b2f35cb4dc | |||
| 618bcbc023 | |||
| bee04fb554 | |||
| 3b9a128704 | |||
| b6f9af3b40 | |||
| 4c68c5018e | |||
| 8d652c3a99 | |||
| bf6cf727fe | |||
| c02d732bef | |||
| d161556c73 | |||
| 8c6dc08361 | |||
| a5d46279b8 | |||
| 8633422c22 | |||
| 7e1bf45998 | |||
| 22de7d5255 | |||
| 75e2b1bf5e | |||
| dcd3ec4efb | |||
| 7209d58ea0 | |||
| 0a02f576d4 | |||
| 5c148472c4 | |||
| 379d32ac38 | |||
| 91acde5317 | |||
| fb905d9714 | |||
| 80e3559d0d | |||
| 20dc937ab8 | |||
| 1e920e0717 | |||
| 62000b7c87 | |||
| 3194c4d8a3 | |||
| 7621507acd |
@@ -1,6 +1,24 @@
|
||||
# Rust build outputs
|
||||
/target
|
||||
.cargo/registry/
|
||||
|
||||
# Lock files (bırakalım, bazen kontrol edilir)
|
||||
Cargo.lock
|
||||
|
||||
# IDE settings
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Temporary files
|
||||
*.swp
|
||||
*.swo
|
||||
.vscode/
|
||||
*.bak
|
||||
|
||||
# Eski sürüm yedekleri (git'e alınmamalı — git zaten var)
|
||||
src/steps/*_orj*.rs*
|
||||
src/steps/partition.rs_[0-9]*
|
||||
*_copy.rs
|
||||
*_copy.toml*
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"rust-analyzer.checkOnSave": false
|
||||
}
|
||||
@@ -0,0 +1,966 @@
|
||||
# AGENTS.md — Yali-rs Geliştirme Notları
|
||||
|
||||
> Ajan oturumları arasında süreklilik için.
|
||||
> Son güncelleme: **26 Eylül 2026** — çoklu disk bölümleme + güvenlik düzeltmeleri.
|
||||
|
||||
---
|
||||
|
||||
## 🚨 EN ÖNEMLİ: 22de7d5 regresyonu (25 Ağu 2026)
|
||||
|
||||
**Bu dosyayı okuyan her ajan önce bu bölümü okumalı.**
|
||||
|
||||
Commit `22de7d5` — *"Dead code uyarılarını gidermek için `#[allow(dead_code)]`
|
||||
eklendi"* (Devin AI ajanı) — mesajı üç tane `#[allow]` eklemekle
|
||||
sınırlıydı. Gerçekte **`src/steps/partition.rs`'i 2438 satırdan 811
|
||||
satıra düşürdü: 1804 satır silindi.**
|
||||
|
||||
```
|
||||
src/steps/partition.rs | 1978 +++++-------------------------------------------
|
||||
181 insertions(+), 1804 deletions(-)
|
||||
```
|
||||
|
||||
### Silinenler
|
||||
Tüm manuel bölümleme arayüzü: ekle/düzenle/sil onayı/LVM VG/LVM LV
|
||||
diyalogları, `draw_partition_table_header`, `manual_dual_bars`,
|
||||
`TableRow` enum'u, `disk_size_mb`, `used_mb_on_disk`, `draw_layout_bar`,
|
||||
`bar_style`, `partition_bar`, `format_number_thousands` ve **21 birim testi**.
|
||||
|
||||
### Etkisi
|
||||
Manuel bölümleme **fiilen işlevsiz** hâle gelmişti:
|
||||
- "+ Ekle" / "Düzenle" / "Sil" butonları yalnızca **kimsenin okumadığı**
|
||||
bayraklar set ediyordu → hiçbir diyalog açılmıyordu
|
||||
- `delete = true` **hiçbir yerden set edilmiyordu** →
|
||||
`CustomPartitionJob`'un FAZ A'sı (`parted rm`) erişilemez
|
||||
- `state.volume_groups` / `logical_volumes` **hiçbir yerden yazılmıyordu**
|
||||
→ `SetupLvmJob` hep boş vektör alıyordu, **LVM tamamen erişilemez**
|
||||
- `#[allow(dead_code)]` derleyicinin bu boşluğu bildirmesini bastırdı
|
||||
|
||||
### Geri alınışı
|
||||
`8c6dc08` ile düzeltildi. Yedek: tag `backup-before-unrevert-22de7d5`,
|
||||
branch `backup/pre-unrevert` (a5d4627 = geri alma öncesi HEAD).
|
||||
`8633422`'nin üç validasyon düzeltmesi geri almanın üzerine yeniden
|
||||
uygulandı; bir test (`manual_mode_complete_with_valid_partitions`)
|
||||
güncellendi, `selected_disk` kuralı için yeni test eklendi.
|
||||
|
||||
### DERS — TEKRARLANSIN
|
||||
1. **Bir commit mesajı ne yaptığını tam anlatmıyorsa, diff'i oku.**
|
||||
`-1804` satır "dead code temizliği" değildir.
|
||||
2. **`#[allow(dead_code)]` bir teşhis aracıdır, tedavi değil.** Bir şey
|
||||
kullanılmıyorsa ya silinmeli ya da bağlanmalı; susturmak, altta
|
||||
yatan boşluğu gizler. Bu projede tam olarak bu oldu.
|
||||
3. **Yedek dosyaları git'e commit etme.** `partition.rs_24082026` gibi
|
||||
dosyalar repoya `git rm` edildi, `.gitignore`'a desen eklendi.
|
||||
Yedek gerekiyorsa **git history**'dir, çalışma ağacı değil.
|
||||
4. Kullanılmıyor görünen bir adımı **silmeden önce** sihirbaza
|
||||
bağlı olup olmadığını yazılım olarak doğrula (grep yeterli değil —
|
||||
`on_enter`'ın ne zaman çağrıldığına bak).
|
||||
|
||||
---
|
||||
|
||||
## Proje Tasarım Kararı: SQUASHFS KURULUMU
|
||||
|
||||
**YALI kurulu sistemi ISO'nun squashfs'inden kopyalar** (`CopyFilesJob`
|
||||
rsync). Bunun temel sonucu:
|
||||
|
||||
> ISO'da hangi paketler, ekran yöneticisi ve masaüstü ortamı varsa,
|
||||
> kurulum onları **zaten içerir**.
|
||||
|
||||
Bu yüzden sihirbaz **paket seçimi, ekran yöneticisi seçimi veya lisans
|
||||
kabulü sunmaz** — sunduğu ayarları kopyalama zaten ezdiği için
|
||||
yanıltıcı olurdu.
|
||||
|
||||
### Netinstall gelecekte ayrı bir proje
|
||||
İki ayrı proje çıkacak:
|
||||
1. **Bu proje** — squashfs'i diske kopyalar (mevcut akış)
|
||||
2. **Netinstall** — paketleri ağdan kurar, squashfs akışı iptal eder
|
||||
|
||||
`src/steps/netinstall.rs` ve `src/jobs/netinstall.rs` **dormant** olarak
|
||||
repoda duruyor. Etkinleştirme koşulu `src/steps/mod.rs`'te yazılı:
|
||||
- `main.rs`'de `steps` vektörüne `Box::new(steps::netinstall::NetinstallStep)`
|
||||
(Bootloader ile Summary arasına)
|
||||
- `JobQueueConfig::selected_package_groups` doldurulmalı
|
||||
- `InstallExtraPackagesJob` kuyruğa alınmalı
|
||||
|
||||
`#[allow(dead_code)]` **yalnızca bu üç dormant modüle** ve **yalnızca
|
||||
belgelenmiş kasıtlı uyku için** konmuştur. Modül dışındaki taze ölü kod
|
||||
yine uyarı üretir.
|
||||
|
||||
### Ekran yöneticisi tespiti (çalışma zamanında)
|
||||
`ConfigureAutologinJob` DM'i **seçmez, tespit eder**:
|
||||
`detect_display_manager()` → `/usr/bin/{sddm,lightdm,gdm}` yoksa yapılandırma
|
||||
dizini. Bu iş `CopyFilesJob`'dan sonra çalıştığı için `mount` doludur.
|
||||
|
||||
> Önceden `state.display_manager`'a (GlobalState'in **sabit `"sddm"`
|
||||
> varsayılanı**) göre dallanılıyordu. ISO lightdm kullanıyorsa
|
||||
> `lightdm.conf`'a dokunulmuyor, `sddm.conf` yazılıyor ve otomatik giriş
|
||||
> sessizce çalışmıyordu.
|
||||
|
||||
### ⭐ Canlı ISO autologin'i + ters görünen checkbox (29 Eyl 2026)
|
||||
|
||||
ISO **canlı kullanıcı için çalışan bir autologin config'i** taşır
|
||||
(kullanıcı doğruladı: canlı açılış otomatik giriyor). Bu iki arızayı
|
||||
birlikte üretiyordu:
|
||||
|
||||
| Kutu | Olan | Neden |
|
||||
|---|---|---|
|
||||
| tikli | login ekranı | bizim config `/usr/lib/...` altına yazılıyordu → `/etc` altındaki canlı config'e **öncelikle yeniliyordu**; bir de `Session=plasmax11.desktop` **sabit** yazılıyordu, hedefte yoksa SDDM oturumu bulamıyor |
|
||||
| boş | "oturum açıyor" | kutu boşken hiçbir şeye dokunulmuyordu → canlı config kopyalanıyor, kurulu sistem **canlı kullanıcıyla** (parolasız!) açılıyordu |
|
||||
|
||||
Kurallar:
|
||||
- sddm config'i **`/etc/sddm.conf.d/10-yali-autologin.conf`** kanonik
|
||||
dosyaya yazılır (en yüksek öncelik, deterministik kazanır).
|
||||
ISO dosyaları **silinmez**, öncelikle ezilir.
|
||||
- Oturum adı hedeften tespit edilir: `resolve_session()` →
|
||||
`/usr/share/{wayland-sessions,xsessions}/*.desktop` taranır,
|
||||
`pick_session_file()` seçer (saf + testli). Sabit eşleme yalnızca
|
||||
**son çaredir**.
|
||||
- Kutu boşken de kanonik dosya yazılır ama **`User=` boş** bırakılır —
|
||||
SDDM bunu "kapalı" sayar, canlı config ezilir. lightdm'de
|
||||
`autologin-user=`/`autologin-session=` satırları ayıklanır.
|
||||
- DM bulunamazsa **yalnızca kutu tikliyken hata**; kapalıyken sessiz
|
||||
geçilir (DM'siz sunucu kurulumunu öldürmemek için).
|
||||
|
||||
`ConfigureAutologinJob` ayrıca:
|
||||
- sddm config'i daima `/etc/sddm.conf.d/10-yali-autologin.conf` dosyasına
|
||||
yazılır (dizin yoksa oluşturulur); eski `/usr/lib/...` + fallback
|
||||
mantığı **kaldırıldı** (öncelik kaybı + yanlış dizin riski vardı)
|
||||
- `session_desktop_file()` eşlemesini **tüm DM'lerde** kullanır
|
||||
(lightdm `autologin-session=plasma` yazıyordu; `.desktop` dosyası
|
||||
adı bekler)
|
||||
- DM bulunamazsa **hata** verir, kullanıcıya "elle yapılandırın"
|
||||
diyebilmeli
|
||||
|
||||
---
|
||||
|
||||
## Mimari
|
||||
|
||||
```
|
||||
egui main thread ──thread::spawn──► tokio::Runtime::new()
|
||||
└─ block_on(JobQueue::run_all)
|
||||
└─ Job::run(&UiSender) ──mpsc──► egui
|
||||
egui frame loop ◄──try_recv()──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- `src/steps/*` — saf UI, `InstallerStep` trait'i
|
||||
- `src/jobs/*` — `async`, `Job: Send + Sync`, `&self` (immutable)
|
||||
- `GlobalState` job thread'leriyle **paylaşılmıyor**; `clone()` edilip
|
||||
`JobQueueConfig` POD'una konuyor. `Arc<Mutex<…>>` yok.
|
||||
|
||||
### `StepRole` — rol artık konumsal değil
|
||||
`InstallerStep::role()` → `Normal | Execution | Finish`.
|
||||
Daha önce `main.rs` rolü `cur == n_steps - 2` ile **tahmin** ediyordu;
|
||||
yeni adım eklemek bu tespiti sessizce kaydırıyordu. `main.rs`'de 4 test
|
||||
var, biri "Execution ÖNÜNE adım eklenince rol algısı bozulmuyor".
|
||||
|
||||
### Hata tipi
|
||||
Tüm projede `Result<(), String>`. `thiserror` yok, hata zinciri yok.
|
||||
Job log satırları Türkçe hardcoded — İngilizce kullanıcı Türkçe hata görüyor.
|
||||
|
||||
---
|
||||
|
||||
## Çoklu Disk Manuel Bölümleme
|
||||
|
||||
### `CustomPartition` alanları (`src/installer.rs`)
|
||||
- `disk`: bölümün ait olduğu disk ("/dev/sda") — gruplama ve **her
|
||||
kapasite/slot hesabı** bununla yapılır
|
||||
- `orig_device`: lsblk'tan gelen fiziksel cihaz adı; **boş = kullanıcı
|
||||
tanımı yeni bölüm**. Silme/işaret mantığı buna dayanır.
|
||||
- `delete: bool`: eski Yali `OperationDestroyDevice` karşılığı
|
||||
|
||||
### ⭐ Bölüm numaralandırması: `plan_disk_layout` (TEK KAYNAK)
|
||||
|
||||
**En kritik bulgu bu oturumdandır — veri kaybı düzeltildi.**
|
||||
|
||||
Bölüm numarası iki ayrı yerde bağımsız hesaplanıyordu:
|
||||
- UI: `assign_partition_numbers` → "en küçük boş slot"
|
||||
- Job: `mkpart` + `part.device` okuması
|
||||
|
||||
**parted davranışı:** `parted rm N` sonrası kernel, bölümleri **başlangıç
|
||||
offset'ine göre** sıralayıp 1'den numaralandırır. `mkpart` ile oluşturulan
|
||||
bölüm de aynı sıraya girer.
|
||||
|
||||
**Uyumsuzluğun gerçekleştiği senaryo:**
|
||||
```
|
||||
sda1 0–100 korunuyor
|
||||
sda2 100–200 SİLİNECEK → 100 MB boşluk
|
||||
sda3 200–30200 korunuyor
|
||||
disk sonu: 10000 MB boş
|
||||
yeni: 5000 MB
|
||||
|
||||
UI : sda2 silinince slot 2 boşalır → yeni = sda2
|
||||
Disk: 100–200 boşluğu 5000 MB'ye SIĞMAZ → first-fit diskin sonuna yazar
|
||||
kernel sırası: sda1, sda2(eskisi 3), sda3(YENİ)
|
||||
Sonuç: UI "yeni = sda2" der, diskte yeni = sda3. FAZ C mkfs'i yanlış
|
||||
aygıta uygular → kullanıcının KORUMAK İSTEDİĞİ bölüm bozulur.
|
||||
```
|
||||
|
||||
**Çözüm:** `plan_disk_layout(disk, total_mb, physical, destroyed, creates)`
|
||||
— saf, test edilebilir fonksiyon. Diskin **tamamını** (fiziksel + yeni)
|
||||
görüp numaralandırır ve şunları döndürür:
|
||||
- `Vec<PlannedCreate>`: her yeni bölümün konumu + **gerçek** slot numarası
|
||||
- `Vec<(u32, String)>`: silmeden sonra **KAYAN** fiziksel bölümlerin
|
||||
eski numarası → yeni aygıt yolu
|
||||
|
||||
FAZ B bu planı kullanır, `device_map` doldurur, FAZ C **gerçek aygıtı**
|
||||
kullanır (kayma varsa loglar). `assign_partition_numbers` artık yalnızca
|
||||
**UI önizlemesi** içindir — doğruluk kaynağı değil.
|
||||
|
||||
> `disk_free_regions_mb` (eski, ayrı kopya) **silindi**. `disk_total_mb`
|
||||
> ve `read_physical_extents` eklendi; artık diskin gerçek boyutu sysfs'ten
|
||||
> okunuyor (önce son bölümün bitişinden tahmin yürütülüyordu).
|
||||
|
||||
### `assign_partition_numbers`
|
||||
Disk bazlı `HashMap<disk, BTreeSet<num>>` ile **bağımsız** çalışır;
|
||||
sda1 ile sdb1 çakışmaz.
|
||||
|
||||
> Eski test `renumber_keeps_disks_independent` **TUUTOLOJİK'ti**: hep
|
||||
> fiziksel bölüm kullanıyordu, fonksiyon no-op çalışıyordu; global
|
||||
> (disk-suz) bir uygulama da testi geçerdi. `multidisk_tests` içinde
|
||||
> gerçek çapraz-disk yolunu (yeni tanım) sınayan testler var.
|
||||
|
||||
### `CustomPartitionJob` fazları (`jobs/partition.rs`)
|
||||
1. **FAZ A**: `delete=true` → `parted rm N`, büyükten küçüğe
|
||||
2. **FAZ B**: `plan_disk_layout` ile yerleşim + numaralandırma, sonra `mkpart`
|
||||
3. **FAZ C**: `device_map` üzerinden **gerçek** aygıta mkfs (LUKS akışı korundu)
|
||||
- Sabit boyutlular ÖNCE, sembolikler SONRA (`sort_by_key(size==0)`)
|
||||
- İşlemi olmayan disklere hiç girilmez (`⏭️ ... korunuyor`)
|
||||
|
||||
### UI (`src/steps/partition.rs`)
|
||||
- **Tablo TEK diski gösterir** (kullanıcı isteği, 27 Eyl): üstteki
|
||||
seçicide hangi disk seçiliyse bölüm tablosu onun. Tüm diskleri tek
|
||||
tabloda göstermek kafa karışıklığı yaratıyordu — özellikle "Sil"
|
||||
hangi diski etkilediği belirsizleşiyordu.
|
||||
- **Disk ŞERİDİ YOK** (27 Eyl, kullanıcı isteği). Tablo altındaki
|
||||
`/dev/sda · 3/20 GB` gibi tıklanabilir disk düğmeleri kaldırıldı:
|
||||
üstteki **"Aktif Disk" ComboBox'ı** zaten tek ve açık kaynak, iki
|
||||
kaynak olması "hangi disk geçerli" sorusunu belirsizleştiriyordu.
|
||||
Başka bir diske geçmek için ComboBox kullanılır, sonra "Yeni Bölüm".
|
||||
"sda'da 3 bölüm var" bilgisi tablo başlığında ve satırlarda durur.
|
||||
- ⚠️ Şerit sadece çizim değildi: disk değişince
|
||||
`on_disk_changed()` ile `selected_idx` / `selected_free`
|
||||
sıfırlıyordu. Bu sorumluluk **ComboBox'a taşındı**; taşınmazsa
|
||||
bayat satır indeksi "Sil" ile **yanlış bölümü** siler.
|
||||
- **"Aktif Disk" ComboBox'ı TEMA sarmalayıcısını kullanır**
|
||||
(`theme::show_combo_box` + `theme::selectable_value`). Ham
|
||||
`egui::ComboBox` + `ui.selectable_value` egui'nin varsayılan stilini
|
||||
kullanıyor ve koyu temada okunmuyordu. Mountpoint ve önyükleyici
|
||||
ComboBox'ları zaten temalı sürümde — **disk ComboBox'ı geride kalmıştı**.
|
||||
- **Diyalogda HEDEF DİSK SEÇİCİSİ YOK** (27 Eyl, kullanıcı isteği) —
|
||||
ne eklemede ne düzenlemede. Bölümleme arayüzü tek disk üzerinde
|
||||
çalışıyor:
|
||||
- ekleme → `state.selected_disk`
|
||||
- düzenleme→ bölümün kendi `disk`/`device` alanları (korunur)
|
||||
|
||||
Başka bir diske bölüm eklemenin yolu: **"Aktif Disk"
|
||||
ComboBox'ından o diske geç**, sonra "Yeni Bölüm".
|
||||
|
||||
Bu karar `ManualState::add_disk` alanını da tamamen gereksiz kıldı ve
|
||||
alan **silindi**. Alan varken şu akış bölümü yanlış diske taşıyordu:
|
||||
"Yeni Bölüm" (`add_disk = sda`) → iptal → sdb'deki bölümü `Düzenle`
|
||||
→ kaydet. Artık "hangi disk" sorusunun **tek cevabı** var ve bayat
|
||||
değer sınıfı hata kalmıyor. `edit_disk_tests` bunu sabitler.
|
||||
- Tablo `TableRow::Part | TableRow::FreeSpace` enum'u ile kurulur.
|
||||
Satır kurma mantığı **`build_table_rows(parts, active_disk, disk_mb)`**
|
||||
saf fonksiyonunda (gömülüydü, iki kez yanlış hesaplandı):
|
||||
- **Boş alan satırı disk başına TEK'tir.** Silinmek işaretliler ilk
|
||||
işaretli tanımın konumunda birleşir (GParted tarzı). Satır ortada
|
||||
basıldıysa sona ikinci bir tane **EKLENMEZ** (`free_row_shown`
|
||||
bayrağı) — eklenirse aynı alan iki kez görünür ve toplam diskten
|
||||
büyük izlenimi doğar.
|
||||
- Disk sonundaki boş alan, diskte tanım **olup olmadığına bakılmaksızın**
|
||||
basılır. REGRESYON: koşul `all(|p| p.disk != active_disk)` idi,
|
||||
yani yalnızca tamamen boş disklerde görünüyordu. `sda1 (500 MB) +
|
||||
15 GB boş` → boş alan hiç çizilmiyordu.
|
||||
- `FreeSpace.first_idx`: silinmiş tanımı temsil ediyorsa `Some`,
|
||||
temsil ettiği tanım yoksa `None`. **`None` seçilemez demek
|
||||
DEĞİLDİR** — tıklanınca `global_idx == None` dalı `selected_free`
|
||||
tutar ve "Yeni Oluştur" butonu çıkar.
|
||||
- Seçili disk hiç tanım içermiyorsa boş-alan satırı basılır
|
||||
(`first_idx: None` → seçim indeksi karşılığı yok; tıklanınca
|
||||
`selected_free` tutulur ve "Yeni Oluştur" çıkar). `selected_idx = 0`
|
||||
ile başka diskin ilk bölümü seçilmesi bu yolla engellenir.
|
||||
- Silinmek işaretli bölümler ayrı satır çizmez; İLK silinenin konumunda
|
||||
TEK "Boş Alan" satırında birleşir (GParted tarzı).
|
||||
- Varsayılan öneriler (EFI/root/swap) **aktif diskin durumuna** göre verilir.
|
||||
- "Kalan alan" SEMBOLİKTİR (size_mb=0): kaydetme anında sayıya çevrilmez.
|
||||
- **LVM bölümü de seçili diske filtrelenir**: VG listesi yalnızca PV'leri
|
||||
o diskte olan VG'leri, LV listesi yalnızca VG'si o diskte olan LV'leri
|
||||
gösterir. Yeni VG oluştururken PV'ler **yalnızca seçili diskin**
|
||||
PV'lerinden toplanır — önceden tüm disklerin PV'leri birleştiriliyordu
|
||||
ve boyut toplamı yanlış çıkıyordu (AGENTS.md'deki açık maddeydi).
|
||||
|
||||
### ⭐ Mountpoint boşluğu mount EDİLMEMİŞ bölüm demek değil
|
||||
|
||||
`read_existing_partitions` mountpoint'i **`lsblk` MOUNTPOINT sütunundan**
|
||||
okur (satır ~67: `tokens.next().unwrap_or("")`). Canlı ISO'da mount
|
||||
edilmemiş her bölümde bu sütun **boştur**.
|
||||
|
||||
**Çift önyüklemeli makinede Windows'un boot bölümü neredeyse her zaman
|
||||
bu durumdadır** ve kullanıcı ona dokunmamıştır. Buna rağmen
|
||||
`validate_manual_on` tüm tanımları kapsayan bir "boş mountpoint"
|
||||
kontrolü içeriyordu → düzen reddediliyordu, hata hangi bölümden
|
||||
geldiğini söylemeden.
|
||||
|
||||
Kural artık: **boş mountpoint zorunluluğu yalnızca `orig_device` BOŞ
|
||||
olan (kullanıcı tanımı) bölümlerde geçerli.** Fiziksel taranmış
|
||||
bölümde boş mountpoint "henüz atanmadı" demektir ve iş zincirinde
|
||||
zaten güvenli:
|
||||
|
||||
| Aşama | Davranış |
|
||||
|---|---|
|
||||
| mount | atlanır (`jobs/partition.rs:1111`) |
|
||||
| fstab | atlanır (`:1171`) |
|
||||
| mkfs | atlanır (`skip_format: true`) |
|
||||
|
||||
Aynı sebeple **mükerrer bağlama noktası kontrolü de boş değerleri
|
||||
dışlar** — aksi hâlde "N atanmamış bölüm" kendi kendine mükerrer
|
||||
sayılır ve asıl sorun maskelenir.
|
||||
|
||||
### `suggest_mountpoint` — öneri TÜM düzeni görmeli
|
||||
|
||||
`/`, `/boot/efi` ve `swap` bir kurulumda **TEKİLDİR**. Öneri bu
|
||||
yüzden aktif diske değil **tüm düzene** bakar.
|
||||
|
||||
Aktif diske bakıyordu: kullanıcı boş bir ikinci diske geçip "Yeni
|
||||
Bölüm" dediğinde o diskte tanım olmadığı için `/boot/efi` öneriliyordu
|
||||
— sdb1'de zaten var olan mountpoint'i ikinci kez. Kullanıcı
|
||||
kaydetmeye çalışınca "mükerrer" alıyor, alanı temizleyince "boş
|
||||
olamaz" alıyordu. İkisi de hiçbir şey söylemiyordu.
|
||||
|
||||
Sıra: `/boot/efi` (UEFI) → `/` → `swap` → `/home` → boş. Son adım
|
||||
ikincil (veri) diskler içindir: boş diske bölüm eklemek için alanı
|
||||
elle doldurmak zorunda kalmamak. Silinmek işaretli tanımlar
|
||||
sayılmaz.
|
||||
|
||||
### ⭐ "Kalan Alan" (sembolik) tanımlar — `size_mb == 0`
|
||||
|
||||
`CustomPartition::size_mb == 0` **"Kalan Alan"** demektir: kurulumda
|
||||
diskin o an kalan boşluğunun **tamamını** alır. Bu yüzden sabit
|
||||
boyutlu tanımlarla birlikte hesaplanırken **sabit toplama katılmaz**.
|
||||
|
||||
Kullanıcı raporu: *"eklenen bölümün 19.8 gb demesine rağmen bunu
|
||||
boş alandan düşmedi"* — 20 GB'lik disk 39 GB kullanıyor gibi görünüyordu,
|
||||
çünkü hem bölüm satırı hem boş alan satırı kalanın tamamını gösteriyordu.
|
||||
|
||||
| Yer | Kural |
|
||||
|---|---|
|
||||
| `has_symbolic_remaining(parts, disk)` | **TEK KAYNAK.** `!delete && size_mb == 0 && orig_device.is_empty()` |
|
||||
| tablo (`build_table_rows`) | sembolik varsa boş alan satırı **çizilmez** — o alan o tanıma ait. Koşul **`show_free`** adıyla TEK yerde tanımlı ve **üç basış yerinin de** ortak koşulu (aşağıdaki REGRESYON 3'e bak) |
|
||||
| çubuk (`manual_dual_bars`) | sembolik varsa serbest segment **eklenmez** |
|
||||
| `manual_capacity_error` | sembolik varsa `fixed_mb >= cap_mb` reddedilir |
|
||||
|
||||
Son satır, AGENTS.md'de açık madde olarak duran hatayı kapatır:
|
||||
`fixed == cap` geçiyordu, kurulumda "yeterli boş alan yok" ile düşülüyordu.
|
||||
Ayrıca **aynı diskte iki "Kalan Alan" tanımı** reddedilir — ikisi de
|
||||
kalanın tamamını alır, ikincisine hiç yer kalmaz.
|
||||
|
||||
`orig_device` dolu (taranmış) 0 boyutlu bölüm sembolik **DEĞİLDİR**:
|
||||
sıfır boyut "kalanın tamamı" anlamına gelmez.
|
||||
|
||||
#### ⭐ Koşul YER YER yazılırsa bu sınıf hata tekrarlar
|
||||
|
||||
Bu tabloda bir hata **üç kez** yaşandı ve üçü de aynı sebepten:
|
||||
`has_symbolic` kontrolü satırın **yalnızca bir** basış yerine
|
||||
eklenmişti.
|
||||
|
||||
1. `else if` dalı vardı, diskte tanım yokken çalışıyordu →
|
||||
tanım varsa satır hiç basılmıyordu (`sda1` + 15 GB boş)
|
||||
2. `else if` düzeltildi, **döngü içindeki** basış unutuldu →
|
||||
`19.8 GB` bölüm + `19.8 GB` boş alan
|
||||
3. döngü içi de düzeltildi ama **koşul üç yere ayrı ayrı yazıldı**
|
||||
|
||||
**Ders:** aynı koşulu birden çok yerde yazma. Tek bir
|
||||
`show_free = free_total_mb > 0 && !has_symbolic` değişkeni hesaplanıp
|
||||
**üç basış yerinin de** koşulu olarak kullanılıyor. Kural tek
|
||||
yerde; onu atlamak ya da çoğaltmak imkânsız.
|
||||
|
||||
> Bu, "sabit bir sayıyı 3 yere yazma" değil — "bir *koşulu* 3
|
||||
> yere yazma". Sabit sayıyı çoğaltmak hataya açıktır ama en azından
|
||||
> yanlışsa sayı yanlış görünür; koşulu çoğaltmak bazı basış
|
||||
> yerlerinde **sessizce yanlış** davranış üretir.
|
||||
|
||||
### ⭐ Bağlanmayan dosya sistemleri — mountpoint isteğe bağlı
|
||||
|
||||
`mountpoint_required(editing, fstype)` üç dosya sisteminde `false`
|
||||
döndürür:
|
||||
|
||||
| FsType | Neden |
|
||||
|---|---|
|
||||
| `Lvm` | PV'nin kendisi bağlanmaz, üzerinde LV'ler yaşar |
|
||||
| **`Ntfs`** | **Linux'ta bağlanması GEREKEN bir dosya sistemi değil** |
|
||||
| düzenlenen fiziksel bölüm | "yalnızca GRUB için" işaretlemesi |
|
||||
|
||||
NTFS'te zorunluluk kullanıcıya anlamsız geliyordu: *"format için ntfs
|
||||
seçtiğim halde bağlama noktası seçmemi bekliyor bu mantıksız değil
|
||||
mi"*. Tipik kullanım Windows ile veri alışverişi — bölüm biçimlendirilir
|
||||
ama sisteme bağlanmaz, kullanıcı elle bağlar veya dosyayı doğrudan açar.
|
||||
|
||||
Boş bırakıldığında kurulumda ne olduğu **mevcut ve doğru**: mount
|
||||
atlanır (`jobs/partition.rs:1111`), fstab'a girmez (`:1171`), ama
|
||||
`mkfs.ntfs -f` **çalışır** (FAZ C, `FsType::Ntfs`).
|
||||
|
||||
Diyalogda NTFS seçilince:
|
||||
1. geçersiz öneri kalıntısı (`/`, `/boot/efi`, `swap`…) **temizlenir**
|
||||
2. `mp_mountpoint_optional` ipucu gösterilir
|
||||
|
||||
### ⭐ Disk listesi kararlı sıralanır, boot medyası hariç
|
||||
|
||||
`list_block_devices` ne `lsblk` çıktısını ne `/sys/block` çıktısını
|
||||
sıralıyordu. `/sys/block` fallback yolunda liste **önyüklemeden
|
||||
önyüklemeye değişebiliyor** — "ilk disk" kavramı anlamsızlaşıyor,
|
||||
varsayılan seçimler tutarsızlaşıyor.
|
||||
|
||||
`sort_disks()`: **çıkarılabilir aygıtlar sona**, dahililer ada göre
|
||||
(`disk_sort_key` — bölüm numarası SAYISAL ayrıştırılır, yoksa
|
||||
`sda10 < sda2` olurdu).
|
||||
|
||||
**Çıkarılabilir aygıtlar listeden de ÖNYÜKLEYİCİ hedefinde de
|
||||
elenir.** `detect_drive_type` "USB" döndürüyor ama o değer hiç
|
||||
okunmuyordu. Canlı ISO'nun USB'i `available_disks`'teydi ve önyükleyici
|
||||
hedefi olarak seçilirse BIOS modunda `grub-install --target=i386-pc`
|
||||
MBR'yi **boot medyasına** yazar → kurulum biter, sistem açılmaz.
|
||||
|
||||
> `is_removable()` kendi ayrıştırıcısını kullanır, `device_to_disk`'i
|
||||
> DEĞİL: o işlev **bölüm yolu** içindir ve çıplak NVMe disk adında
|
||||
> hatalıdır (`/dev/nvme0n1` → `nvme0n`). `base_disk_name` bunu
|
||||
> düzeltir. Bu ayrım notla sabitlendi, ileride "basitleştirilirse"
|
||||
> NVMe'ler çıkarılabilir sanılır.
|
||||
|
||||
Ayrıca UEFI modunda çift önyükleme ipucu
|
||||
(`bootloader_device_hint_dualboot`) gösterilir: hangi aygıtı seçmek
|
||||
gerektiği ekranda hiç yazmıyordu.
|
||||
|
||||
### ⭐ Önyükleyici varsayılanı: KÖRK diski değil, **mevcut OS diski**
|
||||
|
||||
`steps/bootloader.rs::default_boot_device()` saf ve test edilebilir:
|
||||
|
||||
```
|
||||
1. çıkarılabilir aygıt (ISO'nun USB'i) → ASLA hedef değil
|
||||
2. kök diski dışında bölümü olan ilk dahili disk → mevcut OS diski
|
||||
3. kök diski
|
||||
4. selected_disk (son çare)
|
||||
```
|
||||
|
||||
**Neden kök diski değil.** BIOS modunda `grub-install` seçilen
|
||||
diskin **MBR'sine** yazar ve MBR'yi okuyan aygıt firmware'in açılış
|
||||
sırasındaki ilk diski olmak ZORUNDA:
|
||||
|
||||
```
|
||||
sda: Windows sdb: PisiLinux (kök burada)
|
||||
GRUB → sdb firmware sdb'yi açmaz, PisiLinux HİÇ GÖRÜNMEZ
|
||||
GRUB → sda firmware GRUB'a düşer, GRUB ikisini de listeler
|
||||
```
|
||||
|
||||
"İşletim sistemi kurulu"nun en ucuz ve en güvenilir göstergesi:
|
||||
**kök diski dışında hâlihazırda bölüm tablosu olan disk**. Tespit
|
||||
`disk_partition_count()` ile `sysfs`'ten yapılır — **alt süreç yok**
|
||||
(`lsblk` çalıştırmak `on_enter`'da bile gereksiz).
|
||||
|
||||
Tek diskli kurulumda 3. adım çalışır, yani davranış kök diskiyle
|
||||
aynıdır — geriye dönük uyum bozulmaz.
|
||||
|
||||
Kullanıcı elle değiştirdiyse ipucu (`bootloader_device_why_existing`)
|
||||
gösterilmez; ipucu yalnızca seçim bu kurala uyuyorsa çıkar, yani
|
||||
sessiz bir sürpriz olmaz.
|
||||
|
||||
> ⚠️ Bu, daha önceki iki varsayılanın da yerine geçti:
|
||||
> `selected_disk` (manuel modda "aktif düzenleme diski" olduğu için
|
||||
> yanlış diske gidiyordu) ve salt kök diski (dual-boot'ta yanlış).
|
||||
> Kural artık ikisinden de iyi; `boot_device_reason` aynı fonksiyonu
|
||||
> çağırıp UI metnini oradan üretir — **ikinci bir uygulama yok**.
|
||||
|
||||
### ⚠️ `show()` HER KARE çalışır — girdi tepkisi KENAR TETİKLİ olmalı
|
||||
|
||||
egui `show()` ~60 Hz çağrılır. Bu yüzden kullanıcı girdisine tepki
|
||||
veren **her durum mutasyonu kenar tetikli (change-detected) olmak
|
||||
zorunda**; seviye tetikli ("şu an şu koşul doğruysa yaz") yazılan
|
||||
kod kullanıcının yazdığını **sürekli siler**.
|
||||
|
||||
REGRESYON (kullanıcı: *"düzenle kısmında bu seferde bağlama noktasını
|
||||
seçemiyorum"*):
|
||||
|
||||
```rust
|
||||
// ❌ HER KARE çalışır
|
||||
if mp_optional {
|
||||
if matches!(add_mountpoint.as_str(), "/" | "/home" | …) {
|
||||
add_mountpoint.clear(); // kullanıcı `/home` seçti → silindi
|
||||
} // tekrar seç → tekrar silindi
|
||||
}
|
||||
```
|
||||
|
||||
`mp_optional` fiziksel bölümlerde de doğru olduğu için (orijinal
|
||||
mountpoint'i olan bölümü düzenlerken) liste **tamamen kilitlenmişti**.
|
||||
|
||||
İki koşul birlikte gerekiyor:
|
||||
|
||||
| Koşul | Neden |
|
||||
|---|---|
|
||||
| **yalnızca Ekleme modunda** | Eklemede mountpoint `suggest_mountpoint`'in ÜRETTİĞİ öneridir, eski kalıntı olabilir. Düzenlemede bölümün **GERÇEK** değeridir — silmek kullanıcı verisini kaybettirir. |
|
||||
| **yalnızca `add_fstype` DEĞİŞTİĞİ karede** (`add_fstype_prev`) | Geçiş değilse kullanıcının yazdığı her karede silinir. Aynı tuzak (b) olmadan ekleme modunda da yaşanırdı. |
|
||||
|
||||
> Bu sınız hata **sessizdir**: derleyici uyarı vermez, test de
|
||||
> yakalamaz (UI kodu). Bu yüzden `ManualState`'e
|
||||
> `add_fstype_prev: Option<usize>` alanı eklendi ve hem "Yeni Bölüm"
|
||||
> hem "Düzenle" butonlarında diyalog açılışında kuruldu — ilk karede
|
||||
> yanlışlıkla silinmesin diye.
|
||||
|
||||
### Testler ortamdan bağımsız olmalı
|
||||
|
||||
`is_removable()` gerçek `sysfs`'i okur. Bu geliştirme makinesinde
|
||||
`/sys/block/sda/removable == 1` olduğu için `/dev/sda` kullanan testler
|
||||
çıkarılabilir sanılıp **kırıldı** — yani o testler **şansa** geçiyormuş.
|
||||
Şansa geçen test, test olmaktan kötüdür.
|
||||
|
||||
Çözüm: saf katman probu dışarıdan alır —
|
||||
`sort_disks_with(disks, is_removable)` ve
|
||||
`default_boot_device(root, disks, is_removable, has_partitions)`.
|
||||
Üretim sarmalayıcıları gerçek probu verir, testler sabit olanı.
|
||||
|
||||
> `/dev/sda` gibi **gerçek aygıt adları** test verisi olarak
|
||||
> kullanılıyorsa, o adın `sysfs`'te çevreye bağlı bir özelliği
|
||||
> (removable gibi) sorgulanmadığından emin ol. Sorgulanıyorsa probu
|
||||
> dışarıdan al.
|
||||
|
||||
### ⭐ Yerelleşmiş CLI çıktısı ayrıştırılmaz (29 Eyl 2026, P0 veri kaybı)
|
||||
|
||||
`disk_label_is_gpt` parted çıktısındaki İngilizce `Partition Table`
|
||||
satırını arıyordu. Ama parted yerelleşmiştir — canlı ISO'da satır
|
||||
`Disk bölümü Tablosu: msdos` gelir. Ayrıştırıcı hiçbir diski
|
||||
tanıyamadı → her disk "boş" sanıldı → `mktable` dolu diski **komple
|
||||
sildi**. Boş disklerde zararsız olduğu için iki kurulum geçti, üçüncüde
|
||||
(dolu diske ekleme) patladı.
|
||||
|
||||
Kurallar:
|
||||
- İnsan-dili CLI çıktısı **daima `LC_ALL=C` ile** okunur
|
||||
(`env LC_ALL=C parted …`). Makine-dili alternatif varsa (blkid
|
||||
`PTTYPE` gibi) o tercih edilir.
|
||||
- Türkçe başlık çekimlidir (`bölümü`, `bölümü`nün…): tam kalıp
|
||||
eşleştirme ıskalar. Anahtar kelimeyle (`tablosu`) eşleştir + test
|
||||
verisi olarak GERÇEK yerelleşmiş çıktıyı kullan.
|
||||
- **Yıkıcı komutun kilidi olur:** `mktable` öncesi sysfs'ten fiziksel
|
||||
bölüm sayılır; bölüm varken etiket okunamazsa **hata verilir**,
|
||||
tabloya dokunulmaz. Ayrıştırıcı + locale + kilit üçü birlikte
|
||||
kapatır; hiçbiri tek başına yeterli değildir.
|
||||
|
||||
### ⭐ "Biçimlendir" kutusu — olumlu çerçeve
|
||||
|
||||
Kullanıcı isteği: *"düzenle penceresindeki biçimlendirmeyi atla
|
||||
seçeneğini biçimlendir şekline dönüştürsek yani seçili ise
|
||||
biçimlendirsin şimdiki davranışın tam tersi"*.
|
||||
|
||||
| Katman | Alan | Değer |
|
||||
|---|---|---|
|
||||
| Model (`CustomPartition`) | `skip_format` | taranmış bölüm = `true` (**korunur**) |
|
||||
| Diyalog (`ManualState`) | `add_format` | **`skip_format`'ın TERSİ** |
|
||||
|
||||
İki yönlü dönüşüm iki adlandırılmış yardımcıda:
|
||||
`format_checkbox_from(skip)` / `skip_format_from(format)`. Elle `!`
|
||||
yazılmaz; `format_checkbox_and_skip_format_are_exact_inverses` testi
|
||||
çift bağı sabitler.
|
||||
|
||||
**Neden ters çevirdik.** "Biçimlendirmeyi Atla" taranmış bölümlerde
|
||||
**seçili** geliyordu, yani güvenli durum "varsayılan seçim"di. Risk
|
||||
seçeneğin kendisinde değil, seçenekte risk olduğunu sezmeden okuyan
|
||||
kullanıcıda: "Atla" işaretliyse veri silinecek diye düşünürken
|
||||
tam tersi oluyordu. Olumlu çerçevede **seçim kullanıcıya aittir**:
|
||||
|
||||
```
|
||||
Taranmış bölüm (sda1) → "Biçimlendir" ☐ SEÇİLİ DEĞİL → veri korunur
|
||||
Kullanıcı tanımı (sda2) → "Biçimlendir" ☑ SEÇİLİ → biçimlendirilir
|
||||
```
|
||||
|
||||
> ⚠️ Ters çevrim sessizce kaybolursa taranmış bölümler
|
||||
> biçimlendirilir — **veri kaybı**. Bu yüzden dönüşüm `!` olarak
|
||||
> iki yere dağıtılmadı, adlandırılmış çift fonksiyona bağlandı ve
|
||||
> `format_checkbox_and_skip_format_are_exact_inverses` testi
|
||||
> `skip_format` için **her iki** değeri de dener.
|
||||
|
||||
`mp_skip_format` çeviri anahtarı kullanılmaz hale geldiği için iki
|
||||
locale'den de **silindi** (ölü anahtar bırakılmadı).
|
||||
|
||||
### ⭐ `lsblk` AKTİF swap için `[SWAP]` yazar
|
||||
|
||||
**Bu, canlı ISO'ya SSH ile bağlanıp GERÇEK çıktı okunarak bulundu.**
|
||||
Tahminle değil:
|
||||
|
||||
```
|
||||
$ lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT -b --noheadings -l /dev/sdb
|
||||
sdb1 524288000 vfat /boot/efi
|
||||
sdb2 1572864000 swap [SWAP] ← köşeli parantezli!
|
||||
sdb3 25168969728 ext4 /
|
||||
```
|
||||
|
||||
`read_existing_partitions` mountpoint'i doğrudan alıyordu, yani
|
||||
`CustomPartition::mountpoint = "[SWAP]"` oluyordu. İki yere sızıyordu:
|
||||
|
||||
1. **UI** — tablonun bağlama noktası sütununda `[SWAP]` görünüyordu
|
||||
2. **mükerrer kontrolü** — iki swap'ı olan makinede (Windows diski +
|
||||
Linux diski = çift önyüklemeli kurulumun tipik hali) ikisi de aynı
|
||||
`"[SWAP]"` değerini taşıyor ve kullanıcı **var olmayan** bir
|
||||
"mükerrer bağlama noktası" hatası görüyordu
|
||||
|
||||
Bu makinede tek swap olduğu için sorun görünmüyordu — **kolayca kaçar.**
|
||||
|
||||
| Düzeltme | Yer |
|
||||
|---|---|
|
||||
| `normalize_lsblk_mountpoint(raw, fstype)` | `[SWAP]` → `"swap"`. Yalnızca `FsType::Swap` için; diğerleri **dokunulmaz** (sessizce yutmak yok) |
|
||||
| mükerrer kontrolü | yalnızca **gerçekten bağlanan** dosya sistemleri karşılaştırılır: `Swap`/`Lvm`/`LvmLv` hariç. `mountpoint_required` ile aynı gerekçe |
|
||||
|
||||
> `fstab` işi zaten doğruydu: satırı `part.mountpoint`'e değil
|
||||
> `part.fstype`'e göre yazıyor (`FsType::Swap => ("none", "sw", …)`).
|
||||
> Yani mountpoint `"swap"` olması fstab'ı bozmaz — hatta bozmadığı için
|
||||
> hata fstab'da değil, **mükerrer kontrolünde** görünmüyordu.
|
||||
|
||||
### `selected_disk` hâlâ 3 yerde kullanılıyor
|
||||
1. Tablo hangi diski gösteriyor + `manual_dual_bars`
|
||||
2. Diyaloğun varsayılan hedefi
|
||||
3. `build_custom_job_queue` girdisi → `InstallGrubJob.disk`
|
||||
|
||||
Bu yüzden dış ComboBox'un etiketi **"Aktif disk"** + açıklayıcı ipucu
|
||||
("Kurulum Yapılacak Diski Seçin" yanlış anlam çağrışımı yapıyordu).
|
||||
`bootloader_device` varsayılanı `selected_disk` değil, **kökün olduğu disk**.
|
||||
|
||||
---
|
||||
|
||||
## Güvenlik Düzeltmeleri (P0)
|
||||
|
||||
### Parola asla argv'ye geçmez
|
||||
`sh -c "echo 'user:pass' | chpasswd"` deseni **KALDIRILDI**
|
||||
(`jobs/users.rs`). Parola `/proc/<pid>/cmdline` üzerinden okunabiliyordu
|
||||
ve `'`/`$(...)` içeren parola komutu bozuyordu.
|
||||
|
||||
Doğru desen: **`set_password_via_chpasswd()`** — `chroot <mount> chpasswd
|
||||
-c SHA512` komutuna stdin pipe ile yazılır. `jobs/partition.rs::
|
||||
run_luks_format` ile birebir aynı desen. Yazılan veri yazımdan sonra
|
||||
sıfırlanır.
|
||||
|
||||
> **Kural:** parola, LUKS anahtarı, herhangi bir gizli değer
|
||||
> `format!` ile komut satırına gömülmez. stdin pipe kullanılır.
|
||||
|
||||
### LUKS çekirdek parametresi
|
||||
`cryptdevice=UUID=<x>:cryptroot` **geçerli bir söz dizimi değildi** ve
|
||||
kök bölüm `is_uefi` ile (3|2) tahmin ediliyordu — manuel modda yanlış
|
||||
(assign_partition_numbers'tan gelir) ve kök başka diskteyse hiç doğru
|
||||
değildi.
|
||||
|
||||
Artık `luks_backing_device()` (`cryptsetup status` → `device:` satırı) ile
|
||||
mapper'ın arkasındaki **gerçek** aygıt bulunur, blkid ile LUKS başlık
|
||||
UUID'si alınır, dracut (`rd.luks.uuid=luks-…`) + mkinitcpio
|
||||
(`cryptdevice=…`) biçimleri birlikte yazılır. UUID boşsa hata.
|
||||
|
||||
`CRYPTROOT_NAME` sabiti: "cryptroot" 5 yerde hardcoded'dı, tek kaynağa bağlandı.
|
||||
|
||||
### KRİTİK: parolasız LUKS reddediliyor
|
||||
`encrypt_root=true` + boş parola → kök düz ext4 formatlanıyor,
|
||||
`MountPartitionsJob` `/dev/mapper/cryptroot`'u mount etmeye çalışıp
|
||||
kurulumu öldürüyordu. `PartitionJob` başında `Err` ile reddediliyor.
|
||||
|
||||
---
|
||||
|
||||
## Hata Yönetimi
|
||||
|
||||
### Temizlik HER ZAMAN çalışır
|
||||
`JobQueue::cleanup` + `with_cleanup(...)`: liste başarıda da hatada da
|
||||
koşar. Temizlik de hata verirse bu hatalar asıl hataya **eklenip**
|
||||
kullanıcıya "elle ne yapılacak" listesi olarak gösterilir.
|
||||
|
||||
`EmergencyCleanupJob` (`src/jobs/cleanup.rs`) sırasıyla:
|
||||
1. `mount` altındaki tüm dosya sistemleri
|
||||
2. LVM: `vgchange -an`
|
||||
3. LUKS: `cryptsetup close` (yalnızca **hedef diskin** mapper'ları)
|
||||
4. `swapoff`
|
||||
|
||||
`close_crypt_mappers` yalnızca hedef diske ait olanları kapatır —
|
||||
canlı ortamın kendi şifreli bölümlerine dokunmaz.
|
||||
|
||||
### unmount artık tüm ağacı tarar
|
||||
`UnmountBindJob` elle yazılmış sabit liste (`proc`, `sys`, `dev`,
|
||||
`boot/efi`) kullanıyordu; manuel düzende `/home`, `/var`, `/boot`
|
||||
bağlı kalıyordu. Artık `mounted_paths_under()` `/proc/self/mountinfo`
|
||||
okuyor ve `mount/` altındaki **her şeyi** en derinden yüzeye çözüyor.
|
||||
|
||||
> `unescape_mount_field()` mountinfo'daki `\0NNN` oktal kaçışlarını
|
||||
> çözer; boşluk içeren mountpoint'ler ("My Disk") eşleştirilemezdi.
|
||||
|
||||
### Kritik hatalar artık yutulmuyor
|
||||
- `RunComarJob` tamamen yorum satırıydı → `pisi configure-pending`
|
||||
**hiç çalışmıyordu**. Geri getirildi.
|
||||
- `ldconfig` / `update-environment` hataları loglanıp yutuluyordu.
|
||||
`update-environment` init finalizasyonudur: yutulursa kurulum
|
||||
"başarılı" görünür ama sistem açılmaz. Artık **fatal**.
|
||||
- `CleanupLiveJob`'daki 10 `let _ =`: depo tanımları sessizce
|
||||
başarısız olabiliyordu. Artık **fatal**.
|
||||
|
||||
---
|
||||
|
||||
## partition "MB" tuzağı (KRİTİK)
|
||||
|
||||
parted `"MB"` sonekini **ondalık** (10^6) okur, bizim hesaplar **ikili**
|
||||
(2^20). `mkpart 3944MB` → sektör 7703125 (hizasız) → "Becerebildiğimiz
|
||||
buna en yakın konum..." hatası.
|
||||
|
||||
> parted'a **daima** `"{}MiB"` geç. Tamsayı MiB = 2048 sektörün katı →
|
||||
> hizalama hatası çıkmaz. UI'da kullanıcıya MB gösterilir ama parted
|
||||
> argümanı asla "MB" **olmamalı**.
|
||||
|
||||
Bu tuzak iki kez yaşandı: `7209d58`'de düzeltildi, `22de7d5`ten sonra
|
||||
kök bölümde `"{}MB"` olarak **geri gelmişti**. `d161556` ile düzeltildi ve
|
||||
nedeni yorumla sabitlendi. Log bile "MiB" derken argüman "MB" gönderiyordu.
|
||||
|
||||
---
|
||||
|
||||
## Diğer Kalıcı Kurallar
|
||||
|
||||
- MB dönüşümleri **tek bölme**: `x / 1_048_576` (çift `x/1024/1024`
|
||||
hassasiyet kaybı yapardı)
|
||||
- Simge fontu: `assets/remixicon.ttf` fallback (`ui/theme.rs setup_fonts`).
|
||||
**Kutu (tofu) görürsen** karakter fontta yok. `fontTools` ile cmap kontrol:
|
||||
EB7B check, EA6B arrow, EC28 trash, EA13 add, EFE0 pencil, EECD lock,
|
||||
ECA1 error, EDFB hdd, ECB5/B7 eye, F4C6 close.
|
||||
**Emoji kullanma** — `NotoColorEmoji` yüklü değil (yorum satırında).
|
||||
- **Kare başına alt süreç çalıştırma.** egui `show()` her karede
|
||||
çağrılır (~60 Hz). `blkid`, `nmcli` gibi ağır komutlar `on_enter`'a
|
||||
taşındı, sonuç alanlarda saklanıyor, `refresh_*()` ile tazeleniyor.
|
||||
- ⚠️ **Gradient butonlarda `add_enabled_ui` ÇALIŞMAZ.** `theme::*_button()`
|
||||
çağrıları `enabled: true` sabit geçirir; `add_enabled_ui` sarmalayıcısı
|
||||
yalnızca çocuk widget'ın `Response`'unu etkiler, butonun kendi
|
||||
`enabled` parametresine ulaşamaz. Sonuç: buton seçim yokken de tam
|
||||
renkli ve tıklanabilir görünür ama tıklayınca hiçbir şey olmaz.
|
||||
**Doğrusu:** `theme::secondary_button_enabled(ui, label, can)`
|
||||
/ `danger_button_enabled(...)` kullan. `ui.checkbox` gibi sıradan
|
||||
widget'larda `add_enabled_ui` normal çalışır.
|
||||
- **Eylem butonları tablonun hemen altında** olmalı; aralarına görsel
|
||||
özet bloğu (kapasite çubuğu gibi) girdiğinde kullanıcı butonları
|
||||
bulamıyor. Özet blokları butonlardan SONRA çizilir.
|
||||
- **Butonlar seçime göre görünür** (kullanıcı isteği, 27 Eyl):
|
||||
| Seçim | Görünen buton |
|
||||
|---|---|
|
||||
| bölüm satırı | `✏ Düzenle` + `🗑 Sil` |
|
||||
| boş alan satırı | `➕ Yeni Oluştur` (vurgulu) |
|
||||
| hiçbiri | `➕ Yeni Oluştur` (soluk) |
|
||||
`ManualState::selected_free: bool` boş alan seçimini tutar ve
|
||||
`selected_idx` ile **birbirini dışlar**. Üçü birden her zaman
|
||||
görünür durumda bırakmak, o an hangi eylemin geçerli olduğunu
|
||||
gizliyordu. `selection_tests` bu kuralı sabitler.
|
||||
- ⚠️ **Mount edilmemiş fiziksel bölüm de "Düzenle" açar.** `create_over`
|
||||
("Buraya Oluştur") akışı **KALDIRILDI** — iki sebeple:
|
||||
1. "Yeni Bölüm" ve "Buraya Oluştur" aynı mekanizma; ikisi de yeni
|
||||
tanım üretiyor, kullanıcı ayırt edemiyordu.
|
||||
2. O dal `skip_format = false` atıyordu. Taranmış fiziksel bölümler
|
||||
`skip_format: true` ile gelir (verisi korunmalı) — sıfırlanırsa
|
||||
kurulumda mkfs çalıştırıp **kullanıcının verisini bozardı**.
|
||||
Artık her bölüm satırı "✏ Düzenle" açar ve `skip_format` (diyalogda
|
||||
**"Biçimlendir"**, aşağıya bak) dahil
|
||||
mevcut değerleri korur. `ManualState::create_over` alanı da silindi
|
||||
(ölü durum bırakılmadı). `edit_flow_tests` bunu sabitler.
|
||||
- Locale: `locales/` altında yalnızca `tr.toml` ve `en.toml` **var**.
|
||||
`set_locale("de")` hata vermez, sessizce kabul edilir ve `t!()`
|
||||
karşılığı olmayan her anahtar için **anahtarın kendisini** döndürür —
|
||||
arayüz çıplak anahtarlarla dolar. `funct::sanitize_locale()` her
|
||||
`set_locale` çağrısından önce kullanılır. GeoIP sadece
|
||||
`language_for_country()` ile **gerçekten var olan** dillere geçer.
|
||||
- **TOML düzenleme uyarısı:** `locales/*.toml` satır sonuna yapışan
|
||||
düzenlemelerle bozuldu ve `rust_i18n` proc macro derlemeyi durdurdu.
|
||||
Değiştirdikten sonra `tomllib` ile doğrula.
|
||||
- Bağımlılık: `zbus` (`Cargo.toml`) **hiç kullanılmıyor** — projede tek
|
||||
D-Bus/COMAR çağrısı yok, tüm sistem etkileşimi shell üzerinden.
|
||||
|
||||
---
|
||||
|
||||
## Test / Derleme
|
||||
|
||||
```
|
||||
cargo check --all-targets : 0 hata, 0 uyarı
|
||||
cargo test : 151/151 (test modülü sayısı arttıkça güncel tut)
|
||||
cargo clippy : ~77 uyarı, tamamı stil (collapsed_if, replace zinciri)
|
||||
```
|
||||
|
||||
Bu oturumda 1 → 151 test. Test modülü olmayan dosyalar (`users.rs`,
|
||||
`location.rs`, `rescue.rs`, `funct.rs`, `autoinstall/mod.rs`, `main.rs`)
|
||||
içine modüller eklendi. Çoklu test modülü paylaşılan yardımcıları
|
||||
`steps/partition.rs::testutil` altındadır (`disk()`, `part()`).
|
||||
|
||||
### 🧪 ISO TESTİ BEKLENİYOR (kullanıcı yapacak)
|
||||
`plan_disk_layout` mantığı testlerle kanıtlandı ama **gerçek `parted`
|
||||
çıktısıyla masaüstünde karşılaştırılmadı**. ISO'yu boot edip şu
|
||||
senaryoları dene:
|
||||
|
||||
**Çoklu disk (asıl hedef):**
|
||||
1. İki disk bağlı: sda + sdb
|
||||
2. Manuel moda geç → tablo **sadece seçili diski** göstermeli; tablo
|
||||
altında **disk şeridi OLMAMALI**
|
||||
3. **"Aktif Disk" ComboBox'ı açılmalı ve KOYU TEMADA OKUNMALI**
|
||||
(gradient arkaplan, açık metin rengi)
|
||||
4. ComboBox'tan sdb'yi seç → tablo sdb'nin bölümlerine geçmeli
|
||||
5. sda'ya geri dön → tablo sda'nın bölümleri (seçili satır sıfırlanmalı,
|
||||
yanlış bölüm seçili kalmasın)
|
||||
6. sda'da kök + EFI, sdb'de `/home` → **Aktif Disk ComboBox'ı ile**
|
||||
7. Özet ekranında: iki disk, başlıklarda kapasite, altlarında "boş: …",
|
||||
kök diski "[kök]" etiketiyle işaretli
|
||||
|
||||
**Silme + yeni bölüm (veri kaybı senaryosu):**
|
||||
6. sda'da orta bir bölümü sil → "Boş Alan" satırında birleşsin
|
||||
7. Aynı diske büyük bir yeni bölüm ekle (boşluğa SĞMAYACAK boyutta)
|
||||
8. **Kritik:** korunan bölümün verisi sağlam kalmalı — `mkfs` yanlış
|
||||
aygıya gitmemeli. Logda "bölüm numarası kaydı" mesajı çıkarsa bu
|
||||
normal, "gerçek aygıt kullanılıyor" demektir.
|
||||
|
||||
**Geçiş:**
|
||||
9. Geri/İleri ile adımlar arasında gezin, bölüm tanımlarının korunduğunu doğrula
|
||||
10. Önyükleyici adımında GRUB aygıtının **kökün olduğu disk** olduğunu doğrula
|
||||
|
||||
---
|
||||
|
||||
## Bilinen Açıklar / Yapılacaklar
|
||||
|
||||
### Yüksek öncelik
|
||||
- [ ] **ISO testi yapılmadı** (yukarıdaki kontrol listesi)
|
||||
- [ ] `plan_disk_layout` gerçek `parted` ile doğrulanmadı
|
||||
- [ ] Silinmek işaretli bölüm bir VG'nin PV'siyse `SetupLvmJob`
|
||||
tarafında kontrol yok
|
||||
- [ ] `VolumeGroup.pv_devices` bir PV silinince bayat kalıyor;
|
||||
VG yeniden hesaplanmıyor (silinen PV `pv_devices` içinde kalır,
|
||||
job `pvcreate` çalıştırmaya çalışabilir)
|
||||
- [ ] BIOS `device.map` `/dev` **alfabetik** sıraya göre yazılıyor;
|
||||
firmware sırası ile aynı olmayabilir → çoklu diskte BIOS boot kırılgan
|
||||
- [ ] ~~`manual_capacity_error` sembolik tanımları hesaba katmıyor~~ —
|
||||
KAPANDI (bkz. "Kalan Alan" bölümü; `has_symbolic_remaining`)
|
||||
|
||||
### Orta öncelik
|
||||
- [ ] `available_disks` `< 4 GB` diski tamamen gizliyor (ComboBox'ta yok,
|
||||
taranmıyor, kapasite bilgisi yok)
|
||||
- [ ] `disk_size_mb` bilinmeyen disk için 0 döndürüyor → boyut slider'ı
|
||||
kullanılamaz hale geliyor
|
||||
- [ ] `disk_free_regions_mb`'in eski `SECTOR: u64 = 512` varsayımı
|
||||
4Kn (advanced format) disklerde 8× yanlış boyutlar. sysfs
|
||||
`queue/logical_block_size` okunmalı. (Yeni kod hâlâ 512 varsayıyor.)
|
||||
- [ ] `UnmountBindJob` artık tüm ağacı tarıyor ama `EmergencyCleanupJob`
|
||||
yalnızca `custom_partitions`'dan gelen aygıtları biliyor
|
||||
- [ ] `rescue.rs`: `pisi takeback` **senkron** çalışıyor (egui thread'i
|
||||
donuyor); başarısız `mount` başarı diye işaretleniyor
|
||||
- [ ] Hata tipi: `thiserror` ile `InstallError` enum'ı (≥10 varyant)
|
||||
- [ ] `is_complete` → `Result<(), Vec<String>>` veya ayrı
|
||||
`validation_error()`; hatayı gösterme kanalı hâlâ dolambaçlı
|
||||
- [ ] 51 `let _ =` sessiz hata yutma (42'si `jobs/`), çoğu yorumlanmış
|
||||
değil
|
||||
- [ ] `theme.rs`: paletler 4 yerde çelişkili; `branding.toml [theme]`
|
||||
deserialize ediliyor ama **hiç okunmuyor**; `slides.icons` ve
|
||||
`interval_secs` yok sayılıyor
|
||||
- [ ] `setup_fonts` her tema değişiminde 5+ sistem fontu senkron okuyup
|
||||
atlası yeniden rasterize ediyor
|
||||
- [ ] Kabuk betiği olarak gömülü 120 satırlık GRUB mantığı
|
||||
(`bootloader.rs::30_uefi-firmware`), test yok
|
||||
- [ ] `grub2-mkconfig` hedef sistemin kendi betiğini `sed` ile yamıyor,
|
||||
`2>/dev/null` ile çalışıyor, `let _ =` ile yutuluyor → süreç
|
||||
ortada ölürse kurulu sistem yamalı kalıyor
|
||||
- [ ] `is_mount_point` `/proc/mounts` okurken `\040` kaçışlarını çözmüyor
|
||||
- [ ] Debug `eprintln!` / `println!` satırları (GUI uygulamasında) —
|
||||
kısmen: disk ComboBox'ındaki `println!("Seçilen Disk Değişti")`
|
||||
**silindi**; kalanlar `main.rs` CLI yolu, `branding.rs` ve
|
||||
`slideshow.rs` başlangıç uyarıları (GUI'de görünmez ama
|
||||
stdout'a yazıyor)
|
||||
- [ ] ~100 ölü i18n anahtarı (slayt + eski ekran anahtarları)
|
||||
|
||||
### Bilinçli olarak yapılmadı
|
||||
- Otomatik mod (erase disk) çoklu disk değil — tek disk. İstenmedi.
|
||||
- Bölüm küçültme (shrink) — yok, "sil + yeniden oluştur" yaklaşımı
|
||||
kullanılıyor (eski Yali ile aynı).
|
||||
- Lisans ekranı, ekran yöneticisi seçimi, paket seçimi — squashfs
|
||||
akışında anlamsız, dormantanmış durumda.
|
||||
|
||||
---
|
||||
|
||||
## Dosya Haritası (değişmiş olanlar)
|
||||
|
||||
| Dosya | Son durum |
|
||||
|---|---|
|
||||
| `src/jobs/partition.rs` | `plan_disk_layout` (canlı kernel ile doğrulandı), `PhysicalExtent`, `PlannedCreate`, `disk_total_mb`, `read_physical_extents`, `device_map`; `disk_free_regions_mb` ve `remapped` silindi |
|
||||
| `src/jobs/cleanup.rs` | **YENİ** — `EmergencyCleanupJob` |
|
||||
| `src/steps/partition.rs` | **tek diske odaklı tablo, şerit YOK** (diyalogda da disk seçicisi yok), **seçime göre görünen butonlar** (`selected_free`), diske filtreli LVM, `validate_manual_on`, `manual_capacity_error`, `mountpoint_required`, `suggest_mountpoint`, `has_symbolic_remaining`, `build_table_rows`, `normalize_lsblk_mountpoint`, `on_disk_changed`, `format_checkbox_from`, `skip_format_from`; `multidisk_tests`, `table_scope_tests`, `free_space_tests`, `symbolic_capacity_tests`, `selection_tests`, `edit_flow_tests`, `edit_disk_tests`, `testutil` |
|
||||
| `src/jobs/users.rs` | `set_password_via_chpasswd` (stdin pipe) |
|
||||
| `src/jobs/bootloader.rs` | `CRYPTROOT_NAME`, `luks_backing_device`, `mounted_root_device` |
|
||||
| `src/jobs/finish.rs` | `RunComarJob` geri geldi; `ldconfig`/`update-environment`/depo fatal |
|
||||
| `src/jobs/mod.rs` | `JobQueue::cleanup`, `mounted_paths_under`, `unescape_mount_field`, `open_crypt_mappers`, `auto_mode_partitions` |
|
||||
| `src/installer.rs` | `StepRole`, `license_accepted`, `rescue_available` |
|
||||
| `src/autoinstall/mod.rs` | `[system]` bölümü, `SystemConfig` (elle `Default`!) |
|
||||
| `src/funct.rs` | `AVAILABLE_LANGUAGES`, `sanitize_locale` |
|
||||
| `src/ui/buttons.rs` | `gradient_button` — `enabled: false` iken gri + `Sense::hover()` (tıklanamaz). Gradient butonlarda `add_enabled_ui` ÇALIŞMAZ, `*_button_enabled` gerekir. |
|
||||
| `src/lib.rs` | **SİLİNDİ** — çift crate root, `locales/` iki kez gömülüyordu |
|
||||
| `src/steps/display_manager.rs`, `netinstall.rs`, `license.rs` | dormant (derleniyor, sihirbaza bağlı değil) |
|
||||
|
||||
---
|
||||
|
||||
## ✅ ÇÖZÜLDÜ: `plan_disk_layout` gerçek kernel ile doğrulandı ve düzeltildi
|
||||
|
||||
**28 Eyl 2026.** Bu bir P0 **veri kaybı** hatasıydı; canlı ISO'da
|
||||
ölçüldü, düzeltildi ve düzeltme gerçek kernel ile doğrulandı.
|
||||
|
||||
### ❌ Eski (YANLIŞ) varsayım
|
||||
|
||||
> *"parted rm N sonrası kernel bölümleri başlangıç offset'ine göre
|
||||
> sıralayıp 1'den numaralandırır."*
|
||||
|
||||
Kod buna göre `survivors.sort_by_key(start)` yapıp her şeyi 1'den
|
||||
numaralandırıyor, kaymış bölümleri `remapped` ile bildiriyordu.
|
||||
|
||||
### ✅ Gerçek davranış (ölçüldü)
|
||||
|
||||
**Kernel, bölümü başlangıç offset'ine göre DEĞİL, bölüm tablosu
|
||||
slotuna göre adlandırır.** Bu hem `msdos` hem `gpt` için doğrulandı.
|
||||
|
||||
**msdos ölçümü** (VM'de, reboot sonrası taze kernel okuması):
|
||||
|
||||
```
|
||||
mklabel msdos · 1-101 · 101-301 · 301-5001 · rm 2 · mkpart 5001-6001 MiB
|
||||
→ /dev/sdc1 = 1 MiB (100 MiB) korunan
|
||||
/dev/sdc2 = 5001 MiB (1000 MiB) YENİ
|
||||
/dev/sdc3 = 301 MiB (4700 MiB) korunan — KAYMADI
|
||||
```
|
||||
|
||||
**gpt ölçümü** (host'ta loop + `--partscan`): GPT giriş dizisi ile
|
||||
LBA sırası **bilerek farklı** kuruldu (dizi 2 = 5001 MiB, dizi 3 =
|
||||
301 MiB). Kernel yine dizi sırasını verdi → LBA'ya göre sıralamıyor.
|
||||
|
||||
### Düzeltme
|
||||
|
||||
```rust
|
||||
// hayatta kalanlar numaralarını KORUR
|
||||
let mut taken: BTreeSet<u32> = survivors.iter().filter_map(|(_,_,o)| *o).collect();
|
||||
// her yeni bölüm en küçük BOŞ slotu alır
|
||||
for p in planned.iter_mut() {
|
||||
let num = (1..).map(|n| n as u32).find(|n| !taken.contains(n))...;
|
||||
}
|
||||
```
|
||||
|
||||
`remapped` **tamamen kaldırıldı** — hayatta kalan bölüm artık kaymadığı
|
||||
için hep boş kalıyordu; ölü durum bırakılmadı. Dönüş tipi
|
||||
`Result<Vec<PlannedCreate>, String>` oldu, FAZ B'deki döngü silindi.
|
||||
|
||||
### Uçtan uca doğrulama
|
||||
|
||||
Kodun tahmini ile gerçek kernel **birebir** örtüştü (2 yeni bölüm +
|
||||
1 silme, `slot 2` ve `slot 4` — slot 3'ü hayatta kalan bölüm tutuyor):
|
||||
|
||||
```
|
||||
kernel: p1=1MiB/100 p2=5001MiB/300 p3=301MiB/4700 p4=5301MiB/1000
|
||||
kod : sdc2 5001-5301 sdc4 5301-6301
|
||||
```
|
||||
|
||||
### Bu neden önemliydi
|
||||
|
||||
Eski kod `device_map`'i **ters** yazıyordu: yeni bölüme `sdX3`,
|
||||
korunan bölüme `sdX2` diyordu. FAZ C `mkfs`'i **korunması gereken
|
||||
4700 MiB'lik bölüme** uygulardı — tam olarak `plan_disk_layout`'ın
|
||||
*önlemek için* yazıldığı veri kaybı.
|
||||
|
||||
> 💡 **Bu, `assign_partition_numbers`'ın "en küçük boş slot" kuralının
|
||||
> zaten DOĞRU olduğu anlamına gelir.** `plan_disk_layout`'ın bütün
|
||||
> varlık sebebi (offset'e göre yeniden numaralandırma) gerçekte
|
||||
> gerekli değilmiş. Yeni bölümlere verilen konum (first-fit /
|
||||
> en büyük boş bölge) hâlâ gerekli ve kaldı; **yalnızca numaralandırma
|
||||
> basitleştirildi.**
|
||||
>
|
||||
> Ölçüm, tahminin yerine geçmiştir. Ders: "kernel şunu yapar" diye
|
||||
> inanılan her kural **ölçülmelidir** — bu proje tam da o yüzden
|
||||
> `plan_disk_layout`'a 11 test yazmış ama testler de aynı yanlış
|
||||
> varsayımı paylaşıyordu.
|
||||
@@ -1,5 +1,28 @@
|
||||
# Changelog — [English](CHANGELOG_en.md)
|
||||
|
||||
## [4.0.5] — 2026-07-10
|
||||
|
||||
### Added
|
||||
- `slides/slides_tr.toml`, `slides/slides_en.toml` — dil bazlı slayt içerik dosyaları (`[icons]` eşlemesi yerine doğrudan dosya yolu)
|
||||
- `src/bin/slides_test.rs` — bağımsız slayt gösterisi test ikilisi (arg: `[desktop] [lang]`)
|
||||
- `default-run = "yali-rs"` (`Cargo.toml`) — `cargo run` belirsizliği giderildi
|
||||
|
||||
### Changed
|
||||
- Slayt görselleri `assets/*.png` → `slides/img/` altına taşındı
|
||||
- `branding.toml` / `branding_tr.toml` / `branding_en.toml` içindeki `[slides]` bölümü kaldırıldı; içerik `slides/` dizinindeki dil dosyalarına taşındı
|
||||
- `branding.rs::load_for_lang()` — slaytları `slides/slides_{lang}.toml`'den yükler (fallback: `slides/slides.toml`)
|
||||
- `slideshow.rs` — `slide.icon` doğrudan dosya yolu içeriyorsa `file://` URI'sine çevirir (artık `[icons]` eşlemesi gerekmez)
|
||||
- `CopyFilesJob` — rsync çıktı formatı `--info=progress2` → `--out-format=%n`, ilerleme `find` ile toplam dosya sayısı üzerinden hesaplanır
|
||||
- Otomatik kurulum CLI çıktısına alt ilerleme ve dosya adı bilgisi eklendi
|
||||
|
||||
### Fixed
|
||||
- Edit diyalog penceresinin her frame'de büyümesi (`default_size` + min/max genişlik)
|
||||
- TextEdit alanı `available_width * 0.90` yerine `desired_width()` ile düzgün genişlik
|
||||
- `cargo run -- --demo` belirsizliği (çift ikili)
|
||||
- `locales/tr.toml` "Hoş Geldiniz12" yazım hatası
|
||||
|
||||
---
|
||||
|
||||
## [4.0.4] — 2026-06-25
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# Changelog — [Türkçe](CHANGELOG.md)
|
||||
|
||||
## [4.0.5] — 2026-07-10
|
||||
|
||||
### Added
|
||||
- `slides/slides_tr.toml`, `slides/slides_en.toml` — language-specific slide content files (direct file paths instead of `[icons]` mapping)
|
||||
- `src/bin/slides_test.rs` — standalone slideshow test binary (args: `[desktop] [lang]`)
|
||||
- `default-run = "yali-rs"` (`Cargo.toml`) — fixes `cargo run` ambiguity
|
||||
|
||||
### Changed
|
||||
- Slide images moved from `assets/*.png` → `slides/img/`
|
||||
- `[slides]` section removed from `branding.toml`/`branding_tr.toml`/`branding_en.toml`; content moved to `slides/slides_{lang}.toml`
|
||||
- `branding.rs::load_for_lang()` — loads slides from `slides/slides_{lang}.toml` (fallback: `slides/slides.toml`)
|
||||
- `slideshow.rs` — if `slide.icon` contains a file path, converts it to `file://` URI directly (no `[icons]` mapping needed)
|
||||
- `CopyFilesJob` — rsync output format `--info=progress2` → `--out-format=%n`, progress calculated via `find` total file count
|
||||
- Auto-install CLI output now shows sub-progress and current file name
|
||||
|
||||
### Fixed
|
||||
- Edit dialog window growing every frame (`default_size` + min/max width)
|
||||
- TextEdit field width using `desired_width()` instead of `available_width * 0.90`
|
||||
- `cargo run -- --demo` ambiguity (dual binary)
|
||||
- `locales/tr.toml` "Hoş Geldiniz12" typo
|
||||
|
||||
---
|
||||
|
||||
## [4.0.4] — 2026-06-25
|
||||
|
||||
### Added
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "yali-rs"
|
||||
version = "4.0.0"
|
||||
edition = "2024"
|
||||
default-run = "yali-rs"
|
||||
|
||||
[dependencies]
|
||||
eframe = { version = "0.27", features = ["default", "wgpu"] }
|
||||
@@ -15,8 +16,13 @@ sysinfo = "0.30"
|
||||
rust-i18n = "3.1"
|
||||
lazy_static = "1.4"
|
||||
async-trait = "0.1"
|
||||
chrono = "0.4"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono-tz = "0.8"
|
||||
xkbcommon = "0.9"
|
||||
|
||||
[[bin]]
|
||||
name = "yali-rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
[patch.crates-io]
|
||||
# Yerel mudur veya comar-api kütüphaneleri buraya eklenebilir
|
||||
|
||||
@@ -12,6 +12,7 @@ BINDIR ?= $(PREFIX)/bin
|
||||
DATADIR ?= $(PREFIX)/share/yali
|
||||
LOCALEDIR ?= $(DATADIR)/locales
|
||||
ASSETSDIR ?= $(DATADIR)/assets
|
||||
SLIDESDIR ?= $(DATADIR)/slides
|
||||
DESKTOPDIR ?= $(PREFIX)/share/applications
|
||||
PIXMAPDIR ?= $(PREFIX)/share/pixmaps
|
||||
|
||||
@@ -128,11 +129,13 @@ install: release
|
||||
install -dm755 $(DESTDIR)$(LOCALEDIR)
|
||||
install -m644 locales/*.toml $(DESTDIR)$(LOCALEDIR)/
|
||||
|
||||
# Slayt dosyaları
|
||||
install -dm755 $(DESTDIR)$(SLIDESDIR)
|
||||
cp -r slides/* $(DESTDIR)$(SLIDESDIR)/
|
||||
|
||||
# .desktop girdisi
|
||||
install -Dm644 assets/yali.desktop $(DESTDIR)$(DESKTOPDIR)/yali.desktop
|
||||
|
||||
# Logo (pixmap)
|
||||
install -Dm644 assets/yali.png $(DESTDIR)$(PIXMAPDIR)/yali.png
|
||||
|
||||
@echo "$(BOLD)$(GREEN)✓ Kurulum tamamlandı$(RESET)"
|
||||
|
||||
@@ -143,6 +146,7 @@ uninstall:
|
||||
rm -rf $(DESTDIR)$(DATADIR)
|
||||
rm -f $(DESTDIR)$(DESKTOPDIR)/yali.desktop
|
||||
rm -f $(DESTDIR)$(PIXMAPDIR)/yali.png
|
||||
rm -rf $(DESTDIR)$(SLIDESDIR)
|
||||
@echo "$(BOLD)$(GREEN)✓ Kaldırıldı$(RESET)"
|
||||
|
||||
# ── Temizlik ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -8,6 +8,7 @@ Rust ve egui ile yeniden yazılmış **Pisi GNU/Linux** kurulum aracı. YALI, ca
|
||||
## Özellikler
|
||||
|
||||
- **10 adımlı kurulum sihirbazı**: Hoş Geldiniz → Konum → Klavye → Ağ → Disk Bölümleme → Kullanıcılar → Bootloader → Özet → Kurulum → Bitir
|
||||
- **Squashfs tabanlı**: kurulu sistem ISO'nun squashfs'inden kopyanır, paketler ağdan kurulmaz. Ekran yöneticisi, masaüstü ortamı ve paketler ISO'da geldiği için sihirbaz bunları **seçmez** (seçseydi kopyalama zaten ezdiği için yanıltıcı olurdu)
|
||||
- **İ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
|
||||
@@ -34,16 +35,22 @@ cargo run --release -- --demo
|
||||
|
||||
# Otomatik kurulum (cevap dosyası ile)
|
||||
sudo cargo run --release -- --auto-install answer.toml
|
||||
|
||||
# Slayt gösterisi testi (bağımsız pencere, root gerekmez)
|
||||
cargo run --bin slides_test [desktop] [lang]
|
||||
# Örn: cargo run --bin slides_test xfce en
|
||||
```
|
||||
|
||||
## Proje Yapısı
|
||||
|
||||
```
|
||||
├── src/
|
||||
│ ├── main.rs # Giriş noktası, eframe App, GUI düzeni
|
||||
│ ├── main.rs # Giriş noktası, eframe App, GUI düzeni (tek crate root)
|
||||
│ ├── 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)
|
||||
│ ├── bin/
|
||||
│ │ └── slides_test.rs # Slayt gösterisi test aracı (bağımsız ikili)
|
||||
│ ├── ui/
|
||||
│ │ ├── theme.rs # Renkler, fontlar, Visuals, section_heading, error/warning/success kutuları
|
||||
│ │ ├── buttons.rs # Gradient butonlar (primary, secondary, danger, vb.)
|
||||
@@ -71,10 +78,14 @@ sudo cargo run --release -- --auto-install answer.toml
|
||||
├── locales/
|
||||
│ ├── tr.toml # Türkçe çeviriler
|
||||
│ └── en.toml # İngilizce çeviriler
|
||||
├── slides/
|
||||
│ ├── img/ # Slayt görselleri (PNG)
|
||||
│ ├── slides_tr.toml # Türkçe slayt içeriği
|
||||
│ └── slides_en.toml # İngilizce slayt içeriği
|
||||
├── assets/
|
||||
│ ├── icons/ # UI ikonları (YALI_icon_LIGHT.svg, language-icon.svg, vb.)
|
||||
│ ├── flags/ # Ülke bayrakları (~250 SVG)
|
||||
│ └── *.png # Slayt ve logo görselleri
|
||||
│ └── pisi-logo-*.png # Dağıtım logoları (branding `[general]`)
|
||||
├── branding.toml # Varsayılan markalama yapılandırması
|
||||
├── branding_en.toml # İngilizce markalama
|
||||
├── branding_tr.toml # Türkçe markalama
|
||||
|
||||
@@ -5,6 +5,7 @@ A rewritten **Pisi GNU/Linux** installation tool in Rust and egui. YALI installs
|
||||
## Features
|
||||
|
||||
- **10-step installation wizard**: Welcome → Location → Keyboard → Network → Disk Partitioning → Users → Bootloader → Summary → Installation → Finish
|
||||
- **Squashfs-based**: the installed system is copied from the ISO's squashfs, packages are not fetched over the network. The display manager, desktop environment and packages come from the ISO, so the wizard does **not** offer them (offering them would be misleading, since the copy would overwrite the choice)
|
||||
- **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
|
||||
@@ -31,6 +32,10 @@ cargo run --release -- --demo
|
||||
|
||||
# Automatic installation (with answer file)
|
||||
sudo cargo run --release -- --auto-install answer.toml
|
||||
|
||||
# Slideshow test (standalone window, no root required)
|
||||
cargo run --bin slides_test [desktop] [lang]
|
||||
# E.g.: cargo run --bin slides_test xfce en
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
@@ -38,9 +43,12 @@ sudo cargo run --release -- --auto-install answer.toml
|
||||
```
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, eframe App, GUI layout
|
||||
│ ├── lib.rs # Library root (reserved for future DLL structure)
|
||||
│ ├── installer.rs # InstallerStep trait, GlobalState, WelcomeStep
|
||||
│ ├── funct.rs # Shared UI helpers (form fields, language selector)
|
||||
│ ├── branding.rs # Branding.toml loader (logo, slides, theme, OEM)
|
||||
│ ├── bin/
|
||||
│ │ └── slides_test.rs # Standalone slideshow test binary
|
||||
│ ├── ui/
|
||||
│ │ ├── theme.rs # Colors, fonts, Visuals, section_heading, error/warning/success boxes
|
||||
│ │ ├── buttons.rs # Gradient buttons (primary, secondary, danger, etc.)
|
||||
@@ -68,10 +76,14 @@ sudo cargo run --release -- --auto-install answer.toml
|
||||
├── locales/
|
||||
│ ├── tr.toml # Turkish translations
|
||||
│ └── en.toml # English translations
|
||||
├── slides/
|
||||
│ ├── img/ # Slide images (PNG)
|
||||
│ ├── slides_tr.toml # Turkish slide content
|
||||
│ └── slides_en.toml # English slide content
|
||||
├── assets/
|
||||
│ ├── icons/ # UI icons (YALI_icon_LIGHT.svg, language-icon.svg, etc.)
|
||||
│ ├── flags/ # Country flags (~250 SVG)
|
||||
│ └── *.png # Slide and logo images
|
||||
│ └── pisi-logo-*.png # Distribution logos (branding `[general]`)
|
||||
├── branding.toml # Default branding configuration
|
||||
├── branding_en.toml # English branding
|
||||
├── branding_tr.toml # Turkish branding
|
||||
|
||||
@@ -36,3 +36,36 @@ password = "changeme123"
|
||||
|
||||
# Bilgisayar adı: harf, rakam ve - ; tire ile başlayamaz/bitemez
|
||||
hostname = "pisilinux"
|
||||
|
||||
# Kök parolası (isteğe bağlı).
|
||||
# Boş bırakılırsa kök hesap KİLİTLENİR (passwd -l root).
|
||||
root_password = ""
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# [system] — tamamen isteğe bağlı.
|
||||
# Bu bölümü hiç yazmazsanız tüm ayarlar güvenli varsayılanlarda kalır.
|
||||
# ─────────────────────────────────────────────
|
||||
[system]
|
||||
# Otomatik oturum açma (varsayılan: false).
|
||||
#
|
||||
# Gözetimsiz kurulumda açık bırakmayın: kurulan sistemde konsola
|
||||
# erişebilen herkes giriş ekranında kullanıcı adını görür ve
|
||||
# oturumu açabilir. Ekran yöneticisi ISO'dan gelir; bu ayar yalnızca
|
||||
# mevcut oturum yöneticisine "bu kullanıcıyı otomatik aç" yazar.
|
||||
autologin = false
|
||||
|
||||
# GRUB menü bekleme süresi, saniye (varsayılan: 5)
|
||||
bootloader_timeout = 5
|
||||
|
||||
# GRUB parolası (isteğe bağlı). Hedefte PBKDF2 ile hashlenir.
|
||||
# UYARI: Bu dosyayı güvenli tutun; düz metin olarak saklanır.
|
||||
bootloader_password = ""
|
||||
|
||||
# Ek çekirdek parametreleri (ör. "quiet splash nomodeset")
|
||||
kernel_options = ""
|
||||
|
||||
# NOT: Ekran yöneticisi ve masaüstü ortamı BU DOSYADA AYARLANMAZ.
|
||||
# YALI kurulu sistemi ISO'nun squashfs'inden kopyar, dolayısıyla
|
||||
# bunlar zaten görüntüde bulunurlar; kurulum anında otomatik tespit
|
||||
# edilirler. Ek paket kurma (netinstall) da bu akışın parçası değildir.
|
||||
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -2,14 +2,14 @@
|
||||
<!-- Generator: Adobe Illustrator 27.7.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Optimized" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 841.8897705 595.2755737" style="enable-background:new 0 0 841.8897705 595.2755737;" xml:space="preserve">
|
||||
<polygon style="fill:#020203;" points="512.5472412,488.2385864 542.3688354,537.3009033 558.0941772,491.724762 "/>
|
||||
<path style="fill:#020203;" d="M277.7966003,213.2223663c-1.1176147-1.0975342,1.4554749,8.9684143,5.0365906,12.5897064
|
||||
<polygon style="fill:#FFFFFF;" points="512.5472412,488.2385864 542.3688354,537.3009033 558.0941772,491.724762 "/>
|
||||
<path style="fill:#FFFFFF;" d="M277.7966003,213.2223663c-1.1176147-1.0975342,1.4554749,8.9684143,5.0365906,12.5897064
|
||||
c6.3496399,6.4062805,11.3096008,7.23172,13.9502563,7.3376465c5.8437805,0.2337646,13.0553894-1.4554596,17.3377991-3.2506104
|
||||
c4.1435852-1.767746,11.404541-5.4748993,14.1529236-10.8822479c0.5825195-1.1559601,2.1731262-3.0972137,1.1742859-7.8927765
|
||||
c-0.7579041-3.6888733-3.1064148-4.980011-5.9698181-4.7754517c-2.8634644,0.1935883-11.5323792,2.5055237-15.7253113,3.7948151
|
||||
c-4.1947021,1.2728424-12.8344116,3.9025269-16.6000366,4.7188416
|
||||
C287.3968811,215.6767426,279.1151123,214.4842987,277.7966003,213.2223663z"/>
|
||||
<path style="fill:#020203;" d="M383.9637451,333.5533752c-1.6582031-0.6026001-35.9649353-14.8140564-40.828064-17.1424255
|
||||
<path style="fill:#FFFFFF;" d="M383.9637451,333.5533752c-1.6582031-0.6026001-35.9649353-14.8140564-40.828064-17.1424255
|
||||
c-3.979248-1.9137573-13.7365417-6.0391541-18.3276062-7.9127808c12.9312439-19.9383545,21.0942688-34.984314,22.1808472-37.276123
|
||||
c2.0106506-4.1929321,15.697876-30.9757843,16.0174561-32.6248322c0.3104858-1.6709595,0.6994324-7.8434601,0.3981323-9.3098907
|
||||
c-0.3013-1.495636-5.3196411,1.3787689-12.1331482,3.6889343c-6.8244629,2.3009491-19.7940674,10.7361145-24.8032837,11.7934723
|
||||
@@ -26,7 +26,7 @@
|
||||
c2.0288696,0.8454895,18.4335022,14.2113342,22.7140503,17.1734619c4.2806396,2.9602051,21.172821,12.3851318,24.832489,13.9483643
|
||||
c3.6596985,1.5832825,17.7249756,8.0681152,18.3166809,5.8730469
|
||||
C388.7592468,347.1237793,385.6236877,334.1834412,383.9637451,333.5533752z"/>
|
||||
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:#020203;" d="M304.6889954,512.2145996
|
||||
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;" d="M304.6889954,512.2145996
|
||||
c3.2871399,2.0087891,6.3916626,3.6523438,9.8614197,5.2958984c6.9394836,3.4697876,14.7920837,7.1221313,22.2794495,9.8614502
|
||||
c10.2266541,3.8349609,20.4532776,6.9395142,30.6799316,9.3135376c5.6611633,1.2783203,11.8701782,2.3740234,17.8966064,3.2871094
|
||||
c0.5478516,0,16.8009033,2.0087891,20.0880432,2.0087891h16.4356689c6.3916321-0.5478516,12.4180603-0.9130859,18.8096924-1.8261719
|
||||
@@ -43,7 +43,7 @@
|
||||
c-12.6006775-4.7481079-25.7492065-10.9571533-38.1672668-19.1749878c-2.1914062-1.4609375-2.3740234-2.921875-2.3740234-4.5654297
|
||||
c0-2.7392883,2.0087891-5.2959595,5.295929-5.2959595C297.7495117,507.4664917,303.5932922,511.6667175,304.6889954,512.2145996z"/>
|
||||
<g>
|
||||
<path style="fill:#020203;" d="M639.8383789,180.4025879l-40.7682495-12.975708V48.3634644
|
||||
<path style="fill:#FFFFFF;" d="M639.8383789,180.4025879l-40.7682495-12.975708V48.3634644
|
||||
c0-3.4697266-2.5567017-5.843811-5.843811-5.843811c-2.5567017,0-84.9176636,28.3059082-91.4918823,30.6799316
|
||||
c-22.3344727,7.4448242-86.798645,29.7334595-86.798645,29.7334595l-0.000061,0.0002441
|
||||
c-1.0336304,0.2964478-2.6376953,0.7871704-4.7411499,1.4482422L252.854248,48.8499756
|
||||
@@ -57,6 +57,6 @@
|
||||
V182.0402222l199.0542603-66.2905273V409.4003296z M587.9303589,55.6682129v108.2130737l-164.4921875-52.3546143
|
||||
L587.9303589,55.6682129z M567.6870728,385.267334l-10.5206299-38.4284668l-60.5161133-18.3422241l-13.0134277,31.3044434
|
||||
l-29.2919922-8.8861084l62.1779785-152.5870361l28.5085449,8.6360474l51.9385376,187.1876831L567.6870728,385.267334z"/>
|
||||
<polygon style="fill:#020203;" points="507.8375244,300.3788452 547.7670288,312.4828491 529.5562744,247.8833618 "/>
|
||||
<polygon style="fill:#FFFFFF;" points="507.8375244,300.3788452 547.7670288,312.4828491 529.5562744,247.8833618 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -22,315 +22,6 @@ pisi_logo_light = "assets/pisi-logo-light.png"
|
||||
# Kurulum penceresinin başlığı (pencere çubuğu)
|
||||
title = "PiSi Linux Kurulumu"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# [slides] – Kurulum sırasında gösterilen slaytlar
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[slides]
|
||||
|
||||
# Slayt geçiş süresi (saniye)
|
||||
interval_secs = 6
|
||||
|
||||
|
||||
# ── Slayt ikonları → görsel dosya yolu eşlemesi ──────────────────
|
||||
# Geliştirici notu: Yeni ikon eklemek için sadece buraya satır ekle.
|
||||
[slides.icons]
|
||||
pisilogo_dark = "assets/pisi-logo-dark.png"
|
||||
pisilogo_light = "assets/pisi-logo-light.png"
|
||||
pisi = "assets/pisi-software-center.png"
|
||||
community = "assets/community.png"
|
||||
pisi_speed = "assets/pisi_speed.png"
|
||||
securety = "assets/securety.png"
|
||||
customization = "assets/customization.png"
|
||||
|
||||
budgie = "assets/budgie.png"
|
||||
budgie_applets = "assets/budgie_applets.png"
|
||||
budgie_raven = "assets/budgie_raven.png"
|
||||
budgie_gtk_theme = "assets/budgie_gtk_theme.png"
|
||||
|
||||
cinnamon = "assets/cinnamon.png"
|
||||
cinnamon_panel = "assets/cinnamon_panel.png"
|
||||
cinnamon_settings = "assets/cinnamon_settings.png"
|
||||
cinnamon_desklets = "assets/cinnamon_desklets.png"
|
||||
|
||||
gnome = "assets/gnome.png"
|
||||
gnome_search = "assets/gnome_search.png"
|
||||
gnome_extensions = "assets/gnome_extensions.png"
|
||||
|
||||
kde = "assets/kde.png"
|
||||
kde_connect = "assets/kde_connect.png"
|
||||
discover = "assets/discover.png"
|
||||
file_manager = "assets/file_manager.png"
|
||||
|
||||
lxde = "assets/lxde.png"
|
||||
lxqt = "assets/lxqt.png"
|
||||
lumina = "assets/lumina.png"
|
||||
|
||||
mate = "assets/mate.png"
|
||||
mate_settings = "assets/mate_settings.png"
|
||||
|
||||
xfce = "assets/xfce.png"
|
||||
xfce_settings = "assets/xfce_settings.png"
|
||||
xfce_panel = "assets/xfce_panel.png"
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# [[slides.items]] – Slayt içerikleri
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Her [[slides.items]] bloğu bir slaytı tanımlar.
|
||||
#
|
||||
# Alanlar:
|
||||
# icon = Görsel için yukarıdaki [slides.icons] anahtarı
|
||||
# title = Slayt başlığı
|
||||
# description = Slayt açıklama metni
|
||||
# desktop = (isteğe bağlı) Yalnızca belirli masaüstünde göster.
|
||||
# Boş bırakılırsa tüm masaüstlerinde gösterilir.
|
||||
# Değerler: "xfce", "kde", "gnome", "mate", "budgie",
|
||||
# "cinnamon", "lxde", "lxqt", "lumina"
|
||||
#
|
||||
# Geliştirici notu:
|
||||
# Yeni bir slayt eklemek veya metni değiştirmek için yalnızca
|
||||
# buraya [[slides.items]] bloğu ekleyin / düzenleyin.
|
||||
# Rust koduna dokunmanıza gerek yoktur.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Tüm masaüstlerinde gösterilen ortak slaytlar ──────────────────
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisilogo"
|
||||
title = "Pisi GNU/Linux'a Hoş Geldiniz"
|
||||
description = "Pisi; hız, kararlılık ve kullanım kolaylığını bir araya getiren Türkçe destekli bir GNU/Linux dağıtımıdır."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisi"
|
||||
title = "PISI Paket Yöneticisi"
|
||||
description = "Binlerce pakete tek komutla erişin. `pisi install <paket>` ile ihtiyacınız olan her şeyi kolayca kurabilirsiniz."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "community"
|
||||
title = "Topluluk Desteği"
|
||||
description = "forum.pisilinux.org adresindeki aktif topluluğumuz sorularınızı yanıtlamak için hazır. Wiki sayfalarımız da her an erişilebilir."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisi_speed"
|
||||
title = "Hız ve Performans"
|
||||
description = "Optimize edilmiş çekirdek ve başlangıç servisleri sayesinde PisiLinux saniyeler içinde açılır ve minimum kaynak kullanır."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "securety"
|
||||
title = "Güvenlik Önce Gelir"
|
||||
description = "Düzenli güvenlik güncellemeleri, AppArmor profilleri ve güvenli varsayılan ayarlarla sisteminizi koruyun."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "customization"
|
||||
title = "Özelleştirme"
|
||||
description = "Masaüstü ortamından tema renklerine, panel düzeninden klavye kısayollarına kadar her şeyi zevkinize göre ayarlayın."
|
||||
|
||||
# ── XFCE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce"
|
||||
title = "PisiLinux XFCE"
|
||||
description = "Hafif, hızlı ve kullanıcı dostu XFCE masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce_panel"
|
||||
title = "XFCE Panel"
|
||||
description = "Geleneksel panel düzeni ile görev çubuğu, uygulama menüsü ve sistem tepsisi her zaman elinizin altında."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "file_manager"
|
||||
title = "Thunar Dosya Yöneticisi"
|
||||
description = "Hızlı ve hafif Thunar ile dosya işlemlerinizi kolayca yapın. Ağaç görünümü, sekmeli gezinme ve özelleştirilebilir özellikler sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce_settings"
|
||||
title = "Ayarlar Merkezi"
|
||||
description = "Klasik uygulama menüsüne modern bir alternatif. Whisker Menu ile uygulamaları kolayca bulun, favorilerinizi düzenleyin."
|
||||
|
||||
# ── KDE Plasma ────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "kde"
|
||||
title = "PisiLinux KDE Plasma"
|
||||
description = "Modern, özelleştirilebilir ve güçlü KDE Plasma masaüstü ortamıyla tanışın. Her şey elinizin altında."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "file_manager"
|
||||
title = "Dolphin Dosya Yöneticisi"
|
||||
description = "Sekmeli gezinme, ikili panel ve güçlü arama özelliğiyle Dolphin dosyalarınızı yönetmeyi kolaylaştırır."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "kde_connect"
|
||||
title = "KDE Bağlan"
|
||||
description = "Telefonunuzu bilgisayarınıza bağlayın. Bildirimler, dosya aktarımı ve klipboard paylaşımı KDE Connect ile mümkün."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "discover"
|
||||
title = "Discover Yazılım Merkezi"
|
||||
description = "Discover ile uygulamaları görsel arayüzden kolayca bulun, kurun ve güncelleyin."
|
||||
|
||||
# ── GNOME ─────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome"
|
||||
title = "PisiLinux GNOME"
|
||||
description = "Sade ve odaklı GNOME masaüstü ortamı ile dikkat dağıtıcı unsurları azaltın, işinize konsantre olun."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome_search"
|
||||
title = "GNOME Arama"
|
||||
description = "Super tuşuna basıp yazmaya başlayın. Uygulama, dosya ve ayarlara anında ulaşın."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome_extensions"
|
||||
title = "GNOME Eklentileri"
|
||||
description = "extensions.gnome.org adresinden yüzlerce eklentiyle masaüstünüzü ihtiyaçlarınıza göre şekillendirin."
|
||||
|
||||
# ── MATE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "mate"
|
||||
title = "PisiLinux MATE"
|
||||
description = "Geleneksel masaüstü düzenini sevenler için MATE. GTK+ 3 ile geliştirilmiş, modern ve akıcı bir deneyim sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "file_manager"
|
||||
title = "Caja Dosya Yöneticisi"
|
||||
description = "MATE'in güçlü dosya yöneticisi Caja ile dosya işlemlerinizi kolayca yapın. Çift panel, sekmeli gezinme ve geniş özellikler sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "mate_settings"
|
||||
title = "MATE Ayarları"
|
||||
description = "Görünüm, ekran, klavye ve daha fazlası için kapsamlı ayarlarla MATE masaüstünüzü özelleştirin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "customization"
|
||||
title = "Zengin Uygulama Paketi"
|
||||
description = "MATE; metin editöründen görüntü görüntüleme yazılımına kadar geniş, iyi entegre edilmiş klasik uygulamalar sunar."
|
||||
|
||||
# ── Budgie ────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie"
|
||||
title = "PisiLinux Budgie"
|
||||
description = "Modern, sade ve şık Budgie masaüstü. GNOME altyapısı üzerine inşa edilmiş, kullanıcı dostudur."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_applets"
|
||||
title = "Budgie Desktop Applets"
|
||||
description = "Budgie'nin panel appletleri sayesinde hava durumu, sistem izleyici, takvim gibi araçları kolayca ekleyin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_raven"
|
||||
title = "Raven Bildirim Merkezi"
|
||||
description = "Raven panelinden ses seviyesi, medya kontrolleri, bildirimler ve hızlı ayarlara erişin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_gtk_theme"
|
||||
title = "GTK Tema Desteği"
|
||||
description = "Budgie, GTK temalarını ve GNOME uygulamalarını sorunsuz çalıştırır, görsel bütünlük sağlar."
|
||||
|
||||
# ── Cinnamon ──────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon"
|
||||
title = "PisiLinux Cinnamon"
|
||||
description = "Geleneksel masaüstü deneyimini modern özelliklerle birleştiren Cinnamon, güçlü ve esnek bir ortam sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_panel"
|
||||
title = "Cinnamon Paneli"
|
||||
description = "Panelinizi applet'ler, başlatıcılar ve sistem göstergeleriyle özelleştirin; klasik ama güçlü bir masaüstü deneyimi yaşayın."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_settings"
|
||||
title = "Ayarlar Yöneticisi"
|
||||
description = "Cinnamon'un kapsamlı Ayarlar Yöneticisi aracılığıyla masaüstünüzün her yönünü özelleştirin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_desklets"
|
||||
title = "Cinnamon Desklets"
|
||||
description = "Masaüstü widget'ları ile notlar, saatler ve sistem bilgileri her zaman görünür kalsın."
|
||||
|
||||
# ── LXQt ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "lxqt"
|
||||
title = "PisiLinux LXQt"
|
||||
description = "Hafif, hızlı ve modern LXQt masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "file_manager"
|
||||
title = "LXQt Panel"
|
||||
description = "Geleneksel panel düzeni ile görev çubuğu, uygulama başlatıcı ve sistem tepsisi her zaman elinizin altında."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "file_manager"
|
||||
title = "PCManFM-Qt Dosya Yöneticisi"
|
||||
description = "Hafif ve hızlı PCManFM-Qt ile dosya işlemlerini kolayca yönetin. Sekmeli gezinme ve özelleştirilebilir arayüz sunar."
|
||||
|
||||
# ── LXDE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "lxde"
|
||||
title = "PisiLinux LXDE"
|
||||
description = "Efsanevi hafif masaüstü LXDE. Çok düşük kaynak tüketimi ve güvenilir yapısıyla tanınır."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "lxde"
|
||||
title = "Openbox Tabanlı"
|
||||
description = "LXDE, Openbox pencere yöneticisini kullanır. İnce ayarlar ile akıcı bir deneyim sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "file_manager"
|
||||
title = "PCManFM Dosya Yöneticisi"
|
||||
description = "PCManFM, hızlı ve hafif yapısıyla dosya işlemlerinizi kolaylaştırır, ağaç görünümü ve sekme destekler."
|
||||
|
||||
# ── Lumina ────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "lumina"
|
||||
title = "PisiLinux Lumina"
|
||||
description = "Hafif ve bağımsız Lumina masa üstü. Qt tabanlı, BSD ve Linux sistemlerde düzgün çalışan minimal bir ortam."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "customization"
|
||||
title = "Tema Özelleştirme"
|
||||
description = "Lumina'nın tema sistemiyle renk şemaları ve arayüz stilini kolayca özelleştirin."
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# [theme] – Arayüz renk şeması (Hex)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -20,304 +20,6 @@ title = "YALI Installation Wizard"
|
||||
# Subtitle displayed in the left sidebar
|
||||
subtitle = "YALI"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# [slides] – Slides shown during installation
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[slides]
|
||||
|
||||
# Slide transition duration (seconds)
|
||||
interval_secs = 6
|
||||
|
||||
|
||||
# ── Slide Icons → File Path Mappings ─────────────────────────────
|
||||
[slides.icons]
|
||||
pisilogo = "assets/pisilinux-logo.png"
|
||||
pisi = "assets/pisi-software-center.png"
|
||||
community = "assets/community.png"
|
||||
pisi_speed = "assets/pisi_speed.png"
|
||||
securety = "assets/securety.png"
|
||||
customization = "assets/customization.png"
|
||||
|
||||
# Masaüstü İkonları
|
||||
budgie = "assets/budgie.png"
|
||||
budgie_applets = "assets/budgie_applets.png"
|
||||
budgie_raven = "assets/budgie_raven.png"
|
||||
budgie_gtk_theme = "assets/budgie_gtk_theme.png"
|
||||
|
||||
cinnamon = "assets/cinnamon.png"
|
||||
cinnamon_panel = "assets/cinnamon_panel.png"
|
||||
cinnamon_settings = "assets/cinnamon_settings.png"
|
||||
cinnamon_desklets = "assets/cinnamon_desklets.png"
|
||||
|
||||
gnome = "assets/gnome.png"
|
||||
gnome_search = "assets/gnome_search.png"
|
||||
gnome_extensions = "assets/gnome_extensions.png"
|
||||
|
||||
kde = "assets/kde.png"
|
||||
file_manager = "assets/file_manager.png"
|
||||
kde_connect = "assets/kde_connect.png"
|
||||
discover = "assets/discover.png"
|
||||
|
||||
lxde = "assets/lxde.png"
|
||||
lxde_openbox = "assets/lxde_openbox.png"
|
||||
lxde_lxpanel = "assets/lxde_lxpanel.png"
|
||||
|
||||
lxqt = "assets/lxqt.png"
|
||||
lxqt_panel = "assets/lxqt_panel.png"
|
||||
|
||||
lumina = "assets/lumina.png"
|
||||
lumina_applications = "assets/lumina_applications.png"
|
||||
lumina_theme = "assets/lumina_theme.png"
|
||||
lumina_panel = "assets/lumina_panel.png"
|
||||
|
||||
mate = "assets/mate.png"
|
||||
mate_settings = "assets/mate_settings.png"
|
||||
mate_applications = "assets/mate_applications.png"
|
||||
|
||||
xfce = "assets/xfce.png"
|
||||
xfce_panel = "assets/xfce_panel.png"
|
||||
xfce_settings = "assets/xfce_settings.png"
|
||||
|
||||
|
||||
# ── Common Slides ────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
icon = "pisilogo"
|
||||
title = "Welcome to Pisi GNU/Linux"
|
||||
description = "Pisi is a Turkish-supported GNU/Linux distribution that combines speed, stability, and ease of use."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisi"
|
||||
title = "PISI Package Manager"
|
||||
description = "Access thousands of packages with a single command. Install anything you need easily with `pisi install <package>`."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "community"
|
||||
title = "Community Support"
|
||||
description = "Our active community at forum.pisilinux.org is ready to answer your questions. Our wiki pages are always accessible."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisi_speed"
|
||||
title = "Speed & Performance"
|
||||
description = "Thanks to an optimized kernel and startup services, PisiLinux boots in seconds and uses minimal resources."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "securety"
|
||||
title = "Security First"
|
||||
description = "Keep your system secure with regular security updates, AppArmor profiles, and secure default settings."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "customization"
|
||||
title = "Customization"
|
||||
description = "Tailor everything to your taste — from the desktop environment and theme colors to panel layout and keyboard shortcuts."
|
||||
|
||||
# ── Budgie ────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie"
|
||||
title = "PisiLinux Budgie"
|
||||
description = "A modern, clean, and elegant Budgie desktop. Built on the GNOME stack and user-friendly."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_applets"
|
||||
title = "Budgie Desktop Applets"
|
||||
description = "Easily add tools like weather, system monitor, and calendar via Budgie's panel applets."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_raven"
|
||||
title = "Raven Notification Center"
|
||||
description = "Access volume, media controls, notifications, and quick settings from the Raven panel."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_gtk_theme"
|
||||
title = "GTK Theme Support"
|
||||
description = "Budgie runs GTK themes and GNOME applications seamlessly, ensuring visual consistency."
|
||||
|
||||
# ── Cinnamon ──────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon"
|
||||
title = "PisiLinux Cinnamon"
|
||||
description = "Cinnamon combines the traditional desktop experience with modern features, offering a powerful and flexible environment."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_panel"
|
||||
title = "Cinnamon Panel"
|
||||
description = "Customize your panel with applets, launchers, and system indicators for a classic yet powerful desktop."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_settings"
|
||||
title = "Settings Manager"
|
||||
description = "Fine-tune every aspect of your Cinnamon desktop through the comprehensive Settings Manager."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_desklets"
|
||||
title = "Cinnamon Desklets"
|
||||
description = "Keep notes, clocks, and system info always visible on your desktop with Cinnamon Desklets."
|
||||
|
||||
# ── GNOME ─────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome"
|
||||
title = "PisiLinux GNOME"
|
||||
description = "Reduce distractions and focus on your work with the clean and focused GNOME desktop environment."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome_search"
|
||||
title = "GNOME Search"
|
||||
description = "Press the Super key and start typing. Instantly reach applications, files, and settings."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome_extensions"
|
||||
title = "GNOME Extensions"
|
||||
description = "Shape your desktop to your needs with hundreds of extensions from extensions.gnome.org."
|
||||
|
||||
# ── KDE ───────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "kde"
|
||||
title = "PisiLinux KDE Plasma"
|
||||
description = "Meet the modern, customizable, and powerful KDE Plasma desktop. Everything is at your fingertips."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "file_manager"
|
||||
title = "Dolphin File Manager"
|
||||
description = "Dolphin makes managing your files easy with tabbed browsing, dual-panel view, and powerful search."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "kde_connect"
|
||||
title = "KDE Connect"
|
||||
description = "Connect your phone to your computer. Notifications, file transfer, and clipboard sharing — all with KDE Connect."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "discover"
|
||||
title = "Discover Software Center"
|
||||
description = "Easily find, install, and update applications from the visual interface with Discover."
|
||||
|
||||
# ── LXQt ──────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "lxqt"
|
||||
title = "PisiLinux LXQt"
|
||||
description = "A lightweight, fast, and modern LXQt desktop environment. Low resource consumption, high performance."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "lxqt_panel"
|
||||
title = "LXQt Panel"
|
||||
description = "The traditional panel layout puts the taskbar, application launcher, and system tray always within reach."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "file_manager"
|
||||
title = "PCManFM-Qt File Manager"
|
||||
description = "Manage files effortlessly with the lightweight and fast PCManFM-Qt. Tabbed navigation and a customizable interface."
|
||||
|
||||
# ── LXDE ──────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "lxde"
|
||||
title = "PisiLinux LXDE"
|
||||
description = "The legendary lightweight LXDE desktop. Known for extremely low resource usage and a reliable build."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "lxde_openbox"
|
||||
title = "Openbox-Based"
|
||||
description = "LXDE uses the Openbox window manager. Fine-tune it for a smooth and responsive experience."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "file_manager"
|
||||
title = "PCManFM File Manager"
|
||||
description = "PCManFM speeds up your file operations with its fast and lightweight build, supporting tree view and tabs."
|
||||
|
||||
# ── Lumina ────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "lumina"
|
||||
title = "PisiLinux Lumina"
|
||||
description = "A lightweight and independent Lumina desktop. Qt-based, designed to run cleanly on BSD and Linux systems."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "lumina_applications"
|
||||
title = "Application Support"
|
||||
description = "Lumina works well with core utilities and many popular Qt apps, providing a practical everyday experience."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "lumina_theme"
|
||||
title = "Theme Customization"
|
||||
description = "Easily customize color schemes and interface styles with Lumina's flexible theme system."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "lumina_panel"
|
||||
title = "Lumina Panel"
|
||||
description = "A clean panel with a taskbar, application launcher, and system tray keeps your desktop organized."
|
||||
|
||||
# ── MATE ──────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "mate"
|
||||
title = "PisiLinux MATE"
|
||||
description = "MATE for those who love the traditional desktop layout. Developed with GTK+ 3 for a modern and smooth experience."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "file_manager"
|
||||
title = "Caja File Manager"
|
||||
description = "Manage your files easily with MATE's powerful Caja. Dual panel, tabbed navigation, and rich features."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "mate_settings"
|
||||
title = "MATE Settings"
|
||||
description = "Customize your MATE desktop with comprehensive settings for appearance, display, keyboard, and more."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "mate_applications"
|
||||
title = "Rich Application Suite"
|
||||
description = "MATE comes with a rich set of classic applications — from text editors to image viewers, all well-integrated."
|
||||
|
||||
# ── XFCE ──────────────────────────────────────────────────────────
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce"
|
||||
title = "PisiLinux XFCE"
|
||||
description = "A lightweight, fast, and user-friendly XFCE desktop environment. Low resource consumption, high performance."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce_panel"
|
||||
title = "XFCE Panel"
|
||||
description = "The traditional panel layout puts the taskbar, application menu, and system tray always within reach."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "file_manager"
|
||||
title = "Thunar File Manager"
|
||||
description = "Manage your files easily with the fast and lightweight Thunar. Tree view, tabbed navigation, and customizable features."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce_settings"
|
||||
title = "Whisker Menu"
|
||||
description = "A modern alternative to the classic application menu. Find apps easily and organize your favorites with Whisker Menu."
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# [theme] – UI Style & Color Palette
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -30,309 +30,6 @@ subtitle = "YALI"
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
[slides]
|
||||
|
||||
# Slayt geçiş süresi (saniye)
|
||||
interval_secs = 6
|
||||
|
||||
|
||||
# ── Slayt ikonları → görsel dosya yolu eşlemesi ──────────────────
|
||||
# Geliştirici notu: Yeni ikon eklemek için sadece buraya satır ekle.
|
||||
[slides.icons]
|
||||
pisilogo = "assets/pisilinux-logo.png"
|
||||
pisi = "assets/pisi-software-center.png"
|
||||
community = "assets/community.png"
|
||||
pisi_speed = "assets/pisi_speed.png"
|
||||
securety = "assets/securety.png"
|
||||
customization = "assets/customization.png"
|
||||
|
||||
budgie = "assets/budgie.png"
|
||||
budgie_applets = "assets/budgie_applets.png"
|
||||
budgie_raven = "assets/budgie_raven.png"
|
||||
budgie_gtk_theme = "assets/budgie_gtk_theme.png"
|
||||
|
||||
cinnamon = "assets/cinnamon.png"
|
||||
cinnamon_panel = "assets/cinnamon_panel.png"
|
||||
cinnamon_settings = "assets/cinnamon_settings.png"
|
||||
cinnamon_desklets = "assets/cinnamon_desklets.png"
|
||||
|
||||
gnome = "assets/gnome.png"
|
||||
gnome_search = "assets/gnome_search.png"
|
||||
gnome_extensions = "assets/gnome_extensions.png"
|
||||
|
||||
kde = "assets/kde.png"
|
||||
kde_connect = "assets/kde_connect.png"
|
||||
discover = "assets/discover.png"
|
||||
file_manager = "assets/file_manager.png"
|
||||
|
||||
lxde = "assets/lxde.png"
|
||||
lxqt = "assets/lxqt.png"
|
||||
lumina = "assets/lumina.png"
|
||||
|
||||
mate = "assets/mate.png"
|
||||
mate_settings = "assets/mate_settings.png"
|
||||
|
||||
xfce = "assets/xfce.png"
|
||||
xfce_settings = "assets/xfce_settings.png"
|
||||
xfce_panel = "assets/xfce_panel.png"
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# [[slides.items]] – Slayt içerikleri
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Her [[slides.items]] bloğu bir slaytı tanımlar.
|
||||
#
|
||||
# Alanlar:
|
||||
# icon = Görsel için yukarıdaki [slides.icons] anahtarı
|
||||
# title = Slayt başlığı
|
||||
# description = Slayt açıklama metni
|
||||
# desktop = (isteğe bağlı) Yalnızca belirli masaüstünde göster.
|
||||
# Boş bırakılırsa tüm masaüstlerinde gösterilir.
|
||||
# Değerler: "xfce", "kde", "gnome", "mate", "budgie",
|
||||
# "cinnamon", "lxde", "lxqt", "lumina"
|
||||
#
|
||||
# Geliştirici notu:
|
||||
# Yeni bir slayt eklemek veya metni değiştirmek için yalnızca
|
||||
# buraya [[slides.items]] bloğu ekleyin / düzenleyin.
|
||||
# Rust koduna dokunmanıza gerek yoktur.
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Tüm masaüstlerinde gösterilen ortak slaytlar ──────────────────
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisilogo"
|
||||
title = "Pisi GNU/Linux'a Hoş Geldiniz"
|
||||
description = "Pisi; hız, kararlılık ve kullanım kolaylığını bir araya getiren Türkçe destekli bir GNU/Linux dağıtımıdır."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisi"
|
||||
title = "PISI Paket Yöneticisi"
|
||||
description = "Binlerce pakete tek komutla erişin. `pisi install <paket>` ile ihtiyacınız olan her şeyi kolayca kurabilirsiniz."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "community"
|
||||
title = "Topluluk Desteği"
|
||||
description = "forum.pisilinux.org adresindeki aktif topluluğumuz sorularınızı yanıtlamak için hazır. Wiki sayfalarımız da her an erişilebilir."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "pisi_speed"
|
||||
title = "Hız ve Performans"
|
||||
description = "Optimize edilmiş çekirdek ve başlangıç servisleri sayesinde PisiLinux saniyeler içinde açılır ve minimum kaynak kullanır."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "securety"
|
||||
title = "Güvenlik Önce Gelir"
|
||||
description = "Düzenli güvenlik güncellemeleri, AppArmor profilleri ve güvenli varsayılan ayarlarla sisteminizi koruyun."
|
||||
|
||||
[[slides.items]]
|
||||
icon = "customization"
|
||||
title = "Özelleştirme"
|
||||
description = "Masaüstü ortamından tema renklerine, panel düzeninden klavye kısayollarına kadar her şeyi zevkinize göre ayarlayın."
|
||||
|
||||
# ── XFCE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce"
|
||||
title = "PisiLinux XFCE"
|
||||
description = "Hafif, hızlı ve kullanıcı dostu XFCE masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce_panel"
|
||||
title = "XFCE Panel"
|
||||
description = "Geleneksel panel düzeni ile görev çubuğu, uygulama menüsü ve sistem tepsisi her zaman elinizin altında."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "file_manager"
|
||||
title = "Thunar Dosya Yöneticisi"
|
||||
description = "Hızlı ve hafif Thunar ile dosya işlemlerinizi kolayca yapın. Ağaç görünümü, sekmeli gezinme ve özelleştirilebilir özellikler sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "xfce"
|
||||
icon = "xfce_settings"
|
||||
title = "Ayarlar Merkezi"
|
||||
description = "Klasik uygulama menüsüne modern bir alternatif. Whisker Menu ile uygulamaları kolayca bulun, favorilerinizi düzenleyin."
|
||||
|
||||
# ── KDE Plasma ────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "kde"
|
||||
title = "PisiLinux KDE Plasma"
|
||||
description = "Modern, özelleştirilebilir ve güçlü KDE Plasma masaüstü ortamıyla tanışın. Her şey elinizin altında."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "file_manager"
|
||||
title = "Dolphin Dosya Yöneticisi"
|
||||
description = "Sekmeli gezinme, ikili panel ve güçlü arama özelliğiyle Dolphin dosyalarınızı yönetmeyi kolaylaştırır."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "kde_connect"
|
||||
title = "KDE Bağlan"
|
||||
description = "Telefonunuzu bilgisayarınıza bağlayın. Bildirimler, dosya aktarımı ve klipboard paylaşımı KDE Connect ile mümkün."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "kde"
|
||||
icon = "discover"
|
||||
title = "Discover Yazılım Merkezi"
|
||||
description = "Discover ile uygulamaları görsel arayüzden kolayca bulun, kurun ve güncelleyin."
|
||||
|
||||
# ── GNOME ─────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome"
|
||||
title = "PisiLinux GNOME"
|
||||
description = "Sade ve odaklı GNOME masaüstü ortamı ile dikkat dağıtıcı unsurları azaltın, işinize konsantre olun."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome_search"
|
||||
title = "GNOME Arama"
|
||||
description = "Super tuşuna basıp yazmaya başlayın. Uygulama, dosya ve ayarlara anında ulaşın."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "gnome"
|
||||
icon = "gnome_extensions"
|
||||
title = "GNOME Eklentileri"
|
||||
description = "extensions.gnome.org adresinden yüzlerce eklentiyle masaüstünüzü ihtiyaçlarınıza göre şekillendirin."
|
||||
|
||||
# ── MATE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "mate"
|
||||
title = "PisiLinux MATE"
|
||||
description = "Geleneksel masaüstü düzenini sevenler için MATE. GTK+ 3 ile geliştirilmiş, modern ve akıcı bir deneyim sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "file_manager"
|
||||
title = "Caja Dosya Yöneticisi"
|
||||
description = "MATE'in güçlü dosya yöneticisi Caja ile dosya işlemlerinizi kolayca yapın. Çift panel, sekmeli gezinme ve geniş özellikler sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "mate_settings"
|
||||
title = "MATE Ayarları"
|
||||
description = "Görünüm, ekran, klavye ve daha fazlası için kapsamlı ayarlarla MATE masaüstünüzü özelleştirin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "mate"
|
||||
icon = "customization"
|
||||
title = "Zengin Uygulama Paketi"
|
||||
description = "MATE; metin editöründen görüntü görüntüleme yazılımına kadar geniş, iyi entegre edilmiş klasik uygulamalar sunar."
|
||||
|
||||
# ── Budgie ────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie"
|
||||
title = "PisiLinux Budgie"
|
||||
description = "Modern, sade ve şık Budgie masaüstü. GNOME altyapısı üzerine inşa edilmiş, kullanıcı dostudur."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_applets"
|
||||
title = "Budgie Desktop Applets"
|
||||
description = "Budgie'nin panel appletleri sayesinde hava durumu, sistem izleyici, takvim gibi araçları kolayca ekleyin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_raven"
|
||||
title = "Raven Bildirim Merkezi"
|
||||
description = "Raven panelinden ses seviyesi, medya kontrolleri, bildirimler ve hızlı ayarlara erişin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "budgie"
|
||||
icon = "budgie_gtk_theme"
|
||||
title = "GTK Tema Desteği"
|
||||
description = "Budgie, GTK temalarını ve GNOME uygulamalarını sorunsuz çalıştırır, görsel bütünlük sağlar."
|
||||
|
||||
# ── Cinnamon ──────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon"
|
||||
title = "PisiLinux Cinnamon"
|
||||
description = "Geleneksel masaüstü deneyimini modern özelliklerle birleştiren Cinnamon, güçlü ve esnek bir ortam sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_panel"
|
||||
title = "Cinnamon Paneli"
|
||||
description = "Panelinizi applet'ler, başlatıcılar ve sistem göstergeleriyle özelleştirin; klasik ama güçlü bir masaüstü deneyimi yaşayın."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_settings"
|
||||
title = "Ayarlar Yöneticisi"
|
||||
description = "Cinnamon'un kapsamlı Ayarlar Yöneticisi aracılığıyla masaüstünüzün her yönünü özelleştirin."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "cinnamon_desklets"
|
||||
title = "Cinnamon Desklets"
|
||||
description = "Masaüstü widget'ları ile notlar, saatler ve sistem bilgileri her zaman görünür kalsın."
|
||||
|
||||
# ── LXQt ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "lxqt"
|
||||
title = "PisiLinux LXQt"
|
||||
description = "Hafif, hızlı ve modern LXQt masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "file_manager"
|
||||
title = "LXQt Panel"
|
||||
description = "Geleneksel panel düzeni ile görev çubuğu, uygulama başlatıcı ve sistem tepsisi her zaman elinizin altında."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxqt"
|
||||
icon = "file_manager"
|
||||
title = "PCManFM-Qt Dosya Yöneticisi"
|
||||
description = "Hafif ve hızlı PCManFM-Qt ile dosya işlemlerini kolayca yönetin. Sekmeli gezinme ve özelleştirilebilir arayüz sunar."
|
||||
|
||||
# ── LXDE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "lxde"
|
||||
title = "PisiLinux LXDE"
|
||||
description = "Efsanevi hafif masaüstü LXDE. Çok düşük kaynak tüketimi ve güvenilir yapısıyla tanınır."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "lxde"
|
||||
title = "Openbox Tabanlı"
|
||||
description = "LXDE, Openbox pencere yöneticisini kullanır. İnce ayarlar ile akıcı bir deneyim sunar."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lxde"
|
||||
icon = "file_manager"
|
||||
title = "PCManFM Dosya Yöneticisi"
|
||||
description = "PCManFM, hızlı ve hafif yapısıyla dosya işlemlerinizi kolaylaştırır, ağaç görünümü ve sekme destekler."
|
||||
|
||||
# ── Lumina ────────────────────────────────────────────────────────
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "lumina"
|
||||
title = "PisiLinux Lumina"
|
||||
description = "Hafif ve bağımsız Lumina masa üstü. Qt tabanlı, BSD ve Linux sistemlerde düzgün çalışan minimal bir ortam."
|
||||
|
||||
[[slides.items]]
|
||||
desktop = "lumina"
|
||||
icon = "customization"
|
||||
title = "Tema Özelleştirme"
|
||||
description = "Lumina'nın tema sistemiyle renk şemaları ve arayüz stilini kolayca özelleştirin."
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
# [theme] – Arayüz renk şeması (Hex)
|
||||
# ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -57,11 +57,12 @@ users_title = "User Account"
|
||||
users_description = "Create a user account for your system."
|
||||
real_name_label = "Full Name"
|
||||
username_label = "Username"
|
||||
password_label = "Password (at least 8 characters...)"
|
||||
password_label = "Password "
|
||||
password_confirm_label = "Confirm password"
|
||||
hostname_label = "Computer name"
|
||||
username_invalid = "Invalid username. Must start with a lowercase letter or underscore, max 32 characters."
|
||||
username_too_long = "Username too long (maximum 32 characters)"
|
||||
username_empty = "Username cannot be empty"
|
||||
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"
|
||||
@@ -80,6 +81,7 @@ summary_username = "Username"
|
||||
summary_hostname = "Computer name"
|
||||
summary_erase_mode = "Entire disk will be erased"
|
||||
summary_manual_mode = "Manual partitioning"
|
||||
summary_root_disk_tag = "root"
|
||||
summary_not_selected = "Not selected"
|
||||
summary_warning = "⚠ Warning: Clicking Next will permanently erase all data on the selected disk!"
|
||||
|
||||
@@ -98,7 +100,7 @@ finish_title = "Installation Complete"
|
||||
# ── Execution (extra) ─────────────────────────────────────
|
||||
install_failed = "Installation failed"
|
||||
install_done = "Installation complete! Click Next to continue."
|
||||
install_current_file = "Current file"
|
||||
install_current_file = "Copy file"
|
||||
retry_install = "Retry"
|
||||
finish_next = "Finish →"
|
||||
|
||||
@@ -127,6 +129,10 @@ summary_software = "Software"
|
||||
summary_display_manager = "Display Manager"
|
||||
summary_autologin = "Autologin"
|
||||
summary_autologin_user = "Autologin User"
|
||||
summary_autologin_on = "On"
|
||||
summary_autologin_off = "Off (password login)"
|
||||
summary_bootloader_password = "password protected"
|
||||
summary_desktop_from_image = "Comes from the ISO image; detected automatically during install"
|
||||
summary_desktop = "Desktop"
|
||||
|
||||
# ── Display Manager ───────────────────────────────
|
||||
@@ -135,12 +141,24 @@ display_manager_title = "Display Manager Configuration"
|
||||
display_manager_description = "Select your display manager and desktop environment."
|
||||
display_manager_select = "Display manager:"
|
||||
display_manager_de = "Desktop environment:"
|
||||
display_manager_autologin = "Automatic login"
|
||||
display_manager_autologin = "Log in automatically"
|
||||
display_manager_autologin_user = "Autologin user:"
|
||||
display_manager_autologin_hint = "(first user)"
|
||||
display_manager_de_not_found = "(not detected)"
|
||||
display_manager_note = "Note: The display manager will be enabled by default on the installed system."
|
||||
mp_size_remaining = "Remaining Space"
|
||||
mp_size_available = "Available"
|
||||
mp_active_disk = "Active disk"
|
||||
mp_active_disk_hint = "The disk shown in the table and used by New Partition. To add a partition to another disk, switch to that disk in the disk strip above first. The bootloader device defaults to the disk holding the root partition."
|
||||
mp_will_delete = "will be deleted"
|
||||
mp_restore = "Restore"
|
||||
mp_free_space = "Free Space"
|
||||
mp_duplicate_mountpoint = "Two partitions share the same mount point; each partition must have a distinct mount point"
|
||||
mp_multiple_root = "More than one partition is defined as root (/): {disks}. There must be exactly one root partition"
|
||||
mp_partition_no_disk = "A partition definition has no disk selected. Choose the target disk for every partition."
|
||||
mp_disk_not_found = "{disk} is no longer attached. Remove the partition definitions on it, or reconnect the disk."
|
||||
mp_bar_now = "Current Layout"
|
||||
mp_bar_after = "After Installation"
|
||||
|
||||
# ── UI / General ──────────────────────────────────────────
|
||||
installer = "YALI Installation Tool"
|
||||
@@ -204,10 +222,13 @@ job_timezone = "Setting timezone"
|
||||
job_datetime = "Configuring date & time"
|
||||
job_keyboard = "Configuring keyboard layout"
|
||||
job_user = "Creating user account"
|
||||
job_autologin = "Applying automatic login"
|
||||
job_install_packages = "Installing selected packages"
|
||||
job_ldconfig = "Configuring shared libraries (ldconfig)"
|
||||
job_update_env = "Updating system environment"
|
||||
job_cleanup_live = "Cleaning up live environment traces"
|
||||
job_netinstall = "Installing extra packages"
|
||||
job_setup_lvm = "Configuring LVM"
|
||||
|
||||
# ── Manual Partition UI ────────────────────────────────────
|
||||
manual_partition_title = "Manual Partitioning"
|
||||
@@ -216,6 +237,7 @@ mp_device = "Device"
|
||||
mp_size = "Size"
|
||||
mp_fstype = "Filesystem"
|
||||
mp_mountpoint = "Mount Point"
|
||||
mp_format_status = "Format"
|
||||
mp_action = "Action"
|
||||
mp_add = "Add Partition"
|
||||
mp_delete = "Delete"
|
||||
@@ -229,9 +251,18 @@ mp_fs_btrfs = "btrfs"
|
||||
mp_fs_xfs = "xfs"
|
||||
mp_fs_fat32 = "FAT32 (EFI)"
|
||||
mp_fs_swap = "swap"
|
||||
mp_format_partition = "Format (existing data will be erased)"
|
||||
mp_size_gb = "%{n} GB"
|
||||
mp_size_mb = "%{n} MB"
|
||||
|
||||
# ── Table Column Headers ────────────────────────────────────
|
||||
column_device = "Device"
|
||||
column_mount_point = "Mount Point"
|
||||
column_label = "Label"
|
||||
column_type = "Type"
|
||||
column_format = "Format"
|
||||
column_size = "Size"
|
||||
|
||||
|
||||
check_ram = "RAM"
|
||||
check_ram_fail = "%{mb} MB RAM available. Minimum 1 GB required."
|
||||
@@ -265,6 +296,8 @@ job_mount_custom = "Mounting manual partitions"
|
||||
# ── Extra Localization / i18n ─────────────────────────────
|
||||
mp_delete_confirm_title = "Delete Partition"
|
||||
mp_error_empty_mountpoint = "Mount point cannot be empty."
|
||||
mp_mountpoint_optional = "Mount point is optional. Leave it empty to format the partition without mounting it at boot — useful for exchanging data with Windows."
|
||||
mp_error_empty_mountpoint_named = "Mount point cannot be empty: {device}. Pick a mount point from the list, or mark the partition for deletion and continue."
|
||||
mp_plan_valid = "✓ Partition plan is valid. You can proceed."
|
||||
mp_select_disk_first = "← Select a disk first."
|
||||
mp_add_dialog_hint = "/ or /home or swap"
|
||||
@@ -299,6 +332,7 @@ admin_password_short = "Root password must be at least 8 characters."
|
||||
datetime = "Date & Time"
|
||||
datetime_title = "Date & Time Settings"
|
||||
datetime_description = "Configure the system date and time."
|
||||
datetime_current = "Current time:"
|
||||
datetime_ntp_label = "Network Time (NTP):"
|
||||
datetime_ntp_on = "Enabled — time syncs automatically"
|
||||
datetime_ntp_off = "Disabled — set manually"
|
||||
@@ -345,6 +379,8 @@ bootloader_title = "Bootloader Configuration"
|
||||
bootloader_description = "Configure the bootloader (GRUB) settings."
|
||||
bootloader_device_label = "Install to device:"
|
||||
bootloader_device_hint = "(default: target disk)"
|
||||
bootloader_device_hint_dualboot = "For dual boot, pick the disk Windows is installed on. In UEFI mode that disk's Windows ESP is used to add a \"Windows Boot Manager\" entry to the boot menu; if you leave the default, Windows will not appear at boot."
|
||||
bootloader_device_why_existing = "This device was picked because it already holds an operating system: the bootloader has to be written to the disk the firmware already boots."
|
||||
bootloader_timeout_label = "Timeout:"
|
||||
bootloader_timeout_suffix = "seconds"
|
||||
bootloader_password_label = "GRUB password (optional):"
|
||||
@@ -540,6 +576,7 @@ lvm_lv_add = "Add Logical Volume"
|
||||
lvm_lv_error_vg = "At least one Volume Group must be defined."
|
||||
lvm_lv_error_size = "Entered size exceeds Volume Group capacity."
|
||||
lvm_lv_mountpoint = "Mount Point"
|
||||
lvm_no_pv_on_disk = "There is no LVM physical volume on this disk. Add a PV partition to this disk first."
|
||||
lvm_lv_fstype = "File System"
|
||||
lvm_lv_size = "Size (MB, 0=remaining):"
|
||||
lvm_lv_action = "Action"
|
||||
|
||||
@@ -57,11 +57,12 @@ users_title = "Kullanıcı Bilgileri"
|
||||
users_description = "Sisteminiz için bir kullanıcı hesabı oluşturun."
|
||||
real_name_label = "Gerçek Adınız."
|
||||
username_label = "Kullanıcı adı"
|
||||
password_label = "Parola (en az 8 karakter olmalıdır...)"
|
||||
password_label = "Parola"
|
||||
password_confirm_label = "Parola (tekrar)"
|
||||
hostname_label = "Bilgisayar adı"
|
||||
username_invalid = "Geçersiz kullanıcı adı. Küçük harf veya alt çizgi ile başlamalı, en fazla 32 karakter."
|
||||
username_too_long = "Kullanıcı adı çok uzun (maksimum 32 karakter)"
|
||||
username_empty = "Kullanıcı adı boş olamaz"
|
||||
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"
|
||||
@@ -80,8 +81,9 @@ summary_username = "Kullanıcı adı"
|
||||
summary_hostname = "Bilgisayar adı"
|
||||
summary_erase_mode = "Tüm disk silinecek"
|
||||
summary_manual_mode = "Manuel bölümleme"
|
||||
summary_root_disk_tag = "kök"
|
||||
summary_not_selected = "Seçilmedi"
|
||||
summary_warning = "⚠️ Dikkat: İleri'ye basıldığında seçilen diskteki tüm veriler kalıcı olarak silinecektir!"
|
||||
summary_warning = " Dikkat: İleri'ye basıldığında seçilen diskteki tüm veriler kalıcı olarak silinecektir!"
|
||||
|
||||
# ── Execution ─────────────────────────────────────────────
|
||||
execution = "Kurulum"
|
||||
@@ -97,7 +99,7 @@ finish_title = "Kurulum Tamamlandı"
|
||||
# ── Execution (ek) ────────────────────────────────────────
|
||||
install_failed = "Kurulum başarısız"
|
||||
install_done = "Kurulum tamamlandı! Devam etmek için İleri'ye basın."
|
||||
install_current_file = "İşlemdeki dosya"
|
||||
install_current_file = "Kopyalanan dosya"
|
||||
retry_install = "Yeniden Dene"
|
||||
finish_next = "Tamamlandı →"
|
||||
|
||||
@@ -117,6 +119,18 @@ disk_too_small = "Disk çok küçük"
|
||||
summary_partition_plan = "Bölümleme"
|
||||
summary_custom_partitions = "Manuel Bölümler"
|
||||
mp_size_remaining = "Kalan Alan"
|
||||
mp_size_available = "Kullanılabilir"
|
||||
mp_active_disk = "Aktif disk"
|
||||
mp_active_disk_hint = "Tabloda ve Yeni Bölüm işleminde kullanılan disk. Başka bir diske bölüm eklemek için önce üstteki disk şeridinden o diske geçin. GRUB aygıtı kök bölümün olduğu disk olarak ayarlanır."
|
||||
mp_will_delete = "silinecek"
|
||||
mp_restore = "Geri Al"
|
||||
mp_free_space = "Boş Alan"
|
||||
mp_duplicate_mountpoint = "Aynı bağlama noktası iki bölüme birden verilmiş; her bölümün farklı bir bağlama noktası olmalı"
|
||||
mp_multiple_root = "Birden fazla bölüm kök (/) olarak tanımlanmış: {disks}. Kök tek bir bölüm olmalı"
|
||||
mp_partition_no_disk = "Bir bölüm tanımı için disk seçilmemiş. Her bölümün yerleşeceği diski seçin."
|
||||
mp_disk_not_found = "{disk} artık bağlı değil. Bu diske yapılan bölüm tanımını kaldırın ya da diski yeniden bağlayın."
|
||||
mp_bar_now = "Mevcut Durum"
|
||||
mp_bar_after = "Kurulum Sonrası"
|
||||
|
||||
# ── UI / Genel ────────────────────────────────────────────
|
||||
installer = "PisiLinux Kurulum Aracı"
|
||||
@@ -137,7 +151,6 @@ check_warn_notice = "Bazı önerilen gereksinimler karşılanmıyor. Kurulum d
|
||||
check_all_pass = "Tüm sistem gereksinimleri karşılandı."
|
||||
recheck = "🔄 Yeniden Kontrol Et"
|
||||
rescue_button = "🆘 Kurtarma Kipi"
|
||||
rescue_button_tooltip = "PiSi Linux'u Kurtarma Modunda Yeniden Kur"
|
||||
|
||||
# ── Users (ek) ────────────────────────────────────────────
|
||||
pwd_very_weak = "Çok zayıf"
|
||||
@@ -188,12 +201,13 @@ mp_device = "Aygıt"
|
||||
mp_size = "Boyut"
|
||||
mp_fstype = "Dosya Sistemi"
|
||||
mp_mountpoint = "Bağlama Noktası"
|
||||
mp_format_status = "Biçim"
|
||||
mp_action = "İşlem"
|
||||
mp_add = "Bölüm Ekle"
|
||||
mp_delete = "Sil"
|
||||
mp_apply = "Değişiklikleri Uygula"
|
||||
mp_no_efi = "⚠️ EFI bölümü atanmadı. UEFI önyüklemesi için gerekli."
|
||||
mp_no_root = "⚠️ Kök (/) bölümü atanmadı."
|
||||
mp_no_efi = "EFI bölümü atanmadı. UEFI önyüklemesi için gerekli."
|
||||
mp_no_root = "Kök (/) bölümü atanmadı."
|
||||
mp_confirm_delete = "%{dev} bölümü silinsin mi? Bu işlem geri alınamaz."
|
||||
mp_plan_title = "Değişiklik Özeti"
|
||||
mp_fs_ext4 = "ext4"
|
||||
@@ -201,9 +215,18 @@ mp_fs_btrfs = "btrfs"
|
||||
mp_fs_xfs = "xfs"
|
||||
mp_fs_fat32 = "FAT32 (EFI)"
|
||||
mp_fs_swap = "takas (swap)"
|
||||
mp_format_partition = "Biçimlendir (mevcut veri silinir)"
|
||||
mp_size_gb = "%{n} GB"
|
||||
mp_size_mb = "%{n} MB"
|
||||
|
||||
# ── Tablo Sütun Başlıkları ────────────────────────────────────
|
||||
column_device = "Aygıt"
|
||||
column_mount_point = "Bağlama Noktası"
|
||||
column_label = "Etiket"
|
||||
column_type = "Tür"
|
||||
column_format = "Biçim"
|
||||
column_size = "Boyut"
|
||||
|
||||
|
||||
check_ram = "RAM"
|
||||
check_ram_fail = "%{mb} MB RAM mevcut. Kurulum için minimum 1 GB gerekli."
|
||||
@@ -237,16 +260,18 @@ job_mount_custom = "Manuel bölümler bağlanıyor"
|
||||
# ── Ek Yerelleştirme / i18n ──────────────────────────────────
|
||||
mp_delete_confirm_title = "Bölümü Sil"
|
||||
mp_error_empty_mountpoint = "Bağlama noktası boş olamaz."
|
||||
mp_plan_valid = "✓ Bölüm planı geçerli. İleri tuşuna basabilirsiniz."
|
||||
mp_mountpoint_optional = "Bağlama noktası isteğe bağlı. Boş bırakırsanız bölüm biçimlendirilir ama sistem başlangıcında bağlanmaz; Windows'tan erişmek için kullanılır."
|
||||
mp_error_empty_mountpoint_named = "Bağlama noktası boş olamaz: {device}. Listeden bir bağlama noktası seçin ya da bölümü \"Sil\" işaretleyip geçin."
|
||||
mp_plan_valid = "Bölüm planı geçerli. İleri tuşuna basabilirsiniz."
|
||||
mp_select_disk_first = "← Önce bir disk seçin."
|
||||
mp_add_dialog_hint = "/ veya /home veya swap"
|
||||
mp_select_hint = "Seç…"
|
||||
mp_add_btn = "✓ Ekle"
|
||||
mp_add_btn = "Ekle"
|
||||
mp_size_hint = "Boyut (MB, 0=kalan):"
|
||||
mp_delete_confirm_yes = "✓ Evet, Sil"
|
||||
mp_delete_confirm_yes = "Evet, Sil"
|
||||
mp_new_partition = "Yeni Bölüm"
|
||||
mp_edit_partition = "Düzenle"
|
||||
mp_save = "✓ Kaydet"
|
||||
mp_save = "Kaydet"
|
||||
plan_root_suffix = " (kök)"
|
||||
disk_min_required = " ↳ %{err} (min. %{n} GB gerekli)"
|
||||
|
||||
@@ -266,6 +291,10 @@ summary_software = "Yazılım"
|
||||
summary_display_manager = "Görüntü Yöneticisi"
|
||||
summary_autologin = "Otomatik oturum açma"
|
||||
summary_autologin_user = "Otomatik oturum açılacak kullanıcı"
|
||||
summary_autologin_on = "Açık"
|
||||
summary_autologin_off = "Kapalı (parola ile giriş)"
|
||||
summary_bootloader_password = "parola korumalı"
|
||||
summary_desktop_from_image = "ISO görüntüsünden gelir; kurulumda otomatik tespit edilir"
|
||||
summary_desktop = "Masaüstü"
|
||||
|
||||
# ── Görüntü Yöneticisi ───────────────────────────────────────
|
||||
@@ -274,7 +303,7 @@ display_manager_title = "Görüntü Yöneticisi Yapılandırması"
|
||||
display_manager_description = "Görüntü yöneticinizi ve masaüstü ortamınızı seçin."
|
||||
display_manager_select = "Görüntü yöneticisi:"
|
||||
display_manager_de = "Masaüstü ortamı:"
|
||||
display_manager_autologin = "Otomatik oturum açma"
|
||||
display_manager_autologin = "Otomatik oturum aç"
|
||||
display_manager_autologin_user = "Otomatik oturum açılacak kullanıcı:"
|
||||
display_manager_autologin_hint = "(ilk kullanıcı)"
|
||||
display_manager_de_not_found = "(algılanamadı)"
|
||||
@@ -289,7 +318,7 @@ job_ldconfig = "Paylaşımlı kütüphaneler yapılandırılıyor (ldc
|
||||
job_update_env = "Sistem ortamı güncelleniyor"
|
||||
job_cleanup_live = "Canlı ortam izleri temizleniyor"
|
||||
job_datetime = "Tarih & saat yapılandırılıyor"
|
||||
job_display_manager = "Görüntü yöneticisi yapılandırılıyor"
|
||||
job_autologin = "Otomatik oturum açma uygulanıyor"
|
||||
job_netinstall = "Ek paketler kuruluyor"
|
||||
job_setup_lvm = "LVM yapılandırılıyor"
|
||||
|
||||
@@ -311,6 +340,7 @@ admin_password_short = "Root parolası en az 8 karakter olmalıdır."
|
||||
datetime = "Tarih & Saat"
|
||||
datetime_title = "Tarih & Saat Ayarları"
|
||||
datetime_description = "Sistem tarihini ve saatini yapılandırın."
|
||||
datetime_current = "Şu anki zaman:"
|
||||
datetime_ntp_label = "Ağ Zamanı (NTP):"
|
||||
datetime_ntp_on = "Etkin — saat otomatik eşitlenir"
|
||||
datetime_ntp_off = "Devre dışı — elle ayarlanır"
|
||||
@@ -357,6 +387,8 @@ bootloader_title = "Önyükleyici Yapılandırması"
|
||||
bootloader_description = "Önyükleyici (GRUB) ayarlarını yapılandırın."
|
||||
bootloader_device_label = "Aygıta kur:"
|
||||
bootloader_device_hint = "(varsayılan: hedef disk)"
|
||||
bootloader_device_hint_dualboot = "Çift önyükleme yapacaksanız burada Windows'un kurulu olduğu diski seçin. UEFI modunda bu aygıtın bölümündeki Windows ESP'sinden \"Windows Boot Manager\" menüye eklenir; seçmezseniz açılışta Windows görünmez."
|
||||
bootloader_device_why_existing = "Bu aygıt, üzerinde hâlihazırda işletim sistemi bulunduğu için seçildi: önyükleyici açılışta zaten okunan diske yazılmalıdır."
|
||||
bootloader_timeout_label = "Bekleme süresi:"
|
||||
bootloader_timeout_suffix = "saniye"
|
||||
bootloader_password_label = "GRUB parolası (isteğe bağlı):"
|
||||
@@ -422,6 +454,7 @@ lvm_lv_add = "Mantıksal Birim Ekle"
|
||||
lvm_lv_error_vg = "En az bir Birim Grubu tanımlanmalıdır."
|
||||
lvm_lv_error_size = "Girilen boyut, Birim Grubu kapasitesini aşıyor."
|
||||
lvm_lv_mountpoint = "Bağlama Noktası"
|
||||
lvm_no_pv_on_disk = "Bu diskte LVM PV yok. Önce bu diske PV bölümü ekleyin."
|
||||
lvm_lv_fstype = "Dosya Sistemi"
|
||||
lvm_lv_size = "Boyut (MB, 0=kalan):"
|
||||
lvm_lv_action = "İşlem"
|
||||
|
||||
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,243 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ Yali-RS — Slide Content (English) ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
# Slide transition duration (seconds)
|
||||
interval_secs = 6
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisilinux-logo.png"
|
||||
title = "Welcome to Pisi GNU/Linux"
|
||||
description = "Pisi is a Turkish-supported GNU/Linux distribution that combines speed, stability, and ease of use."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisi-software-center.png"
|
||||
title = "PISI Package Manager"
|
||||
description = "Access thousands of packages with a single command. Install anything you need easily with `pisi install <package>`."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/community-101.png"
|
||||
title = "Community Support"
|
||||
description = "Our active community at forum.pisilinux.org is ready to answer your questions. Our wiki pages are always accessible."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisi_speed.png"
|
||||
title = "Speed & Performance"
|
||||
description = "Thanks to an optimized kernel and startup services, PisiLinux boots in seconds and uses minimal resources."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/securety.png"
|
||||
title = "Security First"
|
||||
description = "Keep your system secure with regular security updates, AppArmor profiles, and secure default settings."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/customization-dark.png"
|
||||
title = "Customization"
|
||||
description = "Tailor everything to your taste — from the desktop environment and theme colors to panel layout and keyboard shortcuts."
|
||||
|
||||
# ── Budgie ────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie.png"
|
||||
title = "PisiLinux Budgie"
|
||||
description = "A modern, clean, and elegant Budgie desktop. Built on the GNOME stack and user-friendly."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_applets.png"
|
||||
title = "Budgie Desktop Applets"
|
||||
description = "Easily add tools like weather, system monitor, and calendar via Budgie's panel applets."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_raven.png"
|
||||
title = "Raven Notification Center"
|
||||
description = "Access volume, media controls, notifications, and quick settings from the Raven panel."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_gtk_theme.png"
|
||||
title = "GTK Theme Support"
|
||||
description = "Budgie runs GTK themes and GNOME applications seamlessly, ensuring visual consistency."
|
||||
|
||||
# ── Cinnamon ──────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon.png"
|
||||
title = "PisiLinux Cinnamon"
|
||||
description = "Cinnamon combines the traditional desktop experience with modern features, offering a powerful and flexible environment."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_panel.png"
|
||||
title = "Cinnamon Panel"
|
||||
description = "Customize your panel with applets, launchers, and system indicators for a classic yet powerful desktop."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_settings.png"
|
||||
title = "Settings Manager"
|
||||
description = "Fine-tune every aspect of your Cinnamon desktop through the comprehensive Settings Manager."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_desklets.png"
|
||||
title = "Cinnamon Desklets"
|
||||
description = "Keep notes, clocks, and system info always visible on your desktop with Cinnamon Desklets."
|
||||
|
||||
# ── GNOME ─────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome.png"
|
||||
title = "PisiLinux GNOME"
|
||||
description = "Reduce distractions and focus on your work with the clean and focused GNOME desktop environment."
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome_search.png"
|
||||
title = "GNOME Search"
|
||||
description = "Press the Super key and start typing. Instantly reach applications, files, and settings."
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome_extensions.png"
|
||||
title = "GNOME Extensions"
|
||||
description = "Shape your desktop to your needs with hundreds of extensions from extensions.gnome.org."
|
||||
|
||||
# ── KDE ───────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/kde.png"
|
||||
title = "PisiLinux KDE Plasma"
|
||||
description = "Meet the modern, customizable, and powerful KDE Plasma desktop. Everything is at your fingertips."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Dolphin File Manager"
|
||||
description = "Dolphin makes managing your files easy with tabbed browsing, dual-panel view, and powerful search."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/kde_connect.png"
|
||||
title = "KDE Connect"
|
||||
description = "Connect your phone to your computer. Notifications, file transfer, and clipboard sharing — all with KDE Connect."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/discover.png"
|
||||
title = "Discover Software Center"
|
||||
description = "Easily find, install, and update applications from the visual interface with Discover."
|
||||
|
||||
# ── LXQt ──────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/lxqt.png"
|
||||
title = "PisiLinux LXQt"
|
||||
description = "A lightweight, fast, and modern LXQt desktop environment. Low resource consumption, high performance."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/lxqt_panel.png"
|
||||
title = "LXQt Panel"
|
||||
description = "The traditional panel layout puts the taskbar, application launcher, and system tray always within reach."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "PCManFM-Qt File Manager"
|
||||
description = "Manage files effortlessly with the lightweight and fast PCManFM-Qt. Tabbed navigation and a customizable interface."
|
||||
|
||||
# ── LXDE ──────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/lxde.png"
|
||||
title = "PisiLinux LXDE"
|
||||
description = "The legendary lightweight LXDE desktop. Known for extremely low resource usage and a reliable build."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/lxde_openbox.png"
|
||||
title = "Openbox-Based"
|
||||
description = "LXDE uses the Openbox window manager. Fine-tune it for a smooth and responsive experience."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "PCManFM File Manager"
|
||||
description = "PCManFM speeds up your file operations with its fast and lightweight build, supporting tree view and tabs."
|
||||
|
||||
# ── Lumina ────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/lumina.png"
|
||||
title = "PisiLinux Lumina"
|
||||
description = "A lightweight and independent Lumina desktop. Qt-based, designed to run cleanly on BSD and Linux systems."
|
||||
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/lumina_applications.png"
|
||||
title = "Application Support"
|
||||
description = "Lumina works well with core utilities and many popular Qt apps, providing a practical everyday experience."
|
||||
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/lumina_theme.png"
|
||||
title = "Theme Customization"
|
||||
description = "Easily customize color schemes and interface styles with Lumina's flexible theme system."
|
||||
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/lumina_panel.png"
|
||||
title = "Lumina Panel"
|
||||
description = "A clean panel with a taskbar, application launcher, and system tray keeps your desktop organized."
|
||||
|
||||
# ── MATE ──────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/mate.png"
|
||||
title = "PisiLinux MATE"
|
||||
description = "MATE for those who love the traditional desktop layout. Developed with GTK+ 3 for a modern and smooth experience."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Caja File Manager"
|
||||
description = "Manage your files easily with MATE's powerful Caja. Dual panel, tabbed navigation, and rich features."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/mate_settings.png"
|
||||
title = "MATE Settings"
|
||||
description = "Customize your MATE desktop with comprehensive settings for appearance, display, keyboard, and more."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/mate_applications.png"
|
||||
title = "Rich Application Suite"
|
||||
description = "MATE comes with a rich set of classic applications — from text editors to image viewers, all well-integrated."
|
||||
|
||||
# ── XFCE ──────────────────────────────────────────────────────────
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce.png"
|
||||
title = "PisiLinux XFCE"
|
||||
description = "A lightweight, fast, and user-friendly XFCE desktop environment. Low resource consumption, high performance."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce_panel.png"
|
||||
title = "XFCE Panel"
|
||||
description = "The traditional panel layout puts the taskbar, application menu, and system tray always within reach."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Thunar File Manager"
|
||||
description = "Manage your files easily with the fast and lightweight Thunar. Tree view, tabbed navigation, and customizable features."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce_settings.png"
|
||||
title = "Whisker Menu"
|
||||
description = "A modern alternative to the classic application menu. Find apps easily and organize your favorites with Whisker Menu."
|
||||
@@ -0,0 +1,240 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ Yali-RS — Slide Content (English) ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
# Slide transition duration (seconds)
|
||||
interval_secs = 12
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisilinux-logo.png"
|
||||
title = "Welcome to Pisi GNU/Linux"
|
||||
description = "Pisi GNU/Linux is a Linux distribution developed by the free software community, based on PiSi, Turkey's first native package management system. It focuses on easy installation, easy use, and meeting daily desktop requirements for end users."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisi-software-center.png"
|
||||
title = "Native Software Components"
|
||||
description = "Pisi GNU/Linux, equipped with native software components, stands out with original technologies such as YALI installation software, KAPTAN settings wizard, MÜDÜR for fast system startup, and ÇOMAR for hardware settings."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/community-102.png"
|
||||
title = "Extensive Software Repository"
|
||||
description = "Pisi GNU/Linux offers thousands of applications that users need through its extensive software repository and keeps them regularly updated."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisi_speed.png"
|
||||
title = "Modern Desktop Experience"
|
||||
description = "The free and open-source Pisi GNU/Linux comes with the KDE Plasma desktop environment by default, offering modern and comprehensive features. For users who prefer a desktop with lower system resource consumption, the lightweight, fast, and customizable XFCE desktop environment is also offered as an alternative."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/securety.png"
|
||||
title = "Versatile Usage"
|
||||
description = "Pisi GNU/Linux is a versatile operating system that can be easily used at home, office, and daily use. It provides a powerful infrastructure for music, movies, 3D modeling, graphic design, and professional applications, while also offering a comprehensive gaming platform for gaming enthusiasts."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/customization-dark.png"
|
||||
title = "Customization"
|
||||
description = "Customize everything from the desktop environment to theme colors, from panel layout to keyboard shortcuts according to your taste."
|
||||
|
||||
# ── XFCE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce.png"
|
||||
title = "PisiLinux XFCE"
|
||||
description = "Lightweight, fast and user-friendly XFCE desktop environment. Low resource consumption, high performance."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce_panel.png"
|
||||
title = "XFCE Panel"
|
||||
description = "With the traditional panel layout, taskbar, application menu and system tray are always at your fingertips."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Thunar File Manager"
|
||||
description = "Easily perform file operations with the fast and lightweight Thunar. Offers tree view, tabbed navigation and customizable features."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce_settings.png"
|
||||
title = "Settings Center"
|
||||
description = "A modern alternative to the classic application menu. Find applications easily and organize your favorites with Whisker Menu."
|
||||
|
||||
# ── KDE Plasma ────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/kde.png"
|
||||
title = "PisiLinux KDE Plasma"
|
||||
description = "Meet the modern, customizable and powerful KDE Plasma desktop environment. Everything is at your fingertips."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Dolphin File Manager"
|
||||
description = "Dolphin makes managing your files easy with tabbed browsing, dual panel and powerful search features."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/kde_connect.png"
|
||||
title = "KDE Connect"
|
||||
description = "Connect your phone to your computer. Notifications, file transfer and clipboard sharing are possible with KDE Connect."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/discover.png"
|
||||
title = "Discover Software Center"
|
||||
description = "Easily find, install and update applications from the visual interface with Discover."
|
||||
|
||||
# ── GNOME ─────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome.png"
|
||||
title = "PisiLinux GNOME"
|
||||
description = "Reduce distractions and focus on your work with the clean and focused GNOME desktop environment."
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome_search.png"
|
||||
title = "GNOME Search"
|
||||
description = "Press the Super key and start typing. Instantly reach applications, files and settings."
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome_extensions.png"
|
||||
title = "GNOME Extensions"
|
||||
description = "Shape your desktop to your needs with hundreds of extensions from extensions.gnome.org."
|
||||
|
||||
# ── MATE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/mate.png"
|
||||
title = "PisiLinux MATE"
|
||||
description = "MATE for those who love the traditional desktop layout. Developed with GTK+ 3 for a modern and smooth experience."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Caja File Manager"
|
||||
description = "Easily perform file operations with MATE's powerful Caja. Offers dual panel, tabbed navigation and rich features."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/mate_settings.png"
|
||||
title = "MATE Settings"
|
||||
description = "Customize your MATE desktop with comprehensive settings for appearance, display, keyboard and more."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/customization.png"
|
||||
title = "Rich Application Suite"
|
||||
description = "MATE offers a wide range of well-integrated classic applications from text editor to image viewer."
|
||||
|
||||
# ── Budgie ────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie.png"
|
||||
title = "PisiLinux Budgie"
|
||||
description = "Modern, clean and stylish Budgie desktop. Built on GNOME infrastructure, user-friendly."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_applets.png"
|
||||
title = "Budgie Desktop Applets"
|
||||
description = "Easily add tools like weather, system monitor and calendar via Budgie's panel applets."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_raven.png"
|
||||
title = "Raven Notification Center"
|
||||
description = "Access volume level, media controls, notifications and quick settings from the Raven panel."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_gtk_theme.png"
|
||||
title = "GTK Theme Support"
|
||||
description = "Budgie runs GTK themes and GNOME applications seamlessly, ensuring visual consistency."
|
||||
|
||||
# ── Cinnamon ──────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon.png"
|
||||
title = "PisiLinux Cinnamon"
|
||||
description = "Cinnamon combines the traditional desktop experience with modern features, offering a powerful and flexible environment."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_panel.png"
|
||||
title = "Cinnamon Panel"
|
||||
description = "Customize your panel with applets, launchers and system indicators; experience a classic but powerful desktop."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_settings.png"
|
||||
title = "Settings Manager"
|
||||
description = "Fine-tune every aspect of your Cinnamon desktop through the comprehensive Settings Manager."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_desklets.png"
|
||||
title = "Cinnamon Desklets"
|
||||
description = "Keep notes, clocks and system info always visible on your desktop with Cinnamon Desklets."
|
||||
|
||||
# ── LXQt ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/lxqt.png"
|
||||
title = "PisiLinux LXQt"
|
||||
description = "Lightweight, fast and modern LXQt desktop environment. Low resource consumption, high performance."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "LXQt Panel"
|
||||
description = "The traditional panel layout puts the taskbar, application launcher and system tray always within reach."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "PCManFM-Qt File Manager"
|
||||
description = "Manage file operations easily with the lightweight and fast PCManFM-Qt. Offers tabbed navigation and customizable interface."
|
||||
|
||||
# ── LXDE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/lxde.png"
|
||||
title = "PisiLinux LXDE"
|
||||
description = "The legendary lightweight LXDE desktop. Known for very low resource consumption and reliable structure."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/lxde.png"
|
||||
title = "Openbox-Based"
|
||||
description = "LXDE uses the Openbox window manager. Offers a smooth experience with fine-tuning."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "PCManFM File Manager"
|
||||
description = "PCManFM facilitates your file operations with its fast and lightweight structure, supports tree view and tabs."
|
||||
|
||||
# ── Lumina ────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/lumina.png"
|
||||
title = "PisiLinux Lumina"
|
||||
description = "Lightweight and independent Lumina desktop. Qt-based, minimal environment that works properly on BSD and Linux systems."
|
||||
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/customization.png"
|
||||
title = "Theme Customization"
|
||||
description = "Easily customize color schemes and interface style with Lumina's theme system."
|
||||
@@ -0,0 +1,240 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ Yali-RS — Slayt İçerikleri (Türkçe) ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
# Slayt geçiş süresi (saniye)
|
||||
interval_secs = 12
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisilinux-logo.png"
|
||||
title = "Pisi GNU/Linux'a Hoş Geldiniz"
|
||||
description = "Pisi GNU/Linux; Türkiye'nin ilk yerli paket yönetim sistemi olan, PiSi tabanlı, özgür yazılım topluluğu tarafından geliştirilen, kolay kurulum, kolay kullanım ve günlük masaüstü gereksinimlerini karşılamaya odaklanan, son kullanıcıya yönelik bir Linux dağıtımıdır."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisi-software-center.png"
|
||||
title = "Yerli Yazılım Bileşenleri"
|
||||
description = "Yerli yazılım bileşenleriyle donatılmış olan Pisi GNU/Linux; kurulum yazılımı YALI, ayar sihirbazı KAPTAN, sistemin hızlı açılmasını sağlayan MÜDÜR ve donanım ayarları için kullanılan ÇOMAR gibi özgün teknolojileriyle öne çıkan yerli bir GNU/Linux dağıtımıdır."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/community-102.png"
|
||||
title = "Geniş Yazılım Deposu"
|
||||
description = "Pisi GNU/Linux, geniş yazılım deposu sayesinde kullanıcıların ihtiyaç duyduğu binlerce uygulamayı sunar ve bunları düzenli olarak güncel tutar."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/pisi_speed.png"
|
||||
title = "Modern Masaüstü Deneyimi"
|
||||
description = "Özgür ve açık kaynaklı Pisi GNU/Linux, varsayılan olarak modern ve kapsamlı özellikler sunan KDE Plasma masaüstü ortamıyla gelir. Daha düşük sistem kaynakları tüketen bir masaüstü tercih eden kullanıcılar için ise hafif, hızlı ve özelleştirilebilir XFCE masaüstü ortamı da alternatif olarak sunulmaktadır."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/securety.png"
|
||||
title = "Çok Yönlü Kullanım"
|
||||
description = "Pisi GNU/Linux; evde, ofiste ve günlük kullanımda rahatlıkla kullanılabilen çok yönlü bir işletim sistemidir. Müzik, film, 3D modelleme, grafik tasarım ve profesyonel uygulamalar için güçlü bir altyapı sunarken, oyun tutkunları için de kapsamlı bir oyun platformu sağlar."
|
||||
|
||||
[[items]]
|
||||
icon = "slides/img/customization-dark.png"
|
||||
title = "Özelleştirme"
|
||||
description = "Masaüstü ortamından tema renklerine, panel düzeninden klavye kısayollarına kadar her şeyi zevkinize göre ayarlayın."
|
||||
|
||||
# ── XFCE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce.png"
|
||||
title = "PisiLinux XFCE"
|
||||
description = "Hafif, hızlı ve kullanıcı dostu XFCE masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce_panel.png"
|
||||
title = "XFCE Panel"
|
||||
description = "Geleneksel panel düzeni ile görev çubuğu, uygulama menüsü ve sistem tepsisi her zaman elinizin altında."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Thunar Dosya Yöneticisi"
|
||||
description = "Hızlı ve hafif Thunar ile dosya işlemlerinizi kolayca yapın. Ağaç görünümü, sekmeli gezinme ve özelleştirilebilir özellikler sunar."
|
||||
|
||||
[[items]]
|
||||
desktop = "xfce"
|
||||
icon = "slides/img/xfce_settings.png"
|
||||
title = "Ayarlar Merkezi"
|
||||
description = "Klasik uygulama menüsüne modern bir alternatif. Whisker Menu ile uygulamaları kolayca bulun, favorilerinizi düzenleyin."
|
||||
|
||||
# ── KDE Plasma ────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/kde.png"
|
||||
title = "PisiLinux KDE Plasma"
|
||||
description = "Modern, özelleştirilebilir ve güçlü KDE Plasma masaüstü ortamıyla tanışın. Her şey elinizin altında."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Dolphin Dosya Yöneticisi"
|
||||
description = "Sekmeli gezinme, ikili panel ve güçlü arama özelliğiyle Dolphin dosyalarınızı yönetmeyi kolaylaştırır."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/kde_connect.png"
|
||||
title = "KDE Bağlan"
|
||||
description = "Telefonunuzu bilgisayarınıza bağlayın. Bildirimler, dosya aktarımı ve klipboard paylaşımı KDE Connect ile mümkün."
|
||||
|
||||
[[items]]
|
||||
desktop = "kde"
|
||||
icon = "slides/img/discover.png"
|
||||
title = "Discover Yazılım Merkezi"
|
||||
description = "Discover ile uygulamaları görsel arayüzden kolayca bulun, kurun ve güncelleyin."
|
||||
|
||||
# ── GNOME ─────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome.png"
|
||||
title = "PisiLinux GNOME"
|
||||
description = "Sade ve odaklı GNOME masaüstü ortamı ile dikkat dağıtıcı unsurları azaltın, işinize konsantre olun."
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome_search.png"
|
||||
title = "GNOME Arama"
|
||||
description = "Super tuşuna basıp yazmaya başlayın. Uygulama, dosya ve ayarlara anında ulaşın."
|
||||
|
||||
[[items]]
|
||||
desktop = "gnome"
|
||||
icon = "slides/img/gnome_extensions.png"
|
||||
title = "GNOME Eklentileri"
|
||||
description = "extensions.gnome.org adresinden yüzlerce eklentiyle masaüstünüzü ihtiyaçlarınıza göre şekillendirin."
|
||||
|
||||
# ── MATE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/mate.png"
|
||||
title = "PisiLinux MATE"
|
||||
description = "Geleneksel masaüstü düzenini sevenler için MATE. GTK+ 3 ile geliştirilmiş, modern ve akıcı bir deneyim sunar."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "Caja Dosya Yöneticisi"
|
||||
description = "MATE'in güçlü dosya yöneticisi Caja ile dosya işlemlerinizi kolayca yapın. Çift panel, sekmeli gezinme ve geniş özellikler sunar."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/mate_settings.png"
|
||||
title = "MATE Ayarları"
|
||||
description = "Görünüm, ekran, klavye ve daha fazlası için kapsamlı ayarlarla MATE masaüstünüzü özelleştirin."
|
||||
|
||||
[[items]]
|
||||
desktop = "mate"
|
||||
icon = "slides/img/customization.png"
|
||||
title = "Zengin Uygulama Paketi"
|
||||
description = "MATE; metin editöründen görüntü görüntüleme yazılımına kadar geniş, iyi entegre edilmiş klasik uygulamalar sunar."
|
||||
|
||||
# ── Budgie ────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie.png"
|
||||
title = "PisiLinux Budgie"
|
||||
description = "Modern, sade ve şık Budgie masaüstü. GNOME altyapısı üzerine inşa edilmiş, kullanıcı dostudur."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_applets.png"
|
||||
title = "Budgie Desktop Applets"
|
||||
description = "Budgie'nin panel appletleri sayesinde hava durumu, sistem izleyici, takvim gibi araçları kolayca ekleyin."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_raven.png"
|
||||
title = "Raven Bildirim Merkezi"
|
||||
description = "Raven panelinden ses seviyesi, medya kontrolleri, bildirimler ve hızlı ayarlara erişin."
|
||||
|
||||
[[items]]
|
||||
desktop = "budgie"
|
||||
icon = "slides/img/budgie_gtk_theme.png"
|
||||
title = "GTK Tema Desteği"
|
||||
description = "Budgie, GTK temalarını ve GNOME uygulamalarını sorunsuz çalıştırır, görsel bütünlük sağlar."
|
||||
|
||||
# ── Cinnamon ──────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon.png"
|
||||
title = "PisiLinux Cinnamon"
|
||||
description = "Geleneksel masaüstü deneyimini modern özelliklerle birleştiren Cinnamon, güçlü ve esnek bir ortam sunar."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_panel.png"
|
||||
title = "Cinnamon Paneli"
|
||||
description = "Panelinizi applet'ler, başlatıcılar ve sistem göstergeleriyle özelleştirin; klasik ama güçlü bir masaüstü deneyimi yaşayın."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_settings.png"
|
||||
title = "Ayarlar Yöneticisi"
|
||||
description = "Cinnamon'un kapsamlı Ayarlar Yöneticisi aracılığıyla masaüstünüzün her yönünü özelleştirin."
|
||||
|
||||
[[items]]
|
||||
desktop = "cinnamon"
|
||||
icon = "slides/img/cinnamon_desklets.png"
|
||||
title = "Cinnamon Desklets"
|
||||
description = "Masaüstü widget'ları ile notlar, saatler ve sistem bilgileri her zaman görünür kalsın."
|
||||
|
||||
# ── LXQt ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/lxqt.png"
|
||||
title = "PisiLinux LXQt"
|
||||
description = "Hafif, hızlı ve modern LXQt masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "LXQt Panel"
|
||||
description = "Geleneksel panel düzeni ile görev çubuğu, uygulama başlatıcı ve sistem tepsisi her zaman elinizin altında."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxqt"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "PCManFM-Qt Dosya Yöneticisi"
|
||||
description = "Hafif ve hızlı PCManFM-Qt ile dosya işlemlerini kolayca yönetin. Sekmeli gezinme ve özelleştirilebilir arayüz sunar."
|
||||
|
||||
# ── LXDE ──────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/lxde.png"
|
||||
title = "PisiLinux LXDE"
|
||||
description = "Efsanevi hafif masaüstü LXDE. Çok düşük kaynak tüketimi ve güvenilir yapısıyla tanınır."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/lxde.png"
|
||||
title = "Openbox Tabanlı"
|
||||
description = "LXDE, Openbox pencere yöneticisini kullanır. İnce ayarlar ile akıcı bir deneyim sunar."
|
||||
|
||||
[[items]]
|
||||
desktop = "lxde"
|
||||
icon = "slides/img/file_manager.png"
|
||||
title = "PCManFM Dosya Yöneticisi"
|
||||
description = "PCManFM, hızlı ve hafif yapısıyla dosya işlemlerinizi kolaylaştırır, ağaç görünümü ve sekme destekler."
|
||||
|
||||
# ── Lumina ────────────────────────────────────────────────────────
|
||||
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/lumina.png"
|
||||
title = "PisiLinux Lumina"
|
||||
description = "Hafif ve bağımsız Lumina masa üstü. Qt tabanlı, BSD ve Linux sistemlerde düzgün çalışan minimal bir ortam."
|
||||
|
||||
[[items]]
|
||||
desktop = "lumina"
|
||||
icon = "slides/img/customization.png"
|
||||
title = "Tema Özelleştirme"
|
||||
description = "Lumina'nın tema sistemiyle renk şemaları ve arayüz stilini kolayca özelleştirin."
|
||||
@@ -63,7 +63,7 @@ pub fn all_passed(results: &[CheckResult]) -> bool {
|
||||
fn check_ram() -> CheckResult {
|
||||
let mut sys = System::new();
|
||||
sys.refresh_memory();
|
||||
let mb = sys.total_memory() / 1024 / 1024;
|
||||
let mb = sys.total_memory() / 1_048_576;
|
||||
|
||||
if mb < 1024 {
|
||||
CheckResult::fail(
|
||||
|
||||
@@ -33,6 +33,53 @@ pub struct UserConfig {
|
||||
pub password: String,
|
||||
#[serde(default = "default_hostname")]
|
||||
pub hostname: String,
|
||||
/// Kök hesabın parolası. Boş bırakılırsa root kilitlenir
|
||||
/// (`CreateUserJob` → `passwd -l root`).
|
||||
#[serde(default)]
|
||||
pub root_password: String,
|
||||
}
|
||||
|
||||
/// `[system]` bölümü — isteğe bağlı davranış ayarları.
|
||||
///
|
||||
/// Tümü `#[serde(default)]`: answer dosyası yalnızca zorunlu alanları
|
||||
/// içerebilir. Bu bölümü tanımlamak zorunlu değil.
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct SystemConfig {
|
||||
/// Otomatik oturum açma.
|
||||
///
|
||||
/// Varsayılan KAPALI. Gözetimsiz kurulumda açık olması, kurulmuş
|
||||
/// sistemde herhangi birinin fiziksel olarak konsola oturup
|
||||
/// kullanıcı adını görmesini kolaylaştırır.
|
||||
#[serde(default)]
|
||||
pub autologin: bool,
|
||||
/// GRUB bekleme süresi (saniye).
|
||||
#[serde(default = "default_grub_timeout")]
|
||||
pub bootloader_timeout: u32,
|
||||
/// GRUB parolası (düz metin; hedefte PBKDF2 ile hashlenir).
|
||||
#[serde(default)]
|
||||
pub bootloader_password: String,
|
||||
/// Ek çekirdek parametreleri.
|
||||
#[serde(default)]
|
||||
pub kernel_options: String,
|
||||
}
|
||||
|
||||
// DİKKAT: burada `#[derive(Default)]` KULLANILMAZ.
|
||||
//
|
||||
// `[system]` bloğu answer dosyasında HİÇ YOKSA serde, AnswerFile'daki
|
||||
// `#[serde(default)]` sayesinde bu tipin Default'ını çağırır. Alan
|
||||
// bazlı `#[serde(default = "...")]` anotasyonları yalnızca blok
|
||||
// KATILDIĞINDA, ilgili alan eksik olduğunda devreye girer. Yani
|
||||
// türetilmiş Default, bootloader_timeout'ı 0 yapardı → kurulumda GRUB
|
||||
// menüsü hiç görünmez. Elle uyguluyoruz.
|
||||
impl Default for SystemConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
autologin: false,
|
||||
bootloader_timeout: default_grub_timeout(),
|
||||
bootloader_password: String::new(),
|
||||
kernel_options: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Answer file'ın tamamı (`--auto-install <dosya.toml>`).
|
||||
@@ -40,6 +87,9 @@ pub struct UserConfig {
|
||||
pub struct AnswerFile {
|
||||
pub install: InstallConfig,
|
||||
pub user: UserConfig,
|
||||
/// İsteğe bağlı. Verilmezse tüm ayarlar güvenli varsayılanlarda kalır.
|
||||
#[serde(default)]
|
||||
pub system: SystemConfig,
|
||||
}
|
||||
|
||||
fn default_timezone() -> String { "Europe/Istanbul".to_string() }
|
||||
@@ -48,6 +98,7 @@ fn default_keyboard() -> String { "tr".to_string() }
|
||||
fn default_source() -> String { "/run/livecd/squashfs-root/".to_string() }
|
||||
fn default_mount() -> String { "/mnt".to_string() }
|
||||
fn default_hostname() -> String { "pisilinux".to_string() }
|
||||
fn default_grub_timeout() -> u32 { 5 }
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// ANSWER FILE: YÜKLE ve UYGULA
|
||||
@@ -105,11 +156,28 @@ pub fn apply_to_state(af: &AnswerFile, state: &mut crate::installer::GlobalState
|
||||
state.username = af.user.username.clone();
|
||||
state.password = af.user.password.clone();
|
||||
state.password_confirm = af.user.password.clone();
|
||||
state.root_password = af.user.root_password.clone();
|
||||
state.root_password_confirm = af.user.root_password.clone();
|
||||
state.hostname = af.user.hostname.clone();
|
||||
state.selected_disk = Some(af.install.disk.clone());
|
||||
state.erase_disk = true;
|
||||
state.language = if af.install.locale.starts_with("tr") { "tr" } else { "en" }.to_string();
|
||||
|
||||
// [system] — isteğe bağlı davranış ayarları
|
||||
state.autologin = af.system.autologin;
|
||||
state.bootloader_timeout = af.system.bootloader_timeout;
|
||||
state.bootloader_password = af.system.bootloader_password.clone();
|
||||
state.kernel_options = af.system.kernel_options.clone();
|
||||
// GRUB aygıtı: bölümleme yapılmadan önce bilinmez, kuruluma
|
||||
// InstallGrubJob içinde atanır.
|
||||
state.bootloader_device = String::new();
|
||||
|
||||
// Masaüstü ortamı ve ekran yöneticisi answer file'dan GELMEZ.
|
||||
// Sistem squashfs'ten kopyalandığı için bunlar ISO'nun kendisidir;
|
||||
// GlobalState::default() zaten canlı ortamı tespit ediyor
|
||||
// (detect_desktop_environment). Burada sabit bir değer yazmak,
|
||||
// XFCE ISO'sunda Plasma oturumu açmaya çalışırdı.
|
||||
|
||||
// Disk boyutunu lsblk ile sorgula
|
||||
let disk_info: Option<DiskInfo> = (|| {
|
||||
let out = std::process::Command::new("lsblk")
|
||||
@@ -137,12 +205,12 @@ pub fn apply_to_state(af: &AnswerFile, state: &mut crate::installer::GlobalState
|
||||
if let Some(info) = disk_info {
|
||||
let is_uefi = std::path::Path::new("/sys/firmware/efi").exists();
|
||||
let efi_mb = if is_uefi { 512u64 } else { 0 };
|
||||
let total_mb = info.size_bytes / 1024 / 1024;
|
||||
let total_mb = info.size_bytes / 1_048_576;
|
||||
|
||||
// Swap: RAM * 1.0, min 512 MB, max 2048 MB, ayrıca diskin %10'unu geçmesin
|
||||
let mut sys = sysinfo::System::new();
|
||||
sys.refresh_memory();
|
||||
let ram_mb = sys.total_memory() / 1024 / 1024;
|
||||
let ram_mb = sys.total_memory() / 1_048_576;
|
||||
let disk_10pct = total_mb / 10;
|
||||
let swap_mb = ram_mb.clamp(512, 2048).min(disk_10pct);
|
||||
|
||||
@@ -165,3 +233,99 @@ pub fn apply_to_state(af: &AnswerFile, state: &mut crate::installer::GlobalState
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const MINIMAL: &str = r#"
|
||||
[install]
|
||||
disk = "/dev/sda"
|
||||
|
||||
[user]
|
||||
username = "pisi"
|
||||
password = "gizli123"
|
||||
hostname = "pisi-pc"
|
||||
"#;
|
||||
|
||||
const WITH_SYSTEM: &str = r#"
|
||||
[install]
|
||||
disk = "/dev/nvme0n1"
|
||||
|
||||
[user]
|
||||
username = "pisi"
|
||||
password = "gizli123"
|
||||
root_password = "rootgizli"
|
||||
|
||||
[system]
|
||||
autologin = true
|
||||
bootloader_timeout = 10
|
||||
bootloader_password = "grubparola"
|
||||
kernel_options = "quiet splash"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn minimal_answer_file_parses() {
|
||||
// [system] tamamen isteğe bağlı — yoksa ayrıştırma patlamamalı.
|
||||
let af = toml::from_str::<AnswerFile>(MINIMAL).expect("ayrıştırılmalı");
|
||||
assert_eq!(af.install.disk, "/dev/sda");
|
||||
assert_eq!(af.user.username, "pisi");
|
||||
assert_eq!(af.user.root_password, "", "varsayılan boş olmalı");
|
||||
assert!(!af.system.autologin, "autologin güvenli varsayılanı kapalı");
|
||||
assert_eq!(af.system.bootloader_timeout, 5);
|
||||
assert!(af.system.bootloader_password.is_empty());
|
||||
assert!(af.system.kernel_options.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_section_is_honoured() {
|
||||
let af = toml::from_str::<AnswerFile>(WITH_SYSTEM).expect("ayrıştırılmalı");
|
||||
assert_eq!(af.install.disk, "/dev/nvme0n1");
|
||||
assert_eq!(af.user.root_password, "rootgizli");
|
||||
assert!(af.system.autologin, "autologin açık olmalı");
|
||||
assert_eq!(af.system.bootloader_timeout, 10);
|
||||
assert_eq!(af.system.bootloader_password, "grubparola");
|
||||
assert_eq!(af.system.kernel_options, "quiet splash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_file_on_disk_is_valid() {
|
||||
// Depodaki örnek dosya gerçekten ayrıştırılabiliyor mu?
|
||||
let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("answer-example.toml");
|
||||
let content = std::fs::read_to_string(&p)
|
||||
.expect("answer-example.toml okunmalı");
|
||||
toml::from_str::<AnswerFile>(&content)
|
||||
.expect("answer-example.toml ayrıştırılabilmeli");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bootloader_timeout_default_survives_missing_system_block() {
|
||||
// Regresyon: SystemConfig türetilmiş Default'a sahipti ve 0
|
||||
// veriyordu. `[system]` bloğu dosyada HİÇ YOKSA serde alan
|
||||
// bazlı `#[serde(default = "...")]` anotasyonlarına UĞRAMAZ;
|
||||
// doğrudan Default::default() çağırır. Sonuç: GRUB bekleme
|
||||
// süresi 0 saniye → kurulumda bootloader menüsü hiç görünmez.
|
||||
let af = toml::from_str::<AnswerFile>(MINIMAL).unwrap();
|
||||
assert_eq!(
|
||||
af.system.bootloader_timeout, 5,
|
||||
"[system] yokken de varsayılan geçerli olmalı"
|
||||
);
|
||||
assert_eq!(SystemConfig::default().bootloader_timeout, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_impl_matches_serde_field_defaults() {
|
||||
// İki yol da aynı sonucu vermeli.
|
||||
let d = SystemConfig::default();
|
||||
assert!(!d.autologin);
|
||||
assert!(d.bootloader_password.is_empty());
|
||||
assert!(d.kernel_options.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_required_section_is_rejected() {
|
||||
// [user] olmadan kabul edilmemeli.
|
||||
let bad = "[install]\ndisk = \"/dev/sda\"\n";
|
||||
assert!(toml::from_str::<AnswerFile>(bad).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,11 +134,11 @@ pub struct OemConfig {
|
||||
|
||||
// ── Varsayılan değerler ───────────────────────────────────────────────────────
|
||||
|
||||
fn default_title() -> String { "YALI Installation Tool".to_string() }
|
||||
fn default_title() -> String { "PisiLinux Install".to_string() }
|
||||
fn default_subtitle() -> String { "PiSi GNU/LINUX".to_string() }
|
||||
fn default_yali_logo() -> String { "assets/icons/YALI_icon_LIGHT.svg".to_string() }
|
||||
fn default_pisi_logo_dark() -> String { "assets/pisi-logo-dark.png".to_string() }
|
||||
fn default_pisi_logo_light() -> String { "assets/pisi-logo-light.png".to_string() }
|
||||
fn default_yali_logo() -> String { "assets/icons/YALI_icon_DARK.png".to_string() }
|
||||
fn default_pisi_logo_dark() -> String { "assets/icons/YALI_icon_DARK.png".to_string() }
|
||||
fn default_pisi_logo_light() -> String { "assets/icons/YALI_icon_DARK.png".to_string() }
|
||||
fn default_interval() -> u64 { 6 }
|
||||
fn default_primary() -> String { "#1a73e8".to_string() }
|
||||
fn default_accent() -> String { "#34a853".to_string() }
|
||||
@@ -161,34 +161,87 @@ impl BrandingConfig {
|
||||
"/usr/share/yali/branding.toml",
|
||||
"branding.toml",
|
||||
];
|
||||
let mut config = BrandingConfig::default();
|
||||
for path in &paths {
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
match toml::from_str(&content) {
|
||||
Ok(cfg) => return cfg,
|
||||
Ok(cfg) => { config = cfg; break; }
|
||||
Err(e) => eprintln!("[branding] {}: {}", path, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
BrandingConfig::default()
|
||||
|
||||
// slides/slides.toml varsa onu da yükle (öncelikli)
|
||||
if let Some(content) = std::fs::read_to_string("slides/slides.toml").ok() {
|
||||
if let Ok(slides) = toml::from_str::<SlidesConfig>(&content) {
|
||||
if !slides.icons.is_empty() {
|
||||
config.slides.icons = slides.icons;
|
||||
}
|
||||
if !slides.items.is_empty() {
|
||||
config.slides.items = slides.items;
|
||||
}
|
||||
if slides.interval_secs > 0 {
|
||||
config.slides.interval_secs = slides.interval_secs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Belirtilen dile ait `branding_{lang}.toml` dosyasını arar ve yükler.
|
||||
/// Bulunamazsa varsayılan `branding.toml` dosyasına geri döner.
|
||||
/// Yükleme sonrası `slides/slides_{lang}.toml` varsa slides verilerini da buradan alır.
|
||||
pub fn load_for_lang(lang: &str) -> Self {
|
||||
let lang_paths = [
|
||||
format!("/etc/yali/branding_{}.toml", lang),
|
||||
format!("/usr/share/yali/branding_{}.toml", lang),
|
||||
format!("branding_{}.toml", lang),
|
||||
let mut config = {
|
||||
let lang_paths = [
|
||||
format!("/etc/yali/branding_{}.toml", lang),
|
||||
format!("/usr/share/yali/branding_{}.toml", lang),
|
||||
format!("branding_{}.toml", lang),
|
||||
];
|
||||
let mut found = None;
|
||||
for path in &lang_paths {
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
match toml::from_str(&content) {
|
||||
Ok(cfg) => { found = Some(cfg); break; }
|
||||
Err(e) => eprintln!("[branding] {}: {}", path, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
found.unwrap_or_else(Self::load)
|
||||
};
|
||||
|
||||
// Harici slides dosyasını dene (slides/slides_{lang}.toml)
|
||||
if let Some(slides) = Self::load_external_slides(lang) {
|
||||
if !slides.icons.is_empty() {
|
||||
config.slides.icons = slides.icons;
|
||||
}
|
||||
if !slides.items.is_empty() {
|
||||
config.slides.items = slides.items;
|
||||
}
|
||||
if slides.interval_secs > 0 {
|
||||
config.slides.interval_secs = slides.interval_secs;
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// `slides/slides_{lang}.toml` veya `slides/slides.toml` dosyasını yükler.
|
||||
fn load_external_slides(lang: &str) -> Option<SlidesConfig> {
|
||||
let paths = [
|
||||
format!("slides/slides_{}.toml", lang),
|
||||
"slides/slides.toml".to_string(),
|
||||
];
|
||||
for path in &lang_paths {
|
||||
for path in &paths {
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
match toml::from_str(&content) {
|
||||
Ok(cfg) => return cfg,
|
||||
Err(e) => eprintln!("[branding] {}: {}", path, e),
|
||||
Ok(cfg) => return Some(cfg),
|
||||
Err(e) => eprintln!("[slides] {}: {}", path, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::load()
|
||||
None
|
||||
}
|
||||
|
||||
/// İkon adına göre dosya yolunu döner.
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
use eframe::egui;
|
||||
|
||||
/// Uygulamanın desteklediği diller (`locales/` altındaki dosyalarla eşleşir).
|
||||
///
|
||||
/// `locales/` klasörüne yeni bir dosya eklendiğinde buraya da eklenmelidir.
|
||||
pub const AVAILABLE_LANGUAGES: &[&str] = &["tr", "en"];
|
||||
|
||||
/// Yedek dil: desteklenmeyen bir kod gelirse kullanılır.
|
||||
pub const FALLBACK_LANGUAGE: &str = "en";
|
||||
|
||||
/// `rust_i18n::set_locale` çağrısından ÖNCE kullanılır.
|
||||
///
|
||||
/// Neden: `set_locale("de")` hata vermez, sessizce kabul eder ve `t!()`
|
||||
/// karşılığı olmayan her anahtar için anahtarın KENDİSİNİ döndürür —
|
||||
/// arayüz "location_title", "next", "partition" gibi çıplak metinlere
|
||||
/// dolar. O yüzden karar burada verilir: dil desteklenmiyorsa hiç
|
||||
/// değiştirilmez, kullanıcının seçimi korunur.
|
||||
pub fn sanitize_locale(locale: &str) -> &str {
|
||||
if AVAILABLE_LANGUAGES.contains(&locale) {
|
||||
locale
|
||||
} else {
|
||||
FALLBACK_LANGUAGE
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! draw_icon {
|
||||
($ui:expr, $path:literal, $size:expr, $color:expr) => {
|
||||
@@ -25,7 +48,7 @@ pub fn form_input_text(
|
||||
ui.spacing_mut().button_padding = eframe::egui::vec2(10.0, 10.0);
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgb(255, 255, 255))
|
||||
.stroke(egui::Stroke::new(1.0, egui::Color32::from_gray(200)))
|
||||
.stroke(egui::Stroke::new(1.0_f32, egui::Color32::from_gray(200)))
|
||||
.rounding(4.0)
|
||||
.inner_margin(eframe::egui::Margin::symmetric(2.0, 2.0))
|
||||
.show(ui, |ui| {
|
||||
@@ -45,23 +68,22 @@ pub fn form_input_password(
|
||||
show_password: &mut bool,
|
||||
) -> eframe::egui::Response {
|
||||
let width_percent = 0.90;
|
||||
let height = 32.0;
|
||||
let height = 18.0;
|
||||
let toplam_genislik = ui.available_width() * width_percent;
|
||||
|
||||
let buton_genislik = 32.0;
|
||||
let buton_genislik = 18.0;
|
||||
let alan_genislik = toplam_genislik - buton_genislik - 4.0;
|
||||
|
||||
let mut response: Option<eframe::egui::Response> = None;
|
||||
|
||||
ui.visuals_mut().override_text_color = Some(eframe::egui::Color32::from_rgb(30, 30, 30));
|
||||
|
||||
// SVG içeriklerini string olarak tanımlıyoruz
|
||||
let goz_acik_svg = r#"<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>"#;
|
||||
let goz_kapali_svg = r#"<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" /></svg>"#;
|
||||
// Remixicon karakterlerini kullanıyoruz
|
||||
let goz_ikonu = if *show_password { "\u{ECB5}" } else { "\u{ECB7}" };
|
||||
|
||||
eframe::egui::Frame::none()
|
||||
.fill(eframe::egui::Color32::WHITE)
|
||||
.stroke(eframe::egui::Stroke::new(1.0, eframe::egui::Color32::from_rgb(200, 200, 200)))
|
||||
.stroke(eframe::egui::Stroke::new(1.0_f32, eframe::egui::Color32::from_rgb(200, 200, 200)))
|
||||
.rounding(6.0)
|
||||
.inner_margin(eframe::egui::Margin::symmetric(2.0, 2.0))
|
||||
.show(ui, |ui| {
|
||||
@@ -71,27 +93,18 @@ pub fn form_input_password(
|
||||
eframe::egui::vec2(alan_genislik, height),
|
||||
eframe::egui::TextEdit::singleline(text)
|
||||
.password(!*show_password)
|
||||
.frame(false)
|
||||
.frame(false)
|
||||
);
|
||||
response = Some(r);
|
||||
|
||||
// Duruma göre ilgili SVG string'ini seçip byte dizisine (&[u8]) çeviriyoruz
|
||||
let (uri, svg_bytes) = if *show_password {
|
||||
("bytes://goz_acik.svg", goz_acik_svg.as_bytes())
|
||||
} else {
|
||||
("bytes://goz_kapali.svg", goz_kapali_svg.as_bytes())
|
||||
};
|
||||
|
||||
// Image::from_bytes kullanarak imajı güvenli bir şekilde yüklüyoruz
|
||||
let svg_gosterici = eframe::egui::Image::from_bytes(uri, svg_bytes)
|
||||
.tint(eframe::egui::Color32::from_rgb(120, 120, 120))
|
||||
.max_width(18.0)
|
||||
.max_height(18.0);
|
||||
|
||||
// 2. SVG İkonlu Buton
|
||||
let btn = eframe::egui::Button::image(svg_gosterici)
|
||||
.frame(false)
|
||||
.min_size(eframe::egui::vec2(buton_genislik, height));
|
||||
// 2. Remixicon İkonlu Buton
|
||||
let btn = eframe::egui::Button::new(
|
||||
eframe::egui::RichText::new(goz_ikonu)
|
||||
.size(14.0)
|
||||
.color(eframe::egui::Color32::from_rgb(120, 120, 120))
|
||||
)
|
||||
.frame(false)
|
||||
.min_size(eframe::egui::vec2(buton_genislik, height));
|
||||
|
||||
if ui.add(btn).on_hover_cursor(eframe::egui::CursorIcon::PointingHand).clicked() {
|
||||
*show_password = !*show_password;
|
||||
@@ -160,4 +173,36 @@ pub fn language_selector_ui(ui: &mut eframe::egui::Ui, state: &mut crate::instal
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn supported_locales_pass_through() {
|
||||
assert_eq!(sanitize_locale("tr"), "tr");
|
||||
assert_eq!(sanitize_locale("en"), "en");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_locales_fall_back() {
|
||||
// Bu diller için locale dosyası YOK. set_locage'e sızsaydı
|
||||
// arayüz çıplak anahtarlarla dolardı.
|
||||
for code in ["de", "fr", "it", "es", "ru", "nl", "pl", "xx", ""] {
|
||||
assert_eq!(
|
||||
sanitize_locale(code),
|
||||
FALLBACK_LANGUAGE,
|
||||
"{} desteklenmiyor, yedeğe düşmeli", code
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_language_is_itself_supported() {
|
||||
// Yedeğin de desteklenmemesi durumunda sonsuz hata durumu oluşur.
|
||||
assert!(
|
||||
AVAILABLE_LANGUAGES.contains(&FALLBACK_LANGUAGE),
|
||||
"yedek dil {} desteklenen listede olmalı", FALLBACK_LANGUAGE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +59,34 @@ pub fn part_path(disk: &str, num: u32) -> String {
|
||||
}
|
||||
|
||||
|
||||
/// Bir adımın sihirbaz içindeki rolü.
|
||||
///
|
||||
/// Rol, adımın LİSTEDEKİ KONUMUNDAN değil kendisinden bildirilir. Daha
|
||||
/// önce `main.rs` rolü `cur == n_steps - 2` gibi konumsal olarak
|
||||
/// tahmin ediyordu; sihirbaza yeni bir adım eklemek (ör. LicenseStep)
|
||||
/// bu tespiti sessizce kaydırıp "Geri" butonunu veya bitiş ekranını
|
||||
/// yanlış adıma bağlıyordu.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum StepRole {
|
||||
/// Sıradan bir yapılandırma adımı.
|
||||
Normal,
|
||||
/// Kurulumun çalıştığı ekran. "İleri" gizlenir.
|
||||
Execution,
|
||||
/// Kurulum sonrası ekran. Yalnızca yeniden başlatma/çıkış sunar.
|
||||
Finish,
|
||||
}
|
||||
|
||||
/// Calamares'in "ViewStep" yapısına benzer bir Trait.
|
||||
pub trait InstallerStep {
|
||||
fn name(&self) -> String;
|
||||
fn on_enter(&mut self, _state: &mut GlobalState) {}
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState);
|
||||
fn is_complete(&self, state: &GlobalState) -> bool;
|
||||
|
||||
/// Adımın rolü. Varsayılan `Normal`.
|
||||
fn role(&self) -> StepRole {
|
||||
StepRole::Normal
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
@@ -167,11 +189,86 @@ pub struct LogicalVolume {
|
||||
pub mountpoint: String,
|
||||
}
|
||||
|
||||
/// Cihaz yolundan ait olduğu diskin yolunu çıkarır.
|
||||
///
|
||||
/// - `/dev/sda1` → `/dev/sda`
|
||||
/// - `/dev/nvme0n1p2` → `/dev/nvme0n1`
|
||||
/// - `/dev/mmcblk0p1` → `/dev/mmcblk0`
|
||||
#[allow(dead_code)] // genel yardımcı; testlerde ve gelecekteki işlerde kullanılır
|
||||
pub fn device_to_disk(device: &str) -> String {
|
||||
let base = device.trim_start_matches("/dev/");
|
||||
// NVMe/MMC/loop aygıtlarında bölüm ayracı "p" + rakamdır
|
||||
let needs_p = base.contains("nvme") || base.contains("mmcblk") || base.contains("loop");
|
||||
if let Some(pos) = base.rfind('p') {
|
||||
if needs_p && base[pos + 1..].chars().all(|c| c.is_ascii_digit()) && !base[pos + 1..].is_empty() {
|
||||
return format!("/dev/{}", &base[..pos]);
|
||||
}
|
||||
}
|
||||
// SATA/virtio: sondaki tüm rakamları at
|
||||
let trimmed = base.trim_end_matches(|c: char| c.is_ascii_digit());
|
||||
format!("/dev/{}", trimmed)
|
||||
}
|
||||
|
||||
/// Cihaz adından bölüm numarasını çıkarır ("/dev/sda3" → 3,
|
||||
/// "/dev/nvme0n1p2" → 2). Sonda rakam yoksa None döner.
|
||||
pub fn part_number(device: &str) -> Option<u32> {
|
||||
let base = device.trim_start_matches("/dev/");
|
||||
let digits: String = base.chars().rev().take_while(|c| c.is_ascii_digit()).collect();
|
||||
digits.chars().rev().collect::<String>().parse().ok()
|
||||
}
|
||||
|
||||
/// Bölüm tanımlarına parted'ın gerçek slot davranışını izleyerek
|
||||
/// aygıt numarası atar.
|
||||
///
|
||||
/// Eski Yali modeliyle aynı mantık: fiziksel (silinmemiş) bölümler
|
||||
/// kendi numaralarını korur; silinmek işaretlenenlerin slotları boşalır
|
||||
/// ve yeni tanımlar en küçük boş slotu sırayla doldurur. Örn. sda2
|
||||
/// silinip yeni bölüm eklenirse yeni bölüm sda2 olur; sda3 yerinde kalır.
|
||||
pub fn assign_partition_numbers(parts: &mut [CustomPartition]) {
|
||||
use std::collections::BTreeSet;
|
||||
// Disk bazlı kullanılan slot kümeleri
|
||||
let mut used: std::collections::HashMap<String, BTreeSet<u32>> = std::collections::HashMap::new();
|
||||
for p in parts.iter() {
|
||||
if p.delete { continue; } // silinecekler slot açar
|
||||
if !p.orig_device.is_empty() { // fiziksel: kendi numarası
|
||||
if let Some(n) = part_number(&p.orig_device) {
|
||||
used.entry(p.disk.clone()).or_default().insert(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in parts.iter_mut() {
|
||||
if p.delete {
|
||||
// Silinecek bölüm: kurulumda yok olacak; adı olduğu gibi kalır,
|
||||
// yeni slot tüketmez.
|
||||
continue;
|
||||
}
|
||||
match part_number(&p.orig_device) {
|
||||
Some(n) => { p.device = part_path(&p.disk, n); }
|
||||
None => {
|
||||
let set = used.entry(p.disk.clone()).or_default();
|
||||
// en küçük boş slot
|
||||
let mut n = 1u32;
|
||||
while set.contains(&n) { n += 1; }
|
||||
set.insert(n);
|
||||
p.device = part_path(&p.disk, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kullanıcının manuel olarak tanımladığı tek bir bölüm.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CustomPartition {
|
||||
/// Bölümün ait olduğu disk (ör. "/dev/sda").
|
||||
/// Çoklu disk kurulumunda bölümler diske göre gruplanır.
|
||||
pub disk: String,
|
||||
/// Bölüm aygit adı; oluşturulacak (ör. "/dev/sda1")
|
||||
pub device: String,
|
||||
/// Taramada bulunan fiziksel cihaz adı (örn. "/dev/sda3").
|
||||
/// Kullanıcı tarafından yeni eklenen tanımlarda boştur.
|
||||
/// Silme işleminde satırın listede kalıp yeniden
|
||||
/// tanımlanabilmesini sağlar.
|
||||
pub orig_device: String,
|
||||
/// Boyut MB cinsinden; 0 = "kalan alan"
|
||||
pub size_mb: u64,
|
||||
/// Dosya sistemi
|
||||
@@ -184,6 +281,12 @@ pub struct CustomPartition {
|
||||
pub luks_password: String,
|
||||
/// LUKS harici adı (ör. "cryptroot")
|
||||
pub luks_name: String,
|
||||
/// false = biçimlendir, true = olduğu gibi bırak (mevcut bölümleri koru)
|
||||
pub skip_format: bool,
|
||||
/// true = kurulum sırasında bu bölüm diskten SİLİNECEK
|
||||
/// (eski Yali'deki OperationDestroyDevice karşılığı).
|
||||
/// Satır listede görünür kalır; istenirse işaret geri alınır.
|
||||
pub delete: bool,
|
||||
}
|
||||
|
||||
/// Kurulum boyunca toplanan tüm veriler.
|
||||
@@ -213,6 +316,9 @@ pub struct GlobalState {
|
||||
pub demo_mode: bool,
|
||||
pub is_dark: bool,
|
||||
pub rescue_mode: bool,
|
||||
/// GPL lisans metnini kullanıcı okudu ve kabul etti.
|
||||
/// Demo modunda zorunlu değildir.
|
||||
pub license_accepted: bool,
|
||||
pub bootloader_device: String,
|
||||
pub bootloader_timeout: u32,
|
||||
pub bootloader_password: String,
|
||||
@@ -241,17 +347,12 @@ pub struct GlobalState {
|
||||
/// true olmadan kurulum ilerleyemez.
|
||||
pub oem_password_changed: bool,
|
||||
pub selected_package_groups: Vec<String>,
|
||||
/// branding.toml [slides.icons] tablosundan yüklenen ikon → dosya URI eşlemesi.
|
||||
/// Slideshow bu map üzerinden çalışma zamanında görsel yollarını çözümler.
|
||||
pub branding_icons: std::collections::HashMap<String, String>,
|
||||
/// Sidebar için koyu tema logo URI'si.
|
||||
pub branding_yali_dark: String,
|
||||
pub branding_pisi_dark: String,
|
||||
/// Sidebar için açık tema logo URI'si.
|
||||
pub branding_yali_light: String,
|
||||
pub branding_pisi_light: String,
|
||||
/// branding.toml dosyasından yüklenen slaytlar.
|
||||
pub branding_slides: Vec<crate::branding::SlideItem>,
|
||||
}
|
||||
|
||||
impl Default for GlobalState {
|
||||
@@ -281,6 +382,7 @@ impl Default for GlobalState {
|
||||
demo_mode: false,
|
||||
is_dark: true,
|
||||
rescue_mode: false,
|
||||
license_accepted: false,
|
||||
bootloader_device: String::new(),
|
||||
bootloader_timeout: 5,
|
||||
bootloader_password: String::new(),
|
||||
@@ -305,12 +407,10 @@ impl Default for GlobalState {
|
||||
display_manager: "sddm".to_string(),
|
||||
autologin: false,
|
||||
desktop_environment: detect_desktop_environment(),
|
||||
branding_icons: std::collections::HashMap::new(),
|
||||
branding_pisi_dark: String::new(),
|
||||
branding_pisi_light: String::new(),
|
||||
branding_yali_dark: String::new(),
|
||||
branding_yali_light: String::new(),
|
||||
branding_slides: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,6 +425,9 @@ pub struct WelcomeStep {
|
||||
checks: Vec<CheckResult>,
|
||||
checked: bool,
|
||||
last_language: String,
|
||||
/// Kurtarma modunun kullanılabilir olup olmadığı. `on_enter`'da bir kez
|
||||
/// hesaplanır — `blkid` her karede çalıştırılırsa UI kilitlenir.
|
||||
rescue_available: bool,
|
||||
}
|
||||
|
||||
impl Default for WelcomeStep {
|
||||
@@ -333,6 +436,7 @@ impl Default for WelcomeStep {
|
||||
checks: Vec::new(),
|
||||
checked: false,
|
||||
last_language: String::new(),
|
||||
rescue_available: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,8 +446,9 @@ impl InstallerStep for WelcomeStep {
|
||||
|
||||
fn on_enter(&mut self, state: &mut GlobalState) {
|
||||
if !self.checked || self.last_language != state.language {
|
||||
rust_i18n::set_locale(&state.language);
|
||||
rust_i18n::set_locale(crate::funct::sanitize_locale(&state.language));
|
||||
self.checks = run_all();
|
||||
self.rescue_available = detect_existing_linux();
|
||||
self.checked = true;
|
||||
self.last_language = state.language.clone();
|
||||
}
|
||||
@@ -352,9 +457,13 @@ impl InstallerStep for WelcomeStep {
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
use crate::ui::theme;
|
||||
|
||||
// on_enter ile aynı koşul, aynı etki: dil değiştiyse kontrolleri ve
|
||||
// kurtarma kullanılabilirliğini tazele. (show() içinde yalnızca bu
|
||||
// senkron blok var; artık disk taraması yapmıyor.)
|
||||
if self.last_language != state.language {
|
||||
rust_i18n::set_locale(&state.language);
|
||||
rust_i18n::set_locale(crate::funct::sanitize_locale(&state.language));
|
||||
self.checks = run_all();
|
||||
self.rescue_available = detect_existing_linux();
|
||||
self.last_language = state.language.clone();
|
||||
}
|
||||
|
||||
@@ -455,24 +564,24 @@ impl InstallerStep for WelcomeStep {
|
||||
for check in &self.checks {
|
||||
match check.status {
|
||||
CheckStatus::Pass => {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../assets/icons/check.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(theme::c_success())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EB7B}")
|
||||
.color(theme::c_success())
|
||||
.size(18.0) // SVG max_width/height yerine font boyutu
|
||||
);
|
||||
}
|
||||
CheckStatus::Warn => {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../assets/icons/warning-icon.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(theme::c_warning())
|
||||
);
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EA21}")
|
||||
.color(theme::c_warning())
|
||||
.size(18.0)
|
||||
);
|
||||
}
|
||||
CheckStatus::Fail => {
|
||||
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.label(
|
||||
egui::RichText::new("\u{F4C6}")
|
||||
.color(theme::c_error())
|
||||
.size(18.0)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -494,8 +603,12 @@ impl InstallerStep for WelcomeStep {
|
||||
.inner_margin(egui::Margin::symmetric(8.0, 4.0))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let rescue_available = detect_existing_linux();
|
||||
if rescue_available {
|
||||
// `detect_existing_linux()` bir `blkid -o list` süreci
|
||||
// başlatır — TÜM blok aygıtlarını tarar. Bunu çizim
|
||||
// (show) içinde çağırmak saniyede ~60 kez süreç
|
||||
// doğuruyor, yani kare hızında tam disk taraması.
|
||||
// Sonuç `on_enter`'da bir kez hesaplanıp saklanıyor.
|
||||
if self.rescue_available {
|
||||
if theme::light_gray_btn(ui, &t!("rescue_button")).clicked() {
|
||||
state.rescue_mode = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
use super::{Job, UiSender};
|
||||
|
||||
/// Otomatik oturum açma ayarını hedef sisteme uygular.
|
||||
///
|
||||
/// Ekran yöneticisi (sddm/lightdm/gdm) yapılandırması ISO'da hazır olduğu
|
||||
/// için burada değiştirilmez; yalnızca kullanıcının seçtiği "otomatik oturum
|
||||
/// açma" bilgisi mevcut konfigürasyona eklenir.
|
||||
///
|
||||
/// ## Neden `/etc/sddm.conf.d/10-yali-autologin.conf`
|
||||
///
|
||||
/// ISO canlı kullanıcı için çalışan bir autologin config'i taşıyabilir.
|
||||
/// `/usr/lib/...` altına yazılan drop-in, `/etc/...` altındaki canlı
|
||||
/// config'e öncelik olarak YENİLİR — kutu tikliyken bile canlı config
|
||||
/// kazanırdı. `/etc` altındaki kanonik dosya deterministik olarak kazanır.
|
||||
///
|
||||
/// ## Kapalıyken neden boş `User=` yazılır
|
||||
///
|
||||
/// Kutu boşken hiçbir şeye dokunulmamak, canlı ISO'nun autologin
|
||||
/// config'ini olduğu gibi kopyalamak demekti: kurulu sistem canlı
|
||||
/// kullanıcıyla (parolasız!) otomatik açılıyordu. Bu yüzden kapalı
|
||||
/// durumda da kanonik dosya yazılır ama `User=` boş bırakılır — SDDM
|
||||
/// bunu "autologin yok" sayar ve canlı config'i ezer.
|
||||
pub struct ConfigureAutologinJob {
|
||||
pub mount: String,
|
||||
pub display_manager: String,
|
||||
pub autologin: bool,
|
||||
pub username: String,
|
||||
pub desktop_environment: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for ConfigureAutologinJob {
|
||||
fn name(&self) -> &str { "job_autologin" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
// Ekran yöneticisini SEÇMEK yerine TESPİT ediyoruz.
|
||||
//
|
||||
// YALI kurulu sistemi squashfs'ten kopyalar; ekran yöneticisi ISO'nun
|
||||
// kendisidir; kullanıcı seçmez. Tespit çalışma zamanında yapılır çünkü
|
||||
// `mount` dizini rsync'ten sonra tamdır (bu iş `CopyFilesJob`'dan
|
||||
// sonra çalışır).
|
||||
let dm = detect_display_manager(&self.mount).await;
|
||||
match &dm {
|
||||
Some(found) if found != &self.display_manager => {
|
||||
ui.log(format!(
|
||||
"Ekran yöneticisi tespit edildi: {} (varsayılan: {} kullanılmayacak)",
|
||||
found, self.display_manager
|
||||
));
|
||||
}
|
||||
Some(found) => {
|
||||
ui.log(format!("Ekran yöneticisi: {}", found));
|
||||
}
|
||||
None => {
|
||||
// Kapalıyken DM yoksa etkisizleştirilecek bir şey de yoktur.
|
||||
if !self.autologin {
|
||||
ui.log("Otomatik oturum açma kapalı, değişiklik yapılmıyor.");
|
||||
return Ok(());
|
||||
}
|
||||
return Err(
|
||||
"Hedef sistemde sddm/lightdm/gdm bulunamadı. \
|
||||
Otomatik oturum açma uygulanamadı; kurulumda seçilen \
|
||||
oturum yöneticisini elle yapılandırın."
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Oturum adı HEDEFTEN tespit edilir (aşağıya bak). Eskiden sabit
|
||||
// `plasmax11.desktop` yazılıyordu; hedefte bu dosya yoksa SDDM
|
||||
// oturumu bulamaz ve kutu tikli olmasına rağmen login ekranına
|
||||
// düşerdi — üstelik kutu boşken ISO'nun (doğru oturumlu) canlı
|
||||
// config'i çalıştığı için davranış tam TERS görünüyordu.
|
||||
let session = resolve_session(&self.mount, &self.desktop_environment).await;
|
||||
ui.log(format!("Oturum dosyası: {}", session));
|
||||
|
||||
match dm.as_deref() {
|
||||
Some("sddm") => {
|
||||
let conf = format!("{}/etc/sddm.conf.d/10-yali-autologin.conf", self.mount);
|
||||
if !self.autologin {
|
||||
// Canlı config'i ezen, autologin'i kapatan kanonik dosya.
|
||||
// ISO dosyaları SİLİNMEZ, yalnızca öncelikle ezilir.
|
||||
let content = "# Yali: otomatik oturum açma kapalı (canlı config'i ezer)\n\
|
||||
[Autologin]\nUser=\nSession=\n";
|
||||
if let Some(parent) = std::path::Path::new(&conf).parent() {
|
||||
let _ = tokio::fs::create_dir_all(parent).await;
|
||||
}
|
||||
tokio::fs::write(&conf, content)
|
||||
.await
|
||||
.map_err(|e| format!("{} yazılamadı: {}", conf, e))?;
|
||||
ui.log("Otomatik oturum açma kapalı, canlı config etkisizleştirildi.");
|
||||
return Ok(());
|
||||
}
|
||||
ui.log(format!(
|
||||
"Otomatik oturum açma uygulanıyor: {} ({})",
|
||||
self.username, self.username
|
||||
));
|
||||
if let Some(parent) = std::path::Path::new(&conf).parent() {
|
||||
tokio::fs::create_dir_all(parent).await
|
||||
.map_err(|e| format!("{} oluşturulamadı: {}", parent.display(), e))?;
|
||||
}
|
||||
let content = format!(
|
||||
"# Yali tarafından oluşturuldu\n[Autologin]\nUser={}\nSession={}\n",
|
||||
self.username, session
|
||||
);
|
||||
tokio::fs::write(&conf, content)
|
||||
.await
|
||||
.map_err(|e| format!("{} yazılamadı: {}", conf, e))?;
|
||||
}
|
||||
Some("lightdm") => {
|
||||
let dir = format!("{}/etc/lightdm", self.mount);
|
||||
let _ = tokio::fs::create_dir_all(&dir).await;
|
||||
let conf = format!("{}/lightdm.conf", dir);
|
||||
let mut content = tokio::fs::read_to_string(&conf)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
// Önce bayat autologin satırlarını temizle (canlı config
|
||||
// kalıntısı da, kendi önceki yazımımız da).
|
||||
content = content
|
||||
.lines()
|
||||
.filter(|l| {
|
||||
let t = l.trim_start();
|
||||
!(t.starts_with("autologin-user=")
|
||||
|| t.starts_with("autologin-session="))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if !content.ends_with('\n') {
|
||||
content.push('\n');
|
||||
}
|
||||
if self.autologin {
|
||||
if !content.contains("[Seat:*]") {
|
||||
content.push_str("[Seat:*]\n");
|
||||
}
|
||||
// lightdm oturumu .desktop dosyası adı olarak bekler.
|
||||
content.push_str(&format!("autologin-user={}\n", self.username));
|
||||
content.push_str(&format!("autologin-session={}\n", session));
|
||||
ui.log(format!("Otomatik oturum açma uygulanıyor: {}", self.username));
|
||||
} else {
|
||||
ui.log("Otomatik oturum açma kapalı, canlı config etkisizleştirildi.");
|
||||
}
|
||||
tokio::fs::write(&conf, content)
|
||||
.await
|
||||
.map_err(|e| format!("lightdm.conf yazılamadı: {}", e))?;
|
||||
}
|
||||
Some("gdm") => {
|
||||
if !self.autologin {
|
||||
ui.log("Otomatik oturum açma kapalı, değişiklik yapılmıyor.");
|
||||
return Ok(());
|
||||
}
|
||||
let dir = format!("{}/etc/gdm", self.mount);
|
||||
let _ = tokio::fs::create_dir_all(&dir).await;
|
||||
let conf = format!("{}/custom.conf", dir);
|
||||
let content = format!(
|
||||
"[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\nDefaultSession={}\n",
|
||||
self.username, session
|
||||
);
|
||||
tokio::fs::write(&conf, content)
|
||||
.await
|
||||
.map_err(|e| format!("gdm/custom.conf yazılamadı: {}", e))?;
|
||||
}
|
||||
other => {
|
||||
return Err(format!(
|
||||
"Desteklenmeyen ekran yöneticisi tespit edildi: {} — \
|
||||
otomatik oturum açma uygulanmadı",
|
||||
other.unwrap_or("bilinmiyor")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
ui.log("✓ Otomatik oturum açma yapılandırması tamamlandı");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Masaüstü ortamı adını, oturum yöneticilerinin beklediği `.desktop`
|
||||
/// dosyası adına eşler.
|
||||
///
|
||||
/// lightdm, sddm ve gdm oturumu `plasmax11.desktop` gibi bir dosya adıyla
|
||||
/// arar; ham DE adıyla ("plasma") yazmak oturumu bulamaz.
|
||||
fn session_desktop_file(de: &str) -> &'static str {
|
||||
match de {
|
||||
"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",
|
||||
// Bilinmeyen DE'de plasma varsayılır. Önemli olan, ham adı
|
||||
// yazıp oturumu bulamamaktır.
|
||||
_ => "plasmax11.desktop",
|
||||
}
|
||||
}
|
||||
|
||||
/// Hedef sistemde GERÇEKTEN var olan oturum dosyasını seçer (saf).
|
||||
///
|
||||
/// `wayland` ve `x11`, hedefteki `/usr/share/wayland-sessions` ve
|
||||
/// `/usr/share/xsessions` içerik listesidir. Önce DE anahtarını içeren
|
||||
/// dosya aranır (wayland öncelikli — ISO varsayılanı genelde odur),
|
||||
/// bulunamazsa alfabetik ilk dosya, liste boşsa sabit eşleme döner.
|
||||
///
|
||||
/// Sabit eşleme tek başına YETERSİZDİR: hedefte `plasmax11.desktop`
|
||||
/// yoksa (yalnızca wayland kurulu ISO) SDDM oturumu bulamaz ve kutu
|
||||
/// tikli olmasına rağmen login ekranına düşer.
|
||||
fn pick_session_file(de: &str, wayland: &[String], x11: &[String]) -> String {
|
||||
let key = match de {
|
||||
"plasma" => "plasma",
|
||||
"xfce" => "xfce",
|
||||
"gnome" => "gnome",
|
||||
"cinnamon" => "cinnamon",
|
||||
"budgie" => "budgie",
|
||||
"lxde" => "lxde",
|
||||
"lxqt" => "lxqt",
|
||||
"mate" => "mate",
|
||||
"lumina" => "lumina",
|
||||
_ => "plasma",
|
||||
};
|
||||
for list in [wayland, x11] {
|
||||
let mut sorted = list.to_vec();
|
||||
sorted.sort();
|
||||
for f in &sorted {
|
||||
if f.to_lowercase().contains(key) {
|
||||
return f.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut all: Vec<&String> = wayland.iter().chain(x11.iter()).collect();
|
||||
all.sort();
|
||||
if let Some(f) = all.first() {
|
||||
return (*f).clone();
|
||||
}
|
||||
session_desktop_file(de).to_string()
|
||||
}
|
||||
|
||||
/// Hedef `mount` altındaki oturum dizinlerini okuyup dosya adı seçer.
|
||||
async fn resolve_session(mount: &str, de: &str) -> String {
|
||||
async fn list(mount: &str, sub: &str) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let mut rd = match tokio::fs::read_dir(format!("{}/usr/share/{}", mount, sub)).await {
|
||||
Ok(rd) => rd,
|
||||
Err(_) => return out,
|
||||
};
|
||||
while let Ok(Some(e)) = rd.next_entry().await {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
if name.ends_with(".desktop") {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
let wayland = list(mount, "wayland-sessions").await;
|
||||
let x11 = list(mount, "xsessions").await;
|
||||
pick_session_file(de, &wayland, &x11)
|
||||
}
|
||||
|
||||
/// Hedef sistemde hangi ekran yöneticisinin KURULU olduğunu tespit eder.
|
||||
///
|
||||
/// YALI squashfs'ten kopyaladığı için ekran yöneticisi ISO'nun kendisidir;
|
||||
/// kullanıcı seçmez. Tespit çalışma zamanında yapılır çünkü `mount`
|
||||
/// dizini rsync'ten sonra tamdır (bu iş `CopyFilesJob`'dan sonra çalışır).
|
||||
///
|
||||
/// İki kaynak kontrol edilir: çalıştırılabilir (`/usr/bin/sddm`) ve
|
||||
/// yapılandırma dosyası. Çoğu dağıtımda ikisi de vardır ama paket
|
||||
/// yöneticisine göre değişebilir.
|
||||
async fn detect_display_manager(mount: &str) -> Option<String> {
|
||||
for (bin, name) in [
|
||||
("sddm", "sddm"),
|
||||
("lightdm", "lightdm"),
|
||||
("gdm", "gdm"),
|
||||
] {
|
||||
let exe = format!("{}/usr/bin/{}", mount, bin);
|
||||
if std::path::Path::new(&exe).exists() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
// Yedek: yapılandırma dizini
|
||||
for (dir, name) in [
|
||||
("etc/sddm.conf.d", "sddm"),
|
||||
("usr/lib/sddm", "sddm"),
|
||||
("etc/lightdm", "lightdm"),
|
||||
("etc/gdm", "gdm"),
|
||||
] {
|
||||
if std::path::Path::new(&format!("{}/{}", mount, dir)).is_dir() {
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_de_maps_to_a_desktop_file() {
|
||||
// lightdm/sddm/gdm oturumu .desktop dosyası arar; ham ad
|
||||
// yazılırsa oturum açılmaz.
|
||||
for de in [
|
||||
"plasma", "xfce", "gnome", "cinnamon", "budgie",
|
||||
"lxde", "lxqt", "mate", "lumina",
|
||||
] {
|
||||
let f = session_desktop_file(de);
|
||||
assert!(f.ends_with(".desktop"), "{} → {} uzantısız", de, f);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_de_falls_back_to_a_valid_desktop_file() {
|
||||
for de in ["", "kde5", "deepin", "TOTALLY-UNKNOWN"] {
|
||||
assert!(
|
||||
session_desktop_file(de).ends_with(".desktop"),
|
||||
"{} geçersiz dosya adı veriyor", de
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn s(v: &[&str]) -> Vec<String> {
|
||||
v.iter().map(|x| x.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_prefers_real_wayland_session() {
|
||||
// Yalnızca wayland kurulu ISO: sabit `plasmax11.desktop` yok.
|
||||
// Eskiden bu dosya adı körü körüne yazılıyor, SDDM oturumu
|
||||
// bulamayıp login ekranına düşüyordu.
|
||||
let got = pick_session_file("plasma", &s(&["plasma.desktop"]), &s(&[]));
|
||||
assert_eq!(got, "plasma.desktop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_matches_de_key_case_insensitively() {
|
||||
let got = pick_session_file(
|
||||
"xfce",
|
||||
&s(&[]),
|
||||
&s(&["xfce.desktop", "plasma.desktop"]),
|
||||
);
|
||||
assert_eq!(got, "xfce.desktop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_falls_back_to_first_file_then_static_map() {
|
||||
// DE'ye uyan yoksa alfabetik ilk dosya.
|
||||
let got = pick_session_file("mate", &s(&[]), &s(&["zebra.desktop", "aaa.desktop"]));
|
||||
assert_eq!(got, "aaa.desktop");
|
||||
// Hiç dosya yoksa sabit eşleme (son çare).
|
||||
let got = pick_session_file("plasma", &[], &[]);
|
||||
assert_eq!(got, "plasmax11.desktop");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use super::{blkid_uuid, DEMO_MODE, get_fs_type, is_mount_point, part_path, run_cmd, run_chroot, Job, UiSender};
|
||||
|
||||
/// Kök bölümün LUKS mapper adı. `PartitionJob`, `MountPartitionsJob`,
|
||||
/// `GenerateFstabJob` ve `InstallGrubJob` aynı adı kullanır; tek
|
||||
/// kaynaktan okunsun diye burada tanımlı.
|
||||
pub const CRYPTROOT_NAME: &str = "cryptroot";
|
||||
|
||||
/// Açık bir LUKS mapper'ının arkasındaki GERÇEK aygıtı döndürür
|
||||
/// (`/dev/sda3` gibi).
|
||||
///
|
||||
/// Neden gerekli: `blkid /dev/mapper/cryptroot` dosya sisteminin UUID'sini
|
||||
/// verir, LUKS başlık UUID'sini değil. Kernel'e `rd.luks.uuid=` ile
|
||||
/// verilmesi gereken değer LUKS başlığınkidir; bu yüzden önce mapper'ın
|
||||
/// altındaki bölüme inmek gerekir. `cryptsetup status` tam olarak bu
|
||||
/// bilgiyi verir ve çalışma zamanında doğrudur — bölüm numarası
|
||||
/// tahmin etmeye gerek kalmaz (manuel modda numaralar `is_uefi`'ye göre
|
||||
/// değil `assign_partition_numbers`'a göre belirlenir).
|
||||
async fn luks_backing_device(name: &str) -> Result<String, String> {
|
||||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||||
return Ok("/dev/demo-luks".to_string());
|
||||
}
|
||||
|
||||
let out = tokio::process::Command::new("cryptsetup")
|
||||
.args(["status", name])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("cryptsetup status {} çalıştırılamadı: {}", name, e))?;
|
||||
|
||||
// `cryptsetup status` stdout'a:
|
||||
// /dev/mapper/cryptroot is active and is in use.
|
||||
// type: LUKS2
|
||||
// device: /dev/sda3
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let device = stdout
|
||||
.lines()
|
||||
.find_map(|l| l.trim().strip_prefix("device:"))
|
||||
.map(|d| d.trim().to_string())
|
||||
.filter(|d| !d.is_empty());
|
||||
|
||||
device.ok_or_else(|| {
|
||||
format!(
|
||||
"{} mapper'ının altındaki aygıt çözümlenemedi. \
|
||||
LUKS başlığı açılmamış olabilir; fstab/crypttab kontrol edin.",
|
||||
name
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Bir bağlama noktasının arkasındaki GERÇEK aygıtı döndürür.
|
||||
///
|
||||
/// Kurulum kökü `mount` noktasına bağlandıktan sonra çağrılır; `findmnt`
|
||||
/// doğrudan cevabı verir. Böylece kök bölümünün numarası veya diski
|
||||
/// tahmin edilmez — manuel modda numaralar `assign_partition_numbers`'tan
|
||||
/// gelir ve kök farklı bir diskte olabilir.
|
||||
async fn mounted_root_device(mount: &str) -> Result<String, String> {
|
||||
let out = tokio::process::Command::new("findmnt")
|
||||
.args(["-n", "-o", "SOURCE", "--target", mount])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("findmnt çalıştırılamadı: {}", e))?;
|
||||
|
||||
let dev = String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
// findmnt bazen /dev/... yerine /dev/mapper/... ya da kökü verir.
|
||||
if dev.is_empty() || dev == "none" {
|
||||
return Err(format!(
|
||||
"{} bağlama noktasının arkasındaki aygıt çözümlenemedi; \
|
||||
kök bölümü bağlanmamış olabilir", mount
|
||||
));
|
||||
}
|
||||
Ok(dev)
|
||||
}
|
||||
|
||||
/// initramfs yeniler (GRUB öncesi zorunlu).
|
||||
/// PisiLinux `dracut` kullanıyorsa `dracut -f --regenerate-all`,
|
||||
/// mkinitcpio kullanıyorsa `mkinitcpio -P`.
|
||||
pub struct GenerateInitramfsJob {
|
||||
pub mount: String,
|
||||
pub encrypt_root: bool,
|
||||
pub lvm_active: bool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for GenerateInitramfsJob {
|
||||
fn name(&self) -> &str { "job_initramfs" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
// LVM için initramfs'e lvm modülü/hook'u ekleyen konfigürasyon
|
||||
if self.lvm_active && !self.encrypt_root {
|
||||
ui.log("LVM algılandı: initramfs LVM ayarlanıyor…");
|
||||
|
||||
// dracut: add lvm module
|
||||
let dracut_conf = format!("{}/etc/dracut.conf.d/99-lvm.conf", self.mount);
|
||||
let _ = tokio::fs::create_dir_all(format!("{}/etc/dracut.conf.d", self.mount)).await;
|
||||
let _ = tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" lvm \"\n").await;
|
||||
|
||||
// mkinitcpio: add lvm2 hook
|
||||
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
|
||||
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
|
||||
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
|
||||
if let Ok(content) = tokio::fs::read_to_string(&mkinitcpio_conf).await {
|
||||
if content.contains("HOOKS=") && !content.contains("lvm2") {
|
||||
let new_content = content
|
||||
.replace("HOOKS=\"", "HOOKS=\"lvm2 ")
|
||||
.replace("HOOKS=(", "HOOKS=(lvm2 ");
|
||||
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LUKS için initramfs'e cryptsetup ekleyen konfigürasyon
|
||||
if self.encrypt_root {
|
||||
ui.log("LUKS algılandı: initramfs cryptsetup ayarlanıyor…");
|
||||
|
||||
// dracut: add crypt module
|
||||
let dracut_conf = format!("{}/etc/dracut.conf.d/99-luks.conf", self.mount);
|
||||
tokio::fs::create_dir_all(
|
||||
format!("{}/etc/dracut.conf.d", self.mount)
|
||||
).await.map_err(|e| format!("dracut.conf.d oluşturulamadı: {}", e))?;
|
||||
tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" crypt lvm \"\n")
|
||||
.await.map_err(|e| format!("dracut luks conf yazılamadı: {}", e))?;
|
||||
|
||||
// mkinitcpio: add encrypt hook
|
||||
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
|
||||
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
|
||||
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
|
||||
let content = tokio::fs::read_to_string(&mkinitcpio_conf)
|
||||
.await.unwrap_or_default();
|
||||
if content.contains("HOOKS=") && !content.contains("encrypt") {
|
||||
let new_content = content
|
||||
.replace("HOOKS=\"", "HOOKS=\"encrypt ")
|
||||
.replace("HOOKS=(", "HOOKS=(encrypt ");
|
||||
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PisiLinux /sbin/mkinitramfs, dracut, mkinitcpio sırasıyla dene
|
||||
let cmds: &[&[&str]] = &[
|
||||
&["mkinitramfs", "-f"],
|
||||
&["dracut", "-f", "--regenerate-all"],
|
||||
&["mkinitcpio", "-P"],
|
||||
];
|
||||
for cmd in cmds {
|
||||
if run_chroot(&self.mount, cmd).await.is_ok() {
|
||||
ui.log(format!("initramfs oluşturuldu ({})", cmd[0]));
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err("initramfs oluşturulamadı: mkinitramfs, dracut, mkinitcpio denendi".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// GRUB bootloader kurulumu (UEFI veya BIOS).
|
||||
pub struct InstallGrubJob {
|
||||
pub disk: String,
|
||||
pub mount: String,
|
||||
pub is_uefi: bool,
|
||||
pub boot_device: String,
|
||||
pub timeout: u32,
|
||||
pub password: String,
|
||||
pub kernel_options: String,
|
||||
pub encrypt_root: bool,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for InstallGrubJob {
|
||||
fn name(&self) -> &str { "job_bootloader" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
// Ana dizinleri oluştur (demo modunda rsync çalışmaz)
|
||||
let etc_default = format!("{}/etc/default", self.mount);
|
||||
let _ = tokio::fs::create_dir_all(&etc_default).await;
|
||||
let grub_d = format!("{}/etc/grub.d", self.mount);
|
||||
let _ = tokio::fs::create_dir_all(&grub_d).await;
|
||||
|
||||
// GRUB şifresi (varsa) — grub-mkpasswd-pbkdf2 ile hash üretilir,
|
||||
// düz metin asla diske yazılmaz.
|
||||
if !self.password.is_empty() {
|
||||
ui.log("GRUB şifresi PBKDF2 ile hashleniyor…");
|
||||
|
||||
// grub-mkpasswd-pbkdf2 şifreyi stdin'den iki kez okur (şifre + doğrulama)
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let pw_input = format!("{}\n{}\n", self.password, self.password);
|
||||
let mut child = tokio::process::Command::new("grub-mkpasswd-pbkdf2")
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|e| format!("grub-mkpasswd-pbkdf2 başlatılamadı: {}", e))?;
|
||||
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
stdin.write_all(pw_input.as_bytes()).await
|
||||
.map_err(|e| format!("grub-mkpasswd-pbkdf2 stdin hatası: {}", e))?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().await
|
||||
.map_err(|e| format!("grub-mkpasswd-pbkdf2 bekleme hatası: {}", e))?;
|
||||
|
||||
// Çıktıdan "grub.pbkdf2.sha512...." token'ını ayrıştır
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let hash = stdout
|
||||
.lines()
|
||||
.find_map(|l| {
|
||||
let pos = l.find("grub.pbkdf2")?;
|
||||
Some(l[pos..].trim().to_string())
|
||||
})
|
||||
.ok_or("grub-mkpasswd-pbkdf2 çıktısında hash bulunamadı")?;
|
||||
|
||||
let custom_grub = format!(
|
||||
"#!/bin/sh\nset superusers=\"root\"\npassword_pbkdf2 root {}\n",
|
||||
hash
|
||||
);
|
||||
let pw_path = format!("{}/etc/grub.d/40_custom", self.mount);
|
||||
tokio::fs::write(&pw_path, custom_grub)
|
||||
.await
|
||||
.map_err(|e| format!("GRUB şifre dosyası yazılamadı: {}", e))?;
|
||||
run_cmd("chmod", &["+x", &pw_path]).await?;
|
||||
ui.log("GRUB şifresi PBKDF2 hash olarak yazıldı.");
|
||||
}
|
||||
|
||||
// GRUB timeout ayarı
|
||||
ui.log(format!("GRUB timeout ayarlanıyor: {}s", self.timeout));
|
||||
let default_path = format!("{}/etc/default/grub", self.mount);
|
||||
let mut grub_default = tokio::fs::read_to_string(&default_path)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
// timeout satırını bul/değiştir
|
||||
let timeout_line = format!("GRUB_TIMEOUT={}", self.timeout);
|
||||
let found_timeout = grub_default.lines()
|
||||
.any(|l| l.trim_start().starts_with("GRUB_TIMEOUT="));
|
||||
if found_timeout {
|
||||
let mut lines: Vec<String> = grub_default.lines()
|
||||
.map(|l| {
|
||||
if l.trim_start().starts_with("GRUB_TIMEOUT=") {
|
||||
timeout_line.clone()
|
||||
} else {
|
||||
l.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
lines.push(String::new());
|
||||
grub_default = lines.join("\n");
|
||||
} else {
|
||||
grub_default.push_str(&format!("\n{}\n", timeout_line));
|
||||
}
|
||||
// kernel cmdline
|
||||
let mut cmdline = self.kernel_options.clone();
|
||||
if self.encrypt_root {
|
||||
// LUKS başlık UUID'si GEREKLİ. Önceden bölüm numarası tahmin
|
||||
// ediliyordu (UEFI'de 3, BIOS'ta 2) — bu manuel modda YANLIŞTIR
|
||||
// (numaralar assign_partition_numbers'tan gelir) ve üstelik
|
||||
// "cryptdevice=UUID=x:cryptroot" geçerli bir söz dizimi değildir.
|
||||
//
|
||||
// Bunun yerine çalışma zamanında gerçeği soruyoruz:
|
||||
// `cryptsetup status` LUKS başlığının bulunduğu aygıtı verir,
|
||||
// blkid ise o aygıtın LUKS UUID'sini (crypto_LUKS).
|
||||
// /dev/mapper/cryptroot'un UUID'si değil — mapper'ın arkasındaki
|
||||
// bölümünki kullanılır.
|
||||
let luks_part = luks_backing_device(CRYPTROOT_NAME).await?;
|
||||
let luks_uuid = blkid_uuid(&luks_part).await
|
||||
.map_err(|e| format!("{} LUKS UUID okunamadı: {}", luks_part, e))?;
|
||||
if luks_uuid.trim().is_empty() {
|
||||
return Err(format!("{} için LUKS UUID boş geldi", luks_part));
|
||||
}
|
||||
|
||||
ui.log(format!("LUKS: {} → {}", luks_part, luks_uuid));
|
||||
|
||||
// dracut sözdizimi: rd.luks.uuid=luks-<uuid>
|
||||
// mkinitcpio sözdizimi: cryptdevice=UUID=<uuid>:<mapper>
|
||||
// PisiLinux hangisini kullandığı belli olmadığı için ikisi de
|
||||
// yazılır; iki araç da diğerinin anahtarını yok sayar.
|
||||
let crypt_arg = format!(
|
||||
"rd.luks.uuid=luks-{uuid} rd.luks.name={uuid}={name} \
|
||||
root=/dev/mapper/{name} cryptdevice=UUID={uuid}:{name}",
|
||||
uuid = luks_uuid,
|
||||
name = CRYPTROOT_NAME,
|
||||
);
|
||||
if cmdline.is_empty() {
|
||||
cmdline = crypt_arg;
|
||||
} else {
|
||||
cmdline = format!("{} {}", crypt_arg, cmdline);
|
||||
}
|
||||
}
|
||||
if !cmdline.is_empty() {
|
||||
let cmdline_line = format!("GRUB_CMDLINE_LINUX=\"{}\"", cmdline);
|
||||
let found_cmdline = grub_default.lines()
|
||||
.any(|l| l.trim_start().starts_with("GRUB_CMDLINE_LINUX="));
|
||||
if found_cmdline {
|
||||
let mut lines: Vec<String> = grub_default.lines()
|
||||
.map(|l| {
|
||||
if l.trim_start().starts_with("GRUB_CMDLINE_LINUX=") {
|
||||
cmdline_line.clone()
|
||||
} else {
|
||||
l.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
lines.push(String::new());
|
||||
grub_default = lines.join("\n");
|
||||
} else {
|
||||
grub_default.push_str(&format!("{}\n", cmdline_line));
|
||||
}
|
||||
}
|
||||
tokio::fs::write(&default_path, grub_default)
|
||||
.await
|
||||
.map_err(|e| format!("/etc/default/grub yazılamadı: {}", e))?;
|
||||
|
||||
// Chroot içinde grub2-install çalıştır (UEFI veya BIOS)
|
||||
let mut grub_args = vec!["grub2-install"];
|
||||
let install_dev = if self.boot_device.is_empty() { self.disk.as_str() } else { self.boot_device.as_str() };
|
||||
|
||||
if self.is_uefi {
|
||||
// /boot/efi mevcut değilse veya bağlı değilse bağla
|
||||
let host_efi_dir = format!("{}/boot/efi", self.mount);
|
||||
let efi_dir = std::path::Path::new(&host_efi_dir);
|
||||
if !efi_dir.exists() || !is_mount_point(efi_dir) {
|
||||
ui.log("/boot/efi bağlanmamış, ESP taranıyor…");
|
||||
tokio::fs::create_dir_all(&host_efi_dir).await
|
||||
.map_err(|e| format!("{}/boot/efi oluşturulamadı: {}", self.mount, e))?;
|
||||
// Önyükleyici aygıtında ESP bölümü ara (önce 1, sonra 2)
|
||||
for part_num in &[1u32, 2] {
|
||||
let esp_candidate = part_path(install_dev, *part_num);
|
||||
if let Ok(fs_type) = get_fs_type(&esp_candidate).await {
|
||||
if fs_type == "vfat" {
|
||||
ui.log(format!("{} ESP olarak /boot/efi'ye bağlanıyor…", esp_candidate));
|
||||
let _ = run_cmd("mount", &[&esp_candidate, &host_efi_dir]).await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let efi_dir_arg = "--efi-directory=/boot/efi";
|
||||
ui.log(format!("GRUB UEFI modu {} aygıtına kuruluyor (chroot içinde)…", install_dev));
|
||||
grub_args.extend_from_slice(&[
|
||||
"--target=x86_64-efi",
|
||||
efi_dir_arg,
|
||||
"--bootloader-id=PisiLinux",
|
||||
"--recheck",
|
||||
install_dev,
|
||||
]);
|
||||
} else {
|
||||
ui.log(format!("GRUB BIOS modu {} aygıtına kuruluyor (chroot içinde)…", install_dev));
|
||||
grub_args.extend_from_slice(&[
|
||||
"--target=i386-pc",
|
||||
install_dev,
|
||||
"--recheck",
|
||||
]);
|
||||
}
|
||||
|
||||
run_chroot(&self.mount, &grub_args).await?;
|
||||
|
||||
// device.map: tüm diskleri kapsayan bir mapping oluştur
|
||||
ui.log("GRUB device.map oluşturuluyor…");
|
||||
let mut device_map = String::new();
|
||||
if let Ok(entries) = std::fs::read_dir("/dev") {
|
||||
let mut devices: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
let name = e.file_name();
|
||||
let s = name.to_string_lossy();
|
||||
let s = s.as_ref();
|
||||
match s {
|
||||
x if x.starts_with("sd") || x.starts_with("vd") => {
|
||||
// sdX / vdX = disk; sdX1 / vdX1 = partition
|
||||
x.len() <= 3 || !x[3..].chars().any(|c| c.is_ascii_digit())
|
||||
}
|
||||
x if x.starts_with("nvme") => {
|
||||
// nvme0n1 = disk; nvme0n1p1 = partition
|
||||
!x.contains('p')
|
||||
}
|
||||
x if x.starts_with("mmcblk") => {
|
||||
// mmcblk0 = disk; mmcblk0p1 = partition
|
||||
!x.contains('p')
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.map(|e| format!("/dev/{}", e.file_name().to_string_lossy()))
|
||||
.collect();
|
||||
devices.sort();
|
||||
for (i, dev) in devices.iter().enumerate() {
|
||||
device_map.push_str(&format!("(hd{}) {}\n", i, dev));
|
||||
}
|
||||
}
|
||||
if !device_map.is_empty() {
|
||||
let dm_path = format!("{}/boot/grub2/device.map", self.mount);
|
||||
let _ = tokio::fs::write(&dm_path, device_map).await;
|
||||
}
|
||||
|
||||
// /etc/grub.d/30_uefi-firmware: EFI firmware menüsü + Windows EFI taraması
|
||||
let uefi_script = r#"#!/bin/sh
|
||||
set -e
|
||||
|
||||
# GRUB2 Helper Script - Pisi Linux / Custom ISO
|
||||
prefix="/usr"
|
||||
exec_prefix="/usr"
|
||||
datarootdir="/usr/share"
|
||||
|
||||
export TEXTDOMAIN=grub
|
||||
export TEXTDOMAINDIR="${datarootdir}/locale"
|
||||
|
||||
. "${datarootdir}/grub/grub-mkconfig_lib"
|
||||
|
||||
# 1. EFI Firmware (BIOS Setup) Menü Eklentisi
|
||||
efi_vars_dir=/sys/firmware/efi/efivars
|
||||
EFI_GLOBAL_VARIABLE=8be4df61-93ca-11d2-aa0d-00e098032b8c
|
||||
OsIndications="$efi_vars_dir/OsIndicationsSupported-$EFI_GLOBAL_VARIABLE"
|
||||
|
||||
if [ -e "$OsIndications" ] && \
|
||||
[ "$(( $(printf 0x%x \'"$(cat $OsIndications | cut -b1)") & 1 ))" = 1 ]; then
|
||||
LABEL="System setup"
|
||||
|
||||
cat << EOF
|
||||
if [ "\$grub_platform" = "efi" ]; then
|
||||
fwsetup --is-supported
|
||||
if [ "\$?" = 0 ]; then
|
||||
menuentry '$LABEL' \$menuentry_id_option 'uefi-firmware' {
|
||||
fwsetup
|
||||
}
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 2. Dinamik Windows EFI Taraması (Disk Bağımsız)
|
||||
if [ -d /sys/firmware/efi ]; then
|
||||
|
||||
TEMP_MOUNT="/tmp/grub_win_detect"
|
||||
WIN_UUID=""
|
||||
|
||||
# Sistemdeki vfat (EFI) bölümlerini al
|
||||
EFI_PARTS=$(lsblk -list -no NAME,FSTYPE 2>/dev/null | awk '$2=="vfat" {print $1}')
|
||||
|
||||
for part in $EFI_PARTS; do
|
||||
DEV="/dev/$part"
|
||||
|
||||
MNT=$(findmnt -n -o TARGET "$DEV" 2>/dev/null || true)
|
||||
WAS_MOUNTED=1
|
||||
|
||||
if [ -z "$MNT" ]; then
|
||||
WAS_MOUNTED=0
|
||||
MNT="$TEMP_MOUNT"
|
||||
mkdir -p "$MNT"
|
||||
mount -o ro "$DEV" "$MNT" 2>/dev/null || continue
|
||||
fi
|
||||
|
||||
if [ -f "$MNT/EFI/Microsoft/Boot/bootmgfw.efi" ]; then
|
||||
WIN_UUID=$(blkid -s UUID -o value "$DEV" 2>/dev/null || true)
|
||||
if [ "$WAS_MOUNTED" -eq 0 ]; then
|
||||
umount "$MNT" 2>/dev/null || true
|
||||
fi
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$WAS_MOUNTED" -eq 0 ]; then
|
||||
umount "$MNT" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf "$TEMP_MOUNT"
|
||||
|
||||
if [ -n "$WIN_UUID" ]; then
|
||||
cat << EOF
|
||||
menuentry "Windows Boot Manager (UEFI)" --class windows --class os {
|
||||
insmod part_gpt
|
||||
insmod fat
|
||||
insmod search_fs_uuid
|
||||
insmod chain
|
||||
search --fs-uuid --set=root ${WIN_UUID}
|
||||
chainloader /EFI/Microsoft/Boot/bootmgfw.efi
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
fi
|
||||
"#;
|
||||
let uefi_script_path = format!("{}/etc/grub.d/30_uefi-firmware", self.mount);
|
||||
if let Err(e) = tokio::fs::write(&uefi_script_path, uefi_script).await {
|
||||
ui.log(format!("/etc/grub.d/30_uefi-firmware yazılamadı: {}", e));
|
||||
} else {
|
||||
ui.log("30_uefi-firmware güncellendi (EFI firmware + Windows taraması).");
|
||||
}
|
||||
|
||||
ui.log("grub2-mkconfig çalıştırılıyor…");
|
||||
// grub2-mkconfig'teki set -e'yi geçici olarak set +e yap;
|
||||
// böylece grub2-probe hataları script'i durdurmaz, os-prober hatayı atlayıp
|
||||
// sonraki aygıtlarla devam edebilir. stderr'deki probe uyarıları bastırılır.
|
||||
let _ = run_chroot(&self.mount, &["sh", "-c",
|
||||
"cp /usr/sbin/grub2-mkconfig /usr/sbin/grub2-mkconfig.bak 2>/dev/null; \
|
||||
sed -i 's/^set -e/set +e/' /usr/sbin/grub2-mkconfig 2>/dev/null; \
|
||||
GRUB_DISABLE_OS_PROBER=false grub2-mkconfig -o /boot/grub2/grub.cfg 2>/dev/null; \
|
||||
cp /usr/sbin/grub2-mkconfig.bak /usr/sbin/grub2-mkconfig 2>/dev/null; \
|
||||
rm -f /usr/sbin/grub2-mkconfig.bak 2>/dev/null",
|
||||
]).await;
|
||||
|
||||
let grub_cfg_path = format!("{}/boot/grub2/grub.cfg", self.mount);
|
||||
let grub_cfg_ok = std::path::Path::new(&grub_cfg_path).exists()
|
||||
&& std::fs::metadata(&grub_cfg_path).map(|m| m.len() > 0).unwrap_or(false);
|
||||
|
||||
if grub_cfg_ok {
|
||||
ui.log("GRUB yapılandırması başarıyla oluşturuldu.");
|
||||
Ok(())
|
||||
} else {
|
||||
ui.log("grub2-mkconfig başarısız, minimal yapılandırma oluşturuluyor…");
|
||||
generate_minimal_grub_cfg(ui, &self.mount, &self.disk, &self.boot_device, self.is_uefi, &self.kernel_options).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// grub2-mkconfig başarısız olursa elle minimal bir grub.cfg oluşturur.
|
||||
/// Windows ESP tespit edilirse chainloader girişi de ekler.
|
||||
async fn generate_minimal_grub_cfg(
|
||||
ui: &UiSender, mount: &str, disk: &str, boot_device: &str, is_uefi: bool, kernel_options: &str,
|
||||
) -> Result<(), String> {
|
||||
let boot_dir = format!("{}/boot", mount);
|
||||
let grub_dir = format!("{}/boot/grub2", mount);
|
||||
tokio::fs::create_dir_all(&grub_dir).await
|
||||
.map_err(|e| format!("{} oluşturulamadı: {}", grub_dir, e))?;
|
||||
|
||||
// Kök aygıtı TAHMİN EDİLMEZ.
|
||||
//
|
||||
// Daha önce `is_uefi` ile (3 | 2) deniyor ve `disk` (seçili disk)
|
||||
// üzerinden okunuyordu. Bu manuel modda iki yönden yanlıştır:
|
||||
// 1) numaralar assign_partition_numbers'tan gelir, (3|2) değil
|
||||
// 2) kök farklı bir diskteyse (çoklu disk kurulumu) `disk` üstünde
|
||||
// hiç kök yoktur
|
||||
// Kurulum kökü `mount` noktasına BAĞLANDIĞI için gerçek aygıt
|
||||
// findmnt'den sorulur — hem otomatik hem manuel modda doğru.
|
||||
let root_device = mounted_root_device(mount).await?;
|
||||
let root_uuid = blkid_uuid(&root_device).await
|
||||
.map_err(|e| format!("Kök UUID okunamadı ({}): {}", root_device, e))?;
|
||||
|
||||
let mut kernel_file: Option<String> = None;
|
||||
let mut initrd_files: Vec<String> = Vec::new();
|
||||
let mut ucode_files: Vec<String> = Vec::new();
|
||||
|
||||
if let Ok(mut entries) = tokio::fs::read_dir(&boot_dir).await {
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name.starts_with("kernel-") || name.starts_with("vmlinuz-") {
|
||||
kernel_file = Some(name);
|
||||
} else if name.starts_with("initramfs-") || name.starts_with("initrd-") {
|
||||
initrd_files.push(name);
|
||||
} else if name.starts_with("intel-ucode") || name.starts_with("amd-ucode") {
|
||||
ucode_files.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Demo modunda gerçek dosyalar kopyalanmadığı için /boot boştur;
|
||||
// sentetik bir kernel adıyla akışın tamamı denenebilir.
|
||||
let kernel = match kernel_file {
|
||||
Some(k) => k,
|
||||
None if DEMO_MODE.load(Ordering::Relaxed) => "kernel-demo".to_string(),
|
||||
None => return Err(format!("{}/boot/ içinde kernel bulunamadı", mount)),
|
||||
};
|
||||
|
||||
let kernel_ver = kernel.strip_prefix("kernel-")
|
||||
.or_else(|| kernel.strip_prefix("vmlinuz-"))
|
||||
.unwrap_or("");
|
||||
let initrd = if kernel_ver.is_empty() {
|
||||
initrd_files.first().cloned()
|
||||
} else {
|
||||
let matching: Vec<String> = initrd_files.iter()
|
||||
.filter(|f| f.contains(kernel_ver))
|
||||
.cloned()
|
||||
.collect();
|
||||
matching.first().cloned()
|
||||
};
|
||||
|
||||
let mut cfg = String::new();
|
||||
cfg.push_str("set default=0\n");
|
||||
cfg.push_str("set timeout=5\n\n");
|
||||
|
||||
// PisiLinux girişi
|
||||
if is_uefi {
|
||||
cfg.push_str("menuentry \"PisiLinux\" {\n");
|
||||
cfg.push_str(" insmod part_gpt\n insmod ext2\n");
|
||||
cfg.push_str(&format!(" search --no-floppy --fs-uuid --set=root {}\n", root_uuid));
|
||||
cfg.push_str(&format!(" linux /boot/{} root=UUID={} rw quiet", kernel, root_uuid));
|
||||
} else {
|
||||
// BIOS'ta `set root=(hdX,msdosY)` gerekir. hd0 ve msdos2
|
||||
// SABİT YAZILIYORDU; kök başka bir diskte/bölümdeyse yanlış
|
||||
// bölüm boot edilirdi. numara artık gerçek kök aygıtından gelir
|
||||
// (BIOS sıralaması için bkz. cihaz.map notu aşağıda).
|
||||
let msdos = crate::installer::part_number(&root_device).unwrap_or(2);
|
||||
cfg.push_str("menuentry \"PisiLinux\" {\n");
|
||||
cfg.push_str(" insmod ext2\n");
|
||||
cfg.push_str(&format!(" set root=(hd0,msdos{})\n", msdos));
|
||||
cfg.push_str(&format!(" linux /boot/{} root=UUID={} rw quiet", kernel, root_uuid));
|
||||
}
|
||||
|
||||
if !kernel_options.is_empty() {
|
||||
cfg.push_str(&format!(" {}", kernel_options));
|
||||
}
|
||||
cfg.push('\n');
|
||||
|
||||
if !ucode_files.is_empty() || !initrd_files.is_empty() {
|
||||
let mut initrd_line = String::from(" initrd");
|
||||
for ucode in &ucode_files {
|
||||
initrd_line.push_str(&format!(" /boot/{}", ucode));
|
||||
}
|
||||
if let Some(ref i) = initrd {
|
||||
initrd_line.push_str(&format!(" /boot/{}", i));
|
||||
} else if !initrd_files.is_empty() {
|
||||
for f in &initrd_files {
|
||||
initrd_line.push_str(&format!(" /boot/{}", f));
|
||||
}
|
||||
}
|
||||
cfg.push_str(&initrd_line);
|
||||
cfg.push('\n');
|
||||
}
|
||||
cfg.push_str("}\n");
|
||||
|
||||
// Windows chainloader girişi (yalnız UEFI)
|
||||
if is_uefi && !boot_device.is_empty() && boot_device != disk {
|
||||
let mut win_uuid: Option<String> = None;
|
||||
for part_num in &[1u32, 2] {
|
||||
let esp_candidate = part_path(boot_device, *part_num);
|
||||
if let Ok(fs_type) = get_fs_type(&esp_candidate).await {
|
||||
if fs_type == "vfat" {
|
||||
if let Ok(uuid) = blkid_uuid(&esp_candidate).await {
|
||||
win_uuid = Some(uuid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ref uuid) = win_uuid {
|
||||
cfg.push_str(&format!("\nmenuentry \"Windows Boot Manager\" {{\n"));
|
||||
cfg.push_str(" insmod part_gpt\n insmod fat\n");
|
||||
cfg.push_str(&format!(" search --no-floppy --fs-uuid --set=root {}\n", uuid));
|
||||
cfg.push_str(" chainloader /EFI/Microsoft/Boot/bootmgfw.efi\n");
|
||||
cfg.push_str("}\n");
|
||||
ui.log("Windows Boot Manager eklendi.");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::fs::write(&format!("{}/boot/grub2/grub.cfg", mount), cfg).await
|
||||
.map_err(|e| format!("grub.cfg yazılamadı: {}", e))?;
|
||||
|
||||
ui.log("Minimal GRUB yapılandırması oluşturuldu.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
//! Kurulum bir hata ile durduğunda veya bittiğinde çalışan temizlik işleri.
|
||||
//!
|
||||
//! Bu modülün varlık nedeni: `JobQueue::run_all` eskiden ilk hatada
|
||||
//! `return` ediyordu. O an hedef bölümler bağlıydı, LUKS mapper'ları
|
||||
//! açıktı ve swap aktifti — kullanıcı yeniden başlatmaya çalışırsa
|
||||
//! sistem onu reddediyor, elle de düzeltmesi dakikalar sürüyordu.
|
||||
//!
|
||||
//! Temizlik işleri `JobQueue::with_cleanup(...)` ile verilir ve başarı
|
||||
//! durumunda da, hata durumunda da çalışır.
|
||||
|
||||
use super::execution::umount_clean;
|
||||
use super::{mounted_paths_under, Job, UiSender, DEMO_MODE};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
/// Kurulum bitince veya hata ile durunca hedef sistemin bağlamalarını
|
||||
/// tamamen çözer: tüm mount'lar, LUKS mapper'ları, LVM birimleri ve swap.
|
||||
///
|
||||
/// Sıralama önemlidir — en derinden yüzeye:
|
||||
/// 1. `mount` altındaki tüm dosya sistemleri
|
||||
/// 2. LVM mantıksal birimleri (`lvchange -an`)
|
||||
/// 3. LUKS mapper'ları (`cryptsetup close`)
|
||||
/// 4. Hedef diskin swap bölümü (`swapoff`)
|
||||
///
|
||||
/// Swap en sonda: swap'a yazmış bir süreç varsa erken swapoff denemesi
|
||||
/// "device is busy" verir.
|
||||
pub struct EmergencyCleanupJob {
|
||||
pub mount: String,
|
||||
/// Hedef diskin bölüm yolları (swapoff için). Boşsa otomatik bulunur.
|
||||
pub partitions: Vec<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for EmergencyCleanupJob {
|
||||
fn name(&self) -> &str { "job_cleanup_emergency" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
|
||||
// ── 1. Dosya sistemleri ────────────────────────────────
|
||||
let targets = mounted_paths_under(&self.mount);
|
||||
if targets.is_empty() {
|
||||
ui.log("Bağlı dosya sistemi yok");
|
||||
}
|
||||
for target in &targets {
|
||||
if !umount_clean(target, ui).await {
|
||||
errors.push(format!("{} bağlı kaldı", target));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. LVM mantıksal birimleri ────────────────────────
|
||||
// Etkin VG'lerin tüm LV'leri kapatılır. `vgs --noheadings` boş
|
||||
// dönerse sistemde LVM yok demektir, hata değil.
|
||||
if let Err(e) = deactivate_lvm(ui).await {
|
||||
errors.push(e);
|
||||
}
|
||||
|
||||
// ── 3. LUKS mapper'ları ───────────────────────────────
|
||||
// `lsblk` TYPE=crypt listesi. Hedef diskin LUKS bölümü hangisi
|
||||
// ise onu kapatır; canlı ortamın kendi LUKS'u varsa dokunmaz.
|
||||
match close_crypt_mappers(&self.partitions).await {
|
||||
Ok(closed) => {
|
||||
for name in closed {
|
||||
ui.log(format!("LUKS kapatıldı: {}", name));
|
||||
}
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
|
||||
// ── 4. Swap ───────────────────────────────────────────
|
||||
for part in &self.partitions {
|
||||
if let Err(e) = swapoff_part(part, ui).await {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Etkin tüm LVM VG'lerini deaktive eder.
|
||||
async fn deactivate_lvm(ui: &UiSender) -> Result<(), String> {
|
||||
let out = tokio::process::Command::new("vgs")
|
||||
.args(["--noheadings", "-o", "vg_name"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("vgs çalıştırılamadı: {}", e))?;
|
||||
|
||||
let vgs: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect();
|
||||
|
||||
if vgs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut failed = Vec::new();
|
||||
for vg in vgs {
|
||||
ui.log(format!("LVM deaktive ediliyor: {}", vg));
|
||||
match super::run_cmd("vgchange", &["-an", &vg]).await {
|
||||
Ok(()) => {}
|
||||
Err(e) => failed.push(format!("{} ({})", vg, e)),
|
||||
}
|
||||
}
|
||||
|
||||
if failed.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("LVM deaktive edilemedi: {}", failed.join(", ")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Açık LUKS mapper'larını kapatır.
|
||||
///
|
||||
/// `partitions` verilmişse yalnızca o bölümlerin mapper'ları kapatılır
|
||||
/// (hedef diske ait olanlar). `cryptsetup luksUUID <dev>` ile eşleştirme
|
||||
/// yapılır. Liste boşsa hiçbir şeye dokunulmaz — canlı ortamın kendi
|
||||
/// şifreli bölümlerini kapatmak kullanıcının verisini riske atar.
|
||||
async fn close_crypt_mappers(partitions: &[String]) -> Result<Vec<String>, String> {
|
||||
if partitions.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mappers = super::open_crypt_mappers();
|
||||
if mappers.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut closed = Vec::new();
|
||||
for name in mappers {
|
||||
// Bu mapper hangi bölümün arkasında?
|
||||
let backing = backing_device(&name).await;
|
||||
let belongs = backing
|
||||
.as_deref()
|
||||
.map(|d| partitions.iter().any(|p| p == d))
|
||||
.unwrap_or(false);
|
||||
|
||||
if !belongs {
|
||||
continue;
|
||||
}
|
||||
|
||||
match super::run_cmd("cryptsetup", &["close", &name]).await {
|
||||
Ok(()) => closed.push(name),
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"cryptsetup close {} başarısız: {}. \
|
||||
Hedef bölüm hâlâ kullanımda olabilir.",
|
||||
name, e
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(closed)
|
||||
}
|
||||
|
||||
/// Bir LUKS mapper'ının arkasındaki gerçek aygıtı döndürür.
|
||||
async fn backing_device(mapper: &str) -> Option<String> {
|
||||
let out = tokio::process::Command::new("cryptsetup")
|
||||
.args(["status", mapper])
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.find_map(|l| l.trim().strip_prefix("device:"))
|
||||
.map(|d| d.trim().to_string())
|
||||
.filter(|d| !d.is_empty())
|
||||
}
|
||||
|
||||
/// Verilen bölüm swap ise `swapoff` çalıştırır. Swap değilse sessizce geçer.
|
||||
async fn swapoff_part(part: &str, ui: &UiSender) -> Result<(), String> {
|
||||
let out = tokio::process::Command::new("swapon")
|
||||
.args(["--show", "--noheadings", "-o", "NAME"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("swapon sorgusu başarısız: {}", e))?;
|
||||
|
||||
let active: Vec<String> = String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect();
|
||||
|
||||
if !active.iter().any(|s| s == part) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ui.log(format!("swapoff {}", part));
|
||||
super::run_cmd("swapoff", &[part]).await
|
||||
.map_err(|e| format!("swapoff {} başarısız: {} (bu bölümü kullanan süreçleri kapatın)", part, e))
|
||||
}
|
||||
@@ -0,0 +1,438 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Instant;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
use super::{run_cmd, DEMO_MODE, Job, UiSender};
|
||||
|
||||
/// Sistem dosyalarını kopyala (rsync).
|
||||
pub struct CopyFilesJob {
|
||||
/// Canlı sistemin kök dizini; ör. "/run/livecd/squashfs-root/"
|
||||
pub source: String,
|
||||
/// Hedef bağlama noktası; ör. "/mnt"
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for CopyFilesJob {
|
||||
fn name(&self) -> &str { "job_copy_files" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
ui.log(format!("[DEBUG] CopyFilesJob başladı"));
|
||||
ui.log(format!("[DEBUG] Kaynak: '{}'", self.source));
|
||||
ui.log(format!("[DEBUG] Hedef: '{}'", self.target));
|
||||
|
||||
// Kaynak dizini kontrol et
|
||||
let source_path = std::path::Path::new(&self.source);
|
||||
let source_exists = source_path.exists();
|
||||
let source_readable = source_exists && source_path.is_dir();
|
||||
ui.log(format!("[DEBUG] Kaynak mevcut: {}, dizin: {}", source_exists, source_readable));
|
||||
|
||||
if !source_exists {
|
||||
ui.log(format!("[DEBUG] Kaynak dizin içeriği aranıyor..."));
|
||||
// Kaynağın bir üst dizinini dene
|
||||
let parent = source_path.parent();
|
||||
if let Some(p) = parent {
|
||||
if p.exists() {
|
||||
if let Ok(entries) = std::fs::read_dir(p) {
|
||||
for entry in entries.flatten().take(20) {
|
||||
ui.log(format!("[DEBUG] /{:?}", entry.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.log("[DEBUG] Kaynağın parent'ı yok (kök dizin olabilir)");
|
||||
}
|
||||
} else if source_readable {
|
||||
// İlk 10 girdiyi listele
|
||||
if let Ok(entries) = std::fs::read_dir(source_path) {
|
||||
let mut count = 0;
|
||||
for entry in entries.flatten().take(10) {
|
||||
ui.log(format!("[DEBUG] {:?}", entry.path()));
|
||||
count += 1;
|
||||
}
|
||||
ui.log(format!("[DEBUG] (ilk {} girdi listelendi)", count));
|
||||
}
|
||||
}
|
||||
|
||||
ui.log(format!("rsync {} → {}", self.source, self.target));
|
||||
ui.copy_started();
|
||||
|
||||
let is_root_source = self.source.trim_end_matches('/') == "";
|
||||
|
||||
// ── rsync exclude'leri ──
|
||||
// Aynı liste hem rsync'e hem de aşağıdaki bilgilendirme amaçlı find
|
||||
// sayacına uygulanır. İlerleme artık rsync'in kendi bayt-tabanlı
|
||||
// yüzdesinden (--info=progress2) alındığı için sayım farkları barla
|
||||
// ilgili değildir; find yalnızca "N dosya kopyalanacak" logu içindir.
|
||||
let mut excludes: Vec<String> = vec![
|
||||
"/proc/**".to_string(),
|
||||
"/sys/**".to_string(),
|
||||
"/dev/**".to_string(),
|
||||
"/run/**".to_string(),
|
||||
"/tmp/**".to_string(),
|
||||
"/var/tmp/**".to_string(),
|
||||
"/var/cache/**".to_string(),
|
||||
"/home/**".to_string(),
|
||||
"/root/**".to_string(),
|
||||
"/lost+found".to_string(),
|
||||
"*.sqfs".to_string(),
|
||||
];
|
||||
|
||||
// Kaynak canlı sistem köküyse ekstra exclude'ler
|
||||
if is_root_source {
|
||||
excludes.extend_from_slice(&[
|
||||
"/bootmnt/**".to_string(),
|
||||
"/ro_branch/**".to_string(),
|
||||
"/mnt/**".to_string(),
|
||||
"/media/**".to_string(),
|
||||
"/var/log/journal/**".to_string(),
|
||||
"/var/lib/pisi/cache/**".to_string(),
|
||||
"/var/lib/pisi/archives/**".to_string(),
|
||||
"/etc/adjtime".to_string(),
|
||||
"/etc/mtab".to_string(),
|
||||
"/etc/resolv.conf".to_string(),
|
||||
"/etc/hostname".to_string(),
|
||||
"/etc/machine-id".to_string(),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Önce find ile toplam dosya sayısını bul (yalnızca bilgilendirme) ──
|
||||
let find_path = self.source.trim_end_matches('/');
|
||||
let find_path = if find_path.is_empty() { "/" } else { find_path };
|
||||
let _total_files = {
|
||||
let mut cmd = Command::new("find");
|
||||
cmd.arg(find_path);
|
||||
if is_root_source {
|
||||
cmd.arg("-xdev");
|
||||
}
|
||||
for ex in &excludes {
|
||||
if let Some(dir) = ex.strip_suffix("/**") {
|
||||
cmd.args(["-path", dir, "-prune", "-o"]);
|
||||
} else if ex.starts_with('/') {
|
||||
cmd.args(["-path", ex.as_str(), "-prune", "-o"]);
|
||||
} else {
|
||||
cmd.args(["-name", ex.as_str(), "-prune", "-o"]);
|
||||
}
|
||||
}
|
||||
cmd.args(["-type", "f", "-print"]);
|
||||
cmd.stdout(std::process::Stdio::piped());
|
||||
cmd.stderr(std::process::Stdio::null());
|
||||
match cmd.spawn() {
|
||||
Ok(mut c) => {
|
||||
let stdout = c.stdout.take()
|
||||
.ok_or_else(|| "find stdout alınamadı".to_string())?;
|
||||
let r = BufReader::new(stdout);
|
||||
let mut lines = r.lines();
|
||||
let mut n: u64 = 0;
|
||||
while let Ok(Some(_)) = lines.next_line().await {
|
||||
n += 1;
|
||||
}
|
||||
let _ = c.wait().await;
|
||||
if n > 0 {
|
||||
ui.log(format!("{} dosya kopyalanacak", n));
|
||||
}
|
||||
n
|
||||
}
|
||||
Err(_) => 0
|
||||
}
|
||||
};
|
||||
|
||||
let mut args: Vec<String> = vec![
|
||||
"-aAXH".to_string(),
|
||||
"--out-format=%n".to_string(),
|
||||
// Bayt-tabanlı ilerleme (stdout'a \r ile karışır). rsync kendisi
|
||||
// hesapladığı için %100'e ancak kopyalama gerçekten bitince ulaşır.
|
||||
"--info=progress2".to_string(),
|
||||
// Incremental recursion (varsayılan) dosya listesini aktarım
|
||||
// sırasında büyütür; toplam bayt revize olunca yüzde geriye
|
||||
// düşebilir (%98 → %65 gibi). Liste önden tam kurulunca yüzde
|
||||
// monoton ilerler.
|
||||
"--no-inc-recursive".to_string(),
|
||||
];
|
||||
for ex in &excludes {
|
||||
args.push(format!("--exclude={}", ex));
|
||||
}
|
||||
|
||||
if let (Ok(source_path), Ok(target_path)) = (
|
||||
std::path::Path::new(&self.source).canonicalize(),
|
||||
std::path::Path::new(&self.target).canonicalize(),
|
||||
) {
|
||||
if let Ok(relative_target) = target_path.strip_prefix(&source_path) {
|
||||
if !relative_target.as_os_str().is_empty() {
|
||||
// İçerik hariç tutulur ama dizinin kendisi boş olarak kalır
|
||||
// (ör. /mnt). Çıplak desen kullanılırsa dizinin kendisi de
|
||||
// budanır ve kurulu sistemde /mnt hiç oluşmaz.
|
||||
args.push(format!("--exclude={}/**", relative_target.display()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
args.push(self.source.clone());
|
||||
args.push(self.target.clone());
|
||||
|
||||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||||
ui.log("rsync /run/livecd/squashfs-root/ → /mnt (DEMO)");
|
||||
let steps = 20;
|
||||
for i in 0..steps {
|
||||
let p = (i + 1) as f32 / steps as f32;
|
||||
ui.current_file(format!("/usr/share/doc/{}/README.md", (i * 137) % 50));
|
||||
ui.sub_progress(p);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
|
||||
}
|
||||
ui.copy_finished();
|
||||
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 stdout = child.stdout.take()
|
||||
.ok_or_else(|| "rsync stdout alınamadı".to_string())?;
|
||||
let stderr = child.stderr.take()
|
||||
.ok_or_else(|| "rsync stderr alınamadı".to_string())?;
|
||||
|
||||
// Stderr'i arka planda oku (hata birikimi ve tampon taşmasını engelle).
|
||||
// progress2 satırları stdout'a karıştığı için yalnızca hata metni toplanır.
|
||||
let stderr_buf = std::sync::Arc::new(tokio::sync::Mutex::new(String::new()));
|
||||
let stderr_buf_clone = stderr_buf.clone();
|
||||
tokio::spawn(async move {
|
||||
let r = BufReader::new(stderr);
|
||||
let mut lines = r.lines();
|
||||
while let Ok(Some(l)) = lines.next_line().await {
|
||||
let mut b = stderr_buf_clone.lock().await;
|
||||
b.push_str(&l);
|
||||
b.push('\n');
|
||||
}
|
||||
});
|
||||
|
||||
// Stdout: --out-format=%n ile dosya adları, --info=progress2 ile
|
||||
// bayt-tabanlı ilerleme satırları aynı akışa, \r ve \n ayraçlarıyla
|
||||
// karışık olarak yazılır. Her parçayı içeriğine göre sınıflandırırız:
|
||||
// "%" içerenler ilerleme (yüzde), diğerleri dosya adıdır.
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
let mut file_count: u64 = 0;
|
||||
// Son gönderilen yüzde — barın geriye düşmemesi için (--no-inc-recursive
|
||||
// ile gerekmemesi gerekir ama yine de garanti).
|
||||
let mut last_sub: f32 = 0.0;
|
||||
// Son UI güncellemesi — çok sık güncelleme GPU (özellikle Intel UMA) baskısını azaltır
|
||||
let mut last_update = Instant::now();
|
||||
|
||||
while let Some(line) = lines.next_line().await
|
||||
.map_err(|e| format!("rsync çıktısı okunamadı: {}", e))?
|
||||
{
|
||||
for segment in line.split('\r') {
|
||||
let trimmed = segment.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.contains('%') {
|
||||
if let Some(pct) = parse_progress_percent(trimmed) {
|
||||
if pct >= last_sub {
|
||||
last_sub = pct;
|
||||
ui.sub_progress(pct);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
file_count += 1;
|
||||
|
||||
let now = Instant::now();
|
||||
if now.duration_since(last_update) >= std::time::Duration::from_millis(50)
|
||||
|| file_count == 1
|
||||
|| trimmed.contains("Packages") || trimmed.contains("package")
|
||||
{
|
||||
ui.current_file(trimmed.to_string());
|
||||
last_update = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = child.wait().await
|
||||
.map_err(|e| format!("rsync beklenemedi: {}", e))?;
|
||||
|
||||
if !status.success() {
|
||||
let err = stderr_buf.lock().await.clone();
|
||||
return Err(format!(
|
||||
"rsync çıkış kodu: {:?}. Hata: {}",
|
||||
status.code(),
|
||||
err.trim()
|
||||
));
|
||||
}
|
||||
|
||||
ui.copy_finished();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// /proc, /sys, /dev, /dev/pts dizinlerini chroot hedefine bind-mount yapar.
|
||||
/// Standart `chroot` için gereklidir; kurulum başında çağrılmalıdır.
|
||||
pub struct MountBindJob {
|
||||
pub mount: String, // ör. "/mnt"
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for MountBindJob {
|
||||
fn name(&self) -> &str { "job_mount_bind" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
let mt = &self.mount;
|
||||
// (kaynak, hedef) — kaynak sistem üzerindeki gerçek path
|
||||
let bind_dirs = [
|
||||
("/proc", format!("{}/proc", mt)),
|
||||
("/sys", format!("{}/sys", mt)),
|
||||
("/dev", format!("{}/dev", mt)),
|
||||
("/dev/pts", format!("{}/dev/pts", mt)),
|
||||
("/run", format!("{}/run", mt)), // COMAR/dbus için gerekli
|
||||
];
|
||||
for (src, target) in &bind_dirs {
|
||||
tokio::fs::create_dir_all(target).await
|
||||
.map_err(|e| format!("{} oluşturulamadı: {}", target, e))?;
|
||||
ui.log(format!("mount --bind {} {}", src, target));
|
||||
run_cmd("mount", &["--bind", src, target]).await?;
|
||||
}
|
||||
|
||||
// efivarfs — UEFI kurulumunda grub-install için zorunlu
|
||||
let efivarfs_target = format!("{}/sys/firmware/efi/efivars", mt);
|
||||
if std::path::Path::new("/sys/firmware/efi/efivars").exists() {
|
||||
tokio::fs::create_dir_all(&efivarfs_target).await
|
||||
.map_err(|e| format!("{} oluşturulamadı: {}", efivarfs_target, e))?;
|
||||
ui.log(format!("mount --bind /sys/firmware/efi/efivars {}", efivarfs_target));
|
||||
run_cmd("mount", &["--bind", "/sys/firmware/efi/efivars", &efivarfs_target]).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Chroot ve disk bağlantılarını temizler (kurulum sonunda çağrılır).
|
||||
///
|
||||
/// Önce bind-mount'lar, ardından gerçek bölümler (/boot/efi ve kök)
|
||||
/// ters sırada unmount edilir. FAT32 (ESP) backup boot sektörü yalnızca
|
||||
/// umount anında güncellendiği için bölümler `-lf` ile hemen lazy
|
||||
/// unmount edilmez; önce temiz `umount` denenir, sadece "busy" durumunda
|
||||
/// `umount -lf` ile son çareye düşülür.
|
||||
pub struct UnmountBindJob {
|
||||
pub mount: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for UnmountBindJob {
|
||||
fn name(&self) -> &str { "job_umount_bind" }
|
||||
|
||||
/// `mount` altındaki TÜM bağlı dosya sistemlerini en derinden yüzeye
|
||||
/// doğru çözer.
|
||||
///
|
||||
/// Önceden elle yazılmış sabit bir liste vardı (`proc`, `sys`, `dev`,
|
||||
/// `boot/efi`, ...). Bu liste yalnızca otomatik modda doğruydu:
|
||||
/// manuel bölümlemede `/home`, `/var`, `/boot` gibi ek bölümler
|
||||
/// kurulum bitince bağlı kalıyordu. Artık düzen bilinmiyor —
|
||||
/// `/proc/self/mountinfo` okunup `mount/` altındaki her şey bulunuyor.
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
let mt = &self.mount;
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
|
||||
let mut targets = super::mounted_paths_under(mt);
|
||||
|
||||
if targets.is_empty() {
|
||||
ui.log("Çözülecek bağlama noktası yok (zaten temiz)");
|
||||
targets.push(mt.clone());
|
||||
}
|
||||
|
||||
for target in &targets {
|
||||
if !umount_clean(target, ui).await {
|
||||
errors.push(format!("{} temiz unmount edilemedi", target));
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
return Err(format!(
|
||||
"Mount temizleme hatası — bazı dosya sistemleri bağlı kalmış olabilir:\n{}",
|
||||
errors.join("\n")
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tek bir mount noktasını önce temiz `umount` ile, başarısızsa `umount -lf`
|
||||
/// (lazy) ile çözer. "not mounted" / "no such file" gibi beklenen durumlar
|
||||
/// kritik sayılmaz; yalnızca komut çalıştırılamaması gerçek hatadır.
|
||||
pub(crate) async fn umount_clean(target: &str, ui: &UiSender) -> bool {
|
||||
ui.log(format!("umount {}", target));
|
||||
match tokio::process::Command::new("umount")
|
||||
.arg(target).status().await
|
||||
{
|
||||
Ok(status) if status.success() => true,
|
||||
Ok(status) => {
|
||||
ui.log(format!("⚠ umount temiz deneme başarısız (kod {}): {} → lazy deneniyor", status, target));
|
||||
match tokio::process::Command::new("umount")
|
||||
.args(["-lf", target]).status().await
|
||||
{
|
||||
Ok(s) if s.success() => true,
|
||||
Ok(s) => {
|
||||
// "not mounted" / "no such file" beklenen durumlar
|
||||
ui.log(format!("⚠ umount uyarısı (kritik değil, kod {}): {}", s, target));
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("umount komutu başlatılamadı ({}): {}", target, e);
|
||||
ui.log(format!("✗ {}", msg));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = format!("umount komutu başlatılamadı ({}): {}", target, e);
|
||||
ui.log(format!("✗ {}", msg));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// rsync `--info=progress2` satırından bayt-tabanlı yüzdeyi çıkarır.
|
||||
/// Örnek satır:
|
||||
/// " 1,234,567 45% 3.21MB/s 0:00:05 (xfr#1, to-chk=999/1000)"
|
||||
/// Dosya adı bazlı tahminin aksine bu yüzde gerçek aktarılan veriyi yansıtır
|
||||
/// ve %100'e ancak kopyalama tamamlanınca ulaşır.
|
||||
fn parse_progress_percent(line: &str) -> Option<f32> {
|
||||
let pct_pos = line.rfind('%')?;
|
||||
let before = &line[..pct_pos];
|
||||
let token = before
|
||||
.rsplit(|c: char| c.is_whitespace())
|
||||
.find(|t| !t.is_empty())?;
|
||||
let digits: String = token.chars().filter(|c| c.is_ascii_digit()).collect();
|
||||
let n: f32 = digits.parse().ok()?;
|
||||
Some((n / 100.0).clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_progress_percent;
|
||||
|
||||
#[test]
|
||||
fn parses_progress2_percent() {
|
||||
// Gerçek rsync --info=progress2 çıktı parçaları
|
||||
assert_eq!(
|
||||
parse_progress_percent(" 32.768 0% 0,00kB/s 0:00:00 "),
|
||||
Some(0.0)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_progress_percent(" 20.971.520 99% 587,32MB/s 0:00:00 (xfr#1, to-chk=3/5)"),
|
||||
Some(0.99)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_progress_percent(" 21.022.720 100% 571,93MB/s 0:00:00 (xfr#2, to-chk=0/5)"),
|
||||
Some(1.0)
|
||||
);
|
||||
// Dosya adları yüzde içermez → None
|
||||
assert_eq!(parse_progress_percent("big.bin"), None);
|
||||
assert_eq!(parse_progress_percent("sub/"), None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
use super::{run_chroot, Job, UiSender};
|
||||
|
||||
/// `pisi configure-pending` (kısaltması `pisi cp`): kurulan paketlerin
|
||||
/// post-install betiklerini çalıştırır. Bu adım olmadan kurulu sistemde
|
||||
/// servisler/tetikleyiciler kurulmamış olur ve sistem düzgün açılmaz.
|
||||
pub struct RunComarJob {
|
||||
pub mount: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for RunComarJob {
|
||||
fn name(&self) -> &str { "job_comar" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
ui.log("pisi paketleri yapılandırılıyor (configure-pending)…");
|
||||
match run_chroot(&self.mount, &["pisi", "cp"]).await {
|
||||
Ok(_) => {
|
||||
ui.log("✓ pisi paketleri yapılandırıldı");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
// "Yapılandırılacak paket yok" bir hata değil; yeni kurulumda
|
||||
// (ör. yalnızca temel sistem) normal olarak oluşabilir.
|
||||
let benign = ["no packages to configure", "nothing to do",
|
||||
"yapılandırılacak paket", "nothing to configure"];
|
||||
if benign.iter().any(|m| e.to_lowercase().contains(m)) {
|
||||
ui.log(" pisi: yapılandırılacak paket yok, atlanıyor");
|
||||
Ok(())
|
||||
} else {
|
||||
// Post-install betikleri çalışmadı: bu, kurulumun
|
||||
// sessizce bozuk bırakılması anlamına gelir. Kurulumu
|
||||
// durdurup hatayı kullanıcıya göster.
|
||||
Err(format!(
|
||||
"pisi configure-pending başarısız: {}\n\
|
||||
Paketlerin kurulum betikleri çalışmadı; sistem \
|
||||
düzgün başlamayabilir.", e
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ldconfig: paylaşımlı kütüphane önbelleğini yeniden oluşturur.
|
||||
pub struct RunLdconfigJob {
|
||||
pub mount: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for RunLdconfigJob {
|
||||
fn name(&self) -> &str { "job_ldconfig" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
ui.log("ldconfig çalıştırılıyor…");
|
||||
run_chroot(&self.mount, &["ldconfig"]).await
|
||||
.map_err(|e| format!("ldconfig başarısız: {}\n\
|
||||
Paylaşımlı kütüphane önbelleği oluşturulamadı.", e))?;
|
||||
ui.log("✓ ldconfig tamamlandı");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// update-environment: PisiLinux init/betiklerini yeniden yapılandırır.
|
||||
///
|
||||
/// Bu adım init sistemi için kritiktir (udev kuralları, servisler,
|
||||
/// /etc/localtime senkronizasyonu). Sessizce yutulursa kurulum "başarılı"
|
||||
/// görünür ama sistem açılmaz — bu yüzden hata fatal.
|
||||
pub struct UpdateEnvironmentJob {
|
||||
pub mount: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for UpdateEnvironmentJob {
|
||||
fn name(&self) -> &str { "job_update_env" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
ui.log("update-environment çalıştırılıyor…");
|
||||
run_chroot(&self.mount, &["update-environment"]).await
|
||||
.map_err(|e| format!("update-environment başarısız: {}\n\
|
||||
Init betikleri yapılandırılmadı; kurulu sistem \
|
||||
düzgün başlamayabilir.", e))?;
|
||||
ui.log("✓ update-environment tamamlandı");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Live ortam izlerini hedef sistemden temizler.
|
||||
pub struct CleanupLiveJob {
|
||||
pub mount: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for CleanupLiveJob {
|
||||
fn name(&self) -> &str { "job_cleanup_live" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
let mt = &self.mount;
|
||||
ui.log("Live ortam izleri temizleniyor…");
|
||||
|
||||
// Canlı ortamdan kalan dizinler: yoksa hata değil, `let _ =` uygun.
|
||||
let _ = tokio::fs::remove_dir_all(format!("{}/run/livemedia", mt)).await;
|
||||
let _ = tokio::fs::remove_dir_all(format!("{}/var/cache/pisi/packages", mt)).await;
|
||||
let _ = tokio::fs::remove_dir_all(format!("{}/bootmnt", mt)).await;
|
||||
// YALI desktop dosyaları
|
||||
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali.desktop", mt)).await;
|
||||
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali-bin.desktop", mt)).await;
|
||||
let _ = run_chroot(&self.mount, &["userdel", "-r", "pisi"]).await;
|
||||
let _ = run_chroot(&self.mount, &["pisi", "rm", "yali-rs"]).await;
|
||||
let _ = run_chroot(&self.mount, &["pisi", "rr", "live"]).await;
|
||||
|
||||
// ── Aşağıdakiler FATAL ───────────────────────────────────
|
||||
// Depo tanımları yapılmazsa kurulu sistemde `pisi install`
|
||||
// çalışmaz, yani kullanıcı paket kuramaz. Sessizce yutulursa
|
||||
// kurulum "başarılı" görünür ama sistem kullanılamaz.
|
||||
ui.log("Depolar ekleniyor…");
|
||||
for (name, url) in [
|
||||
("stable2", "https://stable2.pisilinux.org/pisi-index.xml.xz"),
|
||||
("contrib", "https://contrib.pisilinux.org/pisi-index.xml.xz"),
|
||||
] {
|
||||
run_chroot(&self.mount, &["pisi", "ar", name, url]).await
|
||||
.map_err(|e| format!("'{}' deposu eklenemedi: {}\n\
|
||||
Kurulu sistemde paket kurulamaz.", name, e))?;
|
||||
}
|
||||
|
||||
// Depoları etkinleştir
|
||||
ui.log("Depolar etkinleştiriliyor…");
|
||||
run_chroot(&self.mount, &["pisi", "er", "contrib", "stable2"]).await
|
||||
.map_err(|e| format!("Depolar etkinleştirilemedi: {}", e))?;
|
||||
|
||||
// Paket listelerini güncelle (ağ gerektirir, birkaç dakika sürebilir)
|
||||
ui.log("Depolar güncelleniyor (ağ gerekir, biraz sürebilir)…");
|
||||
run_chroot(&self.mount, &["pisi", "ur"]).await
|
||||
.map_err(|e| format!("Paket listeleri güncellenemedi: {}\n\
|
||||
Depolar tanımlı ama indeksleri eksik olabilir.", e))?;
|
||||
|
||||
ui.log("✓ Live temizlik tamamlandı");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use super::{Job, UiSender};
|
||||
|
||||
/// Klavye düzenini /etc/vconsole.conf, /etc/conf.d/mudur ve X11 yapılandırmasına yazar.
|
||||
pub struct ConfigureKeyboardJob {
|
||||
pub mount: String,
|
||||
pub layout: String, // ör. "tr"
|
||||
pub variant: String, // ör. "f" veya ""
|
||||
pub language: String, // ör. "tr", "de", "fr"
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for ConfigureKeyboardJob {
|
||||
fn name(&self) -> &str { "job_keyboard" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
// Konsol (TTY) keymap adı: X11 layout'undan farklı olabilir.
|
||||
// Türkçe'de masaüstü "tr" çalışır ama vconsole keymap'i "trq" (Q) / "trf" (F) olmalı;
|
||||
// "tr" değeri kbd'de hata verir.
|
||||
let keymap = match (self.layout.as_str(), self.variant.as_str()) {
|
||||
("tr", "") => "trq".to_string(),
|
||||
("tr", "f") => "trf".to_string(),
|
||||
_ => {
|
||||
if self.variant.is_empty() {
|
||||
self.layout.clone()
|
||||
} else {
|
||||
format!("{}-{}", self.layout, self.variant)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// /etc/vconsole.conf (systemd-style TTY)
|
||||
let vconsole_path = format!("{}/etc/vconsole.conf", self.mount);
|
||||
let vconsole = format!("KEYMAP={}\nFONT=ter-v16n\n", keymap);
|
||||
ui.log(format!("Yazılıyor: {}", vconsole_path));
|
||||
let _ = tokio::fs::write(&vconsole_path, vconsole).await;
|
||||
|
||||
// /etc/conf.d/mudur (PisiLinux native init)
|
||||
let mudur_dir = format!("{}/etc/conf.d", self.mount);
|
||||
let mudur_path = format!("{}/mudur", mudur_dir);
|
||||
tokio::fs::create_dir_all(&mudur_dir).await
|
||||
.map_err(|e| format!("{} oluşturulamadı: {}", mudur_dir, e))?;
|
||||
|
||||
let content = tokio::fs::read_to_string(&mudur_path).await.unwrap_or_default();
|
||||
let mut lines: Vec<String> = content.lines().map(String::from).collect();
|
||||
let mut found_keymap = false;
|
||||
let mut found_language = false;
|
||||
|
||||
for line in lines.iter_mut() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("keymap=") || trimmed.starts_with("#keymap=") {
|
||||
*line = format!("keymap=\"{}\"", keymap);
|
||||
found_keymap = true;
|
||||
} else if trimmed.starts_with("language=") || trimmed.starts_with("#language=") {
|
||||
*line = format!("language=\"{}\"", self.language);
|
||||
found_language = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_keymap {
|
||||
lines.push(format!("keymap=\"{}\"", keymap));
|
||||
}
|
||||
if !found_language {
|
||||
lines.push(format!("language=\"{}\"", self.language));
|
||||
}
|
||||
|
||||
let result = lines.join("\n");
|
||||
ui.log(format!("Yazılıyor: {}", mudur_path));
|
||||
tokio::fs::write(&mudur_path, result).await
|
||||
.map_err(|e| format!("{} yazılamadı: {}", mudur_path, e))?;
|
||||
|
||||
// /etc/X11/xorg.conf.d/00-keyboard.conf (X11 için)
|
||||
let x11_dir = format!("{}/etc/X11/xorg.conf.d", self.mount);
|
||||
let x11_path = format!("{}/00-keyboard.conf", x11_dir);
|
||||
tokio::fs::create_dir_all(&x11_dir).await
|
||||
.map_err(|e| format!("{} oluşturulamadı: {}", x11_dir, e))?;
|
||||
|
||||
let options_line = if self.variant.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n\t\tOption \"XkbVariant\" \"{}\"", self.variant)
|
||||
};
|
||||
|
||||
let x11_conf = format!(
|
||||
"Section \"InputClass\"\n\
|
||||
\tIdentifier \"system-keyboard\"\n\
|
||||
\tMatchIsKeyboard \"on\"\n\
|
||||
\tOption \"XkbLayout\" \"{layout}\"{options}\n\
|
||||
EndSection\n",
|
||||
layout = self.layout,
|
||||
options = options_line,
|
||||
);
|
||||
ui.log(format!("Yazılıyor: {}", x11_path));
|
||||
tokio::fs::write(&x11_path, x11_conf)
|
||||
.await
|
||||
.map_err(|e| format!("xorg keyboard conf yazılamadı: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use super::{run_chroot, Job, UiSender};
|
||||
|
||||
/// /etc/locale.conf ve /etc/locale.gen'i yazar, sonra locale-gen çalıştırır.
|
||||
pub struct ConfigureLocaleJob {
|
||||
pub mount: String,
|
||||
pub locale: String, // ör. "tr_TR.UTF-8"
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for ConfigureLocaleJob {
|
||||
fn name(&self) -> &str { "job_locale" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
// Locale değeri doğrulama: yalnızca alfanumerik, '.', '_', '-', '@'
|
||||
// karakterlerine izin ver. Yol manipülasyonu ve injection önlenir.
|
||||
let locale_valid = !self.locale.is_empty()
|
||||
&& !self.locale.contains("..")
|
||||
&& !self.locale.contains('/')
|
||||
&& !self.locale.contains('\n')
|
||||
&& self.locale.chars().all(|c| c.is_alphanumeric() || "._-@".contains(c));
|
||||
if !locale_valid {
|
||||
return Err(format!("Geçersiz locale değeri: '{}'", self.locale));
|
||||
}
|
||||
|
||||
let etc_dir = format!("{}/etc", self.mount);
|
||||
let _ = tokio::fs::create_dir_all(&etc_dir).await;
|
||||
let _ = tokio::fs::create_dir_all(format!("{}/etc/env.d", self.mount)).await;
|
||||
|
||||
// /etc/locale.conf (systemd-style)
|
||||
let conf_path = format!("{}/etc/locale.conf", self.mount);
|
||||
let conf = format!(
|
||||
"LANG={locale}\nLC_ALL={locale}\n",
|
||||
locale = self.locale
|
||||
);
|
||||
ui.log(format!("Yazılıyor: {}", conf_path));
|
||||
let _ = tokio::fs::write(&conf_path, conf).await;
|
||||
|
||||
// /etc/env.d/03locale (PisiLinux native)
|
||||
let pisi_locale_path = format!("{}/etc/env.d/03locale", self.mount);
|
||||
let pisi_locale = format!(
|
||||
"LANG={locale}\nLC_ALL={locale}\n",
|
||||
locale = self.locale
|
||||
);
|
||||
ui.log(format!("Yazılıyor: {}", pisi_locale_path));
|
||||
let _ = tokio::fs::write(&pisi_locale_path, pisi_locale).await;
|
||||
|
||||
// /etc/locale.gen — satırın başındaki # kaldırılır
|
||||
let gen_path = format!("{}/etc/locale.gen", self.mount);
|
||||
ui.log(format!("Güncelleniyor: {}", gen_path));
|
||||
let content = tokio::fs::read_to_string(&gen_path).await.unwrap_or_default();
|
||||
// Her zaman aktif olması gereken yedek yerel ayarlar
|
||||
let fallback_locales = ["en_US.UTF-8", "C.UTF-8"];
|
||||
let mut updated: Vec<String> = content
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let stripped = line.trim_start_matches('#').trim_start();
|
||||
if stripped.starts_with(&self.locale)
|
||||
|| fallback_locales.iter().any(|l| stripped.starts_with(l))
|
||||
{
|
||||
stripped.to_string() // yorumu kaldır
|
||||
} else {
|
||||
line.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Yedek yerel ayarlar dosyada yoksa sona ekle
|
||||
for loc in fallback_locales {
|
||||
let present = updated.iter().any(|line| {
|
||||
line.trim_start_matches('#').trim_start().starts_with(loc)
|
||||
});
|
||||
if !present {
|
||||
updated.push(format!("{} UTF-8", loc));
|
||||
}
|
||||
}
|
||||
|
||||
let content_to_write = updated.join("\n") + "\n";
|
||||
|
||||
tokio::fs::write(&gen_path, content_to_write)
|
||||
.await
|
||||
.map_err(|e| format!("/etc/locale.gen yazılamadı: {}", e))?;
|
||||
|
||||
// locale.gen çalıştır — kurulum sonrası locale desteği için zorunlu
|
||||
ui.log("locale.gen çalıştırılıyor…");
|
||||
run_chroot(&self.mount, &["locale-gen"]).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Zaman dilimini ayarlar ve hwclock'u senkronize eder.
|
||||
pub struct ConfigureTimezoneJob {
|
||||
pub mount: String,
|
||||
pub timezone: String, // ör. "Europe/Istanbul"
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for ConfigureTimezoneJob {
|
||||
fn name(&self) -> &str { "job_timezone" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
// Path traversal koruması: timezone değeri yalnızca alfanumerik,
|
||||
// '/', '_', '-', '+' karakterlerine izin verir; ".." asla geçemez.
|
||||
let tz = &self.timezone;
|
||||
let tz_valid = !tz.is_empty()
|
||||
&& !tz.contains("..")
|
||||
&& tz.chars().all(|c| c.is_alphanumeric() || "/._-+".contains(c));
|
||||
if !tz_valid {
|
||||
return Err(format!("Geçersiz timezone değeri: '{}'", tz));
|
||||
}
|
||||
|
||||
// Sembolik link: /etc/localtime → /usr/share/zoneinfo/<tz>
|
||||
let link_target = format!("/usr/share/zoneinfo/{}", self.timezone);
|
||||
let link_path = format!("{}/etc/localtime", self.mount);
|
||||
|
||||
ui.log(format!("ln -sf {} {}", link_target, link_path));
|
||||
// Önce varsa eski linki sil
|
||||
let _ = tokio::fs::remove_file(&link_path).await;
|
||||
// tokio'da Unix symlink için spawn_blocking kullan
|
||||
let lt = link_target.clone();
|
||||
let lp = link_path.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
std::os::unix::fs::symlink(<, &lp)
|
||||
}).await
|
||||
.map_err(|e| format!("symlink spawn hatası: {}", e))?
|
||||
.map_err(|e| format!("localtime symlink: {}", e))?;
|
||||
|
||||
// hwclock --systohc
|
||||
ui.log("hwclock --systohc çalıştırılıyor…");
|
||||
// Hata kritik değil, uyar ve devam et
|
||||
match run_chroot(&self.mount, &["hwclock", "--systohc"]).await {
|
||||
Ok(_) => ui.log("hwclock senkronize edildi."),
|
||||
Err(e) => ui.log(format!("⚠ hwclock uyarısı (kritik değil): {}", e)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// NTP ve manuel tarih/saat yapılandırması.
|
||||
pub struct ConfigureDateTimeJob {
|
||||
pub mount: String,
|
||||
pub use_ntp: bool,
|
||||
pub year: i32,
|
||||
pub month: u32,
|
||||
pub day: u32,
|
||||
pub hour: u32,
|
||||
pub minute: u32,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for ConfigureDateTimeJob {
|
||||
fn name(&self) -> &str { "job_datetime" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
if self.use_ntp {
|
||||
ui.log("NTP etkinleştiriliyor…");
|
||||
// ntpd servisini başlat
|
||||
let _ = run_chroot(&self.mount, &["service", "ntpd", "start"]).await;
|
||||
} else {
|
||||
ui.log("Manuel tarih/saat ayarlanıyor…");
|
||||
let date_str = format!(
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}:00",
|
||||
self.year, self.month, self.day, self.hour, self.minute
|
||||
);
|
||||
let _ = run_chroot(&self.mount, &["date", "-s", &date_str]).await;
|
||||
let _ = run_chroot(&self.mount, &["hwclock", "--systohc"]).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use super::{run_chroot, Job, UiSender};
|
||||
|
||||
/// Ek paket gruplarını chroot içinde kurar.
|
||||
pub struct InstallExtraPackagesJob {
|
||||
pub mount: String,
|
||||
pub package_groups: Vec<String>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for InstallExtraPackagesJob {
|
||||
fn name(&self) -> &str { "job_netinstall" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
use crate::steps::netinstall::PACKAGE_GROUPS;
|
||||
|
||||
let mut packages: Vec<&str> = Vec::new();
|
||||
for (key, _, pkgs) in PACKAGE_GROUPS {
|
||||
if self.package_groups.contains(&key.to_string()) {
|
||||
packages.extend_from_slice(pkgs);
|
||||
}
|
||||
}
|
||||
|
||||
if packages.is_empty() {
|
||||
ui.log("Ek paket seçilmedi, atlanıyor.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pkg_list = packages.join(" ");
|
||||
ui.log(format!("Ek paketler kuruluyor: {}", pkg_list));
|
||||
match run_chroot(&self.mount, &["pisi", "install", &pkg_list]).await {
|
||||
Ok(_) => ui.log("✓ Ek paketler kuruldu"),
|
||||
Err(e) => {
|
||||
ui.log(format!("⚠ pisi install uyarısı: {}", e));
|
||||
ui.log("⚠ Bu hata kritik değilse kurulum devam edecek.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use super::{Job, UiSender};
|
||||
|
||||
/// /etc/hostname ve /etc/hosts dosyalarını yazar.
|
||||
pub struct ConfigureHostnameJob {
|
||||
pub mount: String,
|
||||
pub hostname: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for ConfigureHostnameJob {
|
||||
fn name(&self) -> &str { "job_hostname" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
let etc_dir = format!("{}/etc", self.mount);
|
||||
let _ = tokio::fs::create_dir_all(&etc_dir).await;
|
||||
|
||||
// /etc/hostname
|
||||
let hostname_path = format!("{}/etc/hostname", self.mount);
|
||||
ui.log(format!("Yazılıyor: {}", hostname_path));
|
||||
tokio::fs::write(&hostname_path, format!("{}\n", self.hostname))
|
||||
.await
|
||||
.map_err(|e| format!("/etc/hostname yazılamadı: {}", e))?;
|
||||
|
||||
// /etc/hosts
|
||||
let hosts_path = format!("{}/etc/hosts", self.mount);
|
||||
let hosts_content = format!(
|
||||
"127.0.0.1\tlocalhost\n\
|
||||
::1\t\tlocalhost\n\
|
||||
127.0.1.1\t{hostname}\n",
|
||||
hostname = self.hostname
|
||||
);
|
||||
ui.log(format!("Yazılıyor: {}", hosts_path));
|
||||
tokio::fs::write(&hosts_path, hosts_content)
|
||||
.await
|
||||
.map_err(|e| format!("/etc/hosts yazılamadı: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
use super::{run_chroot, run_cmd, Job, UiSender};
|
||||
|
||||
/// Kullanıcı hesabı oluşturur ve şifresini ayarlar.
|
||||
pub struct CreateUserJob {
|
||||
pub mount: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub root_password: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Job for CreateUserJob {
|
||||
fn name(&self) -> &str { "job_user" }
|
||||
|
||||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||||
// wheel grubunun varlığını garantile (PisiLinux'ta olmayabilir)
|
||||
let group_file = format!("{}/etc/group", self.mount);
|
||||
let group_content = tokio::fs::read_to_string(&group_file)
|
||||
.await.unwrap_or_default();
|
||||
if !group_content.lines().any(|l| l.starts_with("wheel:")) {
|
||||
ui.log("wheel grubu oluşturuluyor…");
|
||||
run_chroot(&self.mount, &["groupadd", "-r", "wheel"]).await
|
||||
.unwrap_or_default(); // hata olsa bile devam et
|
||||
}
|
||||
|
||||
// useradd: kullanıcıyı temel gruplarla oluştur
|
||||
// PisiLinux'ta "storage" grubu yoksa useradd hata verir; -M flag ile grup yoksa atla
|
||||
// Güvenli yöntem: önce her grubu tek tek ekle
|
||||
let groups = ["wheel", "users", "pnp", "disk", "audio", "video", "power", "dialout", "lp", "lpadmin", "cdrom", "floppy"];
|
||||
for grp in &groups {
|
||||
if group_content.lines().any(|l| l.starts_with(&format!("{}:", grp))) {
|
||||
// grup zaten var
|
||||
} else {
|
||||
// grup yoksa oluşturma (hata görmezden gel)
|
||||
let _ = run_chroot(&self.mount, &["groupadd", grp]).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Kullanıcıyı oluştur
|
||||
let groups_str = groups.join(",");
|
||||
ui.log(format!("useradd -m -G {} -s /bin/bash {}", groups_str, self.username));
|
||||
run_chroot(&self.mount, &[
|
||||
"useradd",
|
||||
"-m",
|
||||
"-G", &groups_str,
|
||||
"-s", "/bin/bash",
|
||||
&self.username,
|
||||
]).await?;
|
||||
|
||||
// Kullanıcı şifresi: `chpasswd` stdin üzerinden.
|
||||
// `sh -c "echo 'user:pass' | ..."` KULLANILMAZ: parola düz metin
|
||||
// olarak kabuğun argv'sine geçer ve /proc/<pid>/cmdline üzerinden
|
||||
// okunabilir; ayrıca `'` veya `$(...)` içeren parola komutu bozar.
|
||||
// Aynı desen jobs/partition.rs::run_luks_format'ta da kullanılıyor.
|
||||
ui.log(format!("Şifre ayarlanıyor: {}", self.username));
|
||||
set_password_via_chpasswd(&self.mount, &self.username, &self.password).await?;
|
||||
|
||||
// Root şifresi: belirlenmişse ayarla, yoksa kilitle
|
||||
if self.root_password.is_empty() {
|
||||
ui.log("Root hesabı kilitleniyor…");
|
||||
run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
|
||||
} else {
|
||||
ui.log("Root şifresi ayarlanıyor…");
|
||||
set_password_via_chpasswd(&self.mount, "root", &self.root_password).await?;
|
||||
}
|
||||
|
||||
// sudoers: wheel grubu sudo yetkisi alsın
|
||||
let sudoers_dir = format!("{}/etc/sudoers.d", self.mount);
|
||||
tokio::fs::create_dir_all(&sudoers_dir).await
|
||||
.map_err(|e| format!("{} oluşturulamadı: {}", sudoers_dir, e))?;
|
||||
let sudoers_path = format!("{}/wheel", sudoers_dir);
|
||||
ui.log(format!("Yazılıyor: {}", sudoers_path));
|
||||
tokio::fs::write(&sudoers_path, "%wheel ALL=(ALL:ALL) ALL\n")
|
||||
.await
|
||||
.map_err(|e| format!("sudoers yazılamadı: {}", e))?;
|
||||
|
||||
// Dosya izinleri: sudoers 440 olmalı
|
||||
run_cmd("chmod", &["440", &sudoers_path]).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// `chroot <mount> chpasswd -c SHA512` komutuna `kullanici:sifre` bilgisini
|
||||
/// **stdin pipe'ı** üzerinden iletir.
|
||||
///
|
||||
/// Neden `sh -c` + `format!` değil:
|
||||
/// 1. Parola kabuğun argv'sine yazılırsa `/proc/<pid>/cmdline` üzerinden
|
||||
/// canlı sistemdeki herhangi bir süreç tarafından okunabilir.
|
||||
/// 2. Parola içindeki `'` veya `$(...)` karakterleri komutu bozar ve
|
||||
/// shell injection'a yol açar.
|
||||
/// 3. `format!` ile üretilen komut `user:pass` içinde `:` geçse hatalı
|
||||
/// ayrıştırılır.
|
||||
///
|
||||
/// Bu desen `jobs/partition.rs::run_luks_format` ile birebir aynıdır.
|
||||
async fn set_password_via_chpasswd(
|
||||
mount: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), String> {
|
||||
if super::DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
let mut child = tokio::process::Command::new("chroot")
|
||||
.args([mount, "chpasswd", "-c", "SHA512"])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("chpasswd başlatılamadı: {}", e))?;
|
||||
|
||||
// Parolayı yalnızca çocuk sürecin stdin'ine yaz; argv'de hiç görünmez.
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
// chpasswd "user:pass" satırı bekler ve satır sonu ile ayırır.
|
||||
let mut data = format!("{}:{}\n", username, password).into_bytes();
|
||||
if data.last() != Some(&b'\n') {
|
||||
data.push(b'\n');
|
||||
}
|
||||
let result = stdin.write_all(&data).await;
|
||||
// Yazdıktan sonra parolanın kopyasını bellekte bırakma.
|
||||
data.iter_mut().for_each(|b| *b = 0);
|
||||
drop(stdin);
|
||||
result.map_err(|e| format!("chpasswd stdin yazma hatası: {}", e))?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output().await
|
||||
.map_err(|e| format!("chpasswd bekleme hatası: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
if err.is_empty() {
|
||||
Err(format!("{} için şifre ayarlanamadı", username))
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,24 @@ fn main() -> eframe::Result<()> {
|
||||
/// Grafik arayüzü başlatır (normal mod).
|
||||
fn run_gui(demo_mode: bool) -> eframe::Result<()> {
|
||||
|
||||
let icon_data = {
|
||||
let img = image::load_from_memory(include_bytes!("../assets/icons/YALI_icon_DARK.png"))
|
||||
.expect("YALI_icon_DARK.png yüklenemedi")
|
||||
.into_rgba8();
|
||||
let (w, h) = img.dimensions();
|
||||
egui::IconData {
|
||||
width: w,
|
||||
height: h,
|
||||
rgba: img.into_raw(),
|
||||
}
|
||||
};
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default()
|
||||
.with_inner_size([1200.0, 900.0])
|
||||
.with_min_inner_size([1080.0, 720.0])
|
||||
.with_title(t!("installer")),
|
||||
.with_title(t!("installer"))
|
||||
.with_icon(icon_data),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -115,6 +128,7 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
|
||||
let timezone = af.install.timezone.clone();
|
||||
let username = af.user.username.clone();
|
||||
let password = af.user.password.clone();
|
||||
let root_password = af.user.root_password.clone();
|
||||
let hostname = af.user.hostname.clone();
|
||||
let kb_layout = af.install.keyboard.clone();
|
||||
let kb_variant = af.install.keyboard_variant.clone();
|
||||
@@ -122,6 +136,16 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
|
||||
let mount = af.install.mount.clone();
|
||||
let plan = state.partition_plan.unwrap();
|
||||
|
||||
// Squashfs kurulumu: oturum yöneticisi ve masaüstü ortamı ISO'dan
|
||||
// gelir. `apply_to_state` bunları GlobalState'in canlı ortam
|
||||
// tespitine bırakıyor; burada sabit yazmıyoruz.
|
||||
let display_manager = state.display_manager.clone();
|
||||
let desktop_environment = state.desktop_environment.clone();
|
||||
let autologin = af.system.autologin;
|
||||
let boot_timeout = af.system.bootloader_timeout;
|
||||
let boot_password = af.system.bootloader_password.clone();
|
||||
let kernel_options = af.system.kernel_options.clone();
|
||||
|
||||
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
|
||||
rt.block_on(async {
|
||||
use std::sync::mpsc;
|
||||
@@ -134,6 +158,7 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
|
||||
let cli_ctx = eframe::egui::Context::default();
|
||||
let ui_sender = UiSender::new(tx, cli_ctx);
|
||||
|
||||
let language_c = if locale.starts_with("tr") { "tr".to_string() } else { "en".to_string() };
|
||||
let plan_c = plan.clone();
|
||||
let locale_c = locale.clone();
|
||||
let timezone_c = timezone.clone();
|
||||
@@ -152,6 +177,7 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
|
||||
jobs::JobQueueConfig {
|
||||
mount: mount_c,
|
||||
source: source_c,
|
||||
language: language_c,
|
||||
locale: locale_c,
|
||||
timezone: timezone_c,
|
||||
username: username_c,
|
||||
@@ -161,14 +187,16 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
|
||||
kb_variant: kb_variant_c,
|
||||
demo_mode,
|
||||
boot_device: String::new(),
|
||||
bootloader_timeout: 5,
|
||||
bootloader_password: String::new(),
|
||||
kernel_options: String::new(),
|
||||
bootloader_timeout: boot_timeout,
|
||||
bootloader_password: boot_password,
|
||||
kernel_options: kernel_options,
|
||||
use_ntp: true,
|
||||
root_password: String::new(),
|
||||
display_manager: "sddm".to_string(),
|
||||
autologin: true,
|
||||
desktop_environment: "plasma".to_string(),
|
||||
root_password: root_password,
|
||||
// Tespit edilmiş değerler (ISO'nun kendisi) — sabit
|
||||
// yazılmıyor.
|
||||
display_manager,
|
||||
autologin,
|
||||
desktop_environment,
|
||||
selected_package_groups: Vec::new(),
|
||||
manual_year: 2026,
|
||||
manual_month: 5,
|
||||
@@ -188,8 +216,15 @@ fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
|
||||
print!("\r {}: [{:50}] {:.0}%", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
}
|
||||
Ok(InstallMessage::SubProgress(_)) => {}
|
||||
Ok(InstallMessage::CurrentFile(_)) => {}
|
||||
Ok(InstallMessage::SubProgress(p)) => {
|
||||
print!("\r {}: [{:50}] {:.0}% ", t!("progress"), "=".repeat((p * 50.0) as usize), p * 100.0);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
}
|
||||
Ok(InstallMessage::CopyStarted) | Ok(InstallMessage::CopyFinished) => {}
|
||||
Ok(InstallMessage::CurrentFile(f)) => {
|
||||
print!("\r {}: {} ", t!("install_current_file"), f);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
}
|
||||
Ok(InstallMessage::Done) => {
|
||||
println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description"));
|
||||
break;
|
||||
@@ -223,6 +258,19 @@ impl Default for YaliApp {
|
||||
let branding = branding::BrandingConfig::load_for_lang(&default_lang);
|
||||
let mut app = Self {
|
||||
state: GlobalState::default(),
|
||||
// Sihirbaz akışı: yalı KURULU SİSTEMİ squashfs'ten diske
|
||||
// kopyalar (CopyFilesJob rsync). Bu yüzden ISO'da hangi
|
||||
// paketler, ekran yöneticisi ve masaüstü ortamı varsa
|
||||
// kurulum BUNLARI ZATEN İÇERİR.
|
||||
//
|
||||
// Bu yüzden LicenseStep, DisplayManagerStep ve NetinstallStep
|
||||
// bilinçli olarak listede DEĞİL:
|
||||
// - lisans kabulü bu akışta anlam taşımıyor
|
||||
// - ekran yöneticisi ISO'da pişmiş geliyor
|
||||
// - ağdan ek paket kurmak, diski squashfs'ten kopyalamayla
|
||||
// çelişir; netinstall ayrı bir proje olarak ileride
|
||||
// devreye alınacak
|
||||
// Adımlar silinmedi — mod.rs'te tanımlı ve hazır duruyor.
|
||||
steps: vec![
|
||||
Box::new(WelcomeStep::default()),
|
||||
Box::new(LocationStep::default()),
|
||||
@@ -264,11 +312,6 @@ impl Default for YaliApp {
|
||||
app.state.branding_yali_light = branding.yali_logo_uri();
|
||||
app.state.branding_pisi_dark = branding.pisi_logo_dark_uri();
|
||||
app.state.branding_pisi_light = branding.pisi_logo_light_uri();
|
||||
app.state.branding_icons = branding.slides.icons
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), branding::path_to_file_uri(v)))
|
||||
.collect();
|
||||
app.state.branding_slides = branding.slides.items.clone();
|
||||
|
||||
app.steps[0].on_enter(&mut app.state);
|
||||
app.last_step = 0;
|
||||
@@ -278,7 +321,7 @@ impl Default for YaliApp {
|
||||
|
||||
impl eframe::App for YaliApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
rust_i18n::set_locale(&self.state.language);
|
||||
rust_i18n::set_locale(crate::funct::sanitize_locale(&self.state.language));
|
||||
|
||||
// Dil değişimi kontrolü -> branding dosyasını dinamik olarak yeniden yükle
|
||||
if self.loaded_language != self.state.language {
|
||||
@@ -289,11 +332,6 @@ impl eframe::App for YaliApp {
|
||||
self.state.branding_yali_light = branding.yali_logo_uri();
|
||||
self.state.branding_pisi_dark = branding.pisi_logo_dark_uri();
|
||||
self.state.branding_pisi_light = branding.pisi_logo_light_uri();
|
||||
self.state.branding_icons = branding.slides.icons
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), branding::path_to_file_uri(v)))
|
||||
.collect();
|
||||
self.state.branding_slides = branding.slides.items.clone();
|
||||
|
||||
// Pencere başlığını dinamik olarak güncelle
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Title(self.branding.general.title.clone()));
|
||||
@@ -310,19 +348,19 @@ impl eframe::App for YaliApp {
|
||||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||
rescue.show(ui, &mut self.state);
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
if ui.button(t!("rescue_return")).clicked() {
|
||||
self.state.rescue_mode = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let cur = self.state.current_step;
|
||||
let n_steps = self.steps.len();
|
||||
let is_exec = cur == n_steps - 2;
|
||||
let is_finish = cur == n_steps - 1;
|
||||
// Rol artık konumsal tahminle değil, adımın kendi bildirdiği
|
||||
// `role()` ile belirleniyor. Sihirbaza yeni adım eklemek rol
|
||||
// algısını bozmaz (bkz. StepRole).
|
||||
let role = self.steps.get(cur).map(|s| s.role())
|
||||
.unwrap_or(installer::StepRole::Normal);
|
||||
let is_exec = role == installer::StepRole::Execution;
|
||||
let is_finish = role == installer::StepRole::Finish;
|
||||
|
||||
// Adım değişmişse on_enter çağır
|
||||
if cur != self.last_step {
|
||||
@@ -432,7 +470,7 @@ impl eframe::App for YaliApp {
|
||||
egui::TopBottomPanel::bottom("footer")
|
||||
.frame(egui::Frame::none()
|
||||
.fill(ui::theme::c_footer_bg())
|
||||
.stroke(egui::Stroke::new(1.0, ui::theme::c_footer_bg())))
|
||||
.stroke(egui::Stroke::new(1.0_f32, ui::theme::c_footer_bg())))
|
||||
.show(ctx, |ui| {
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
@@ -501,6 +539,7 @@ impl eframe::App for YaliApp {
|
||||
.inner_margin(egui::Margin::same(20.0)))
|
||||
.show(ctx, |ui| {
|
||||
egui::ScrollArea::vertical()
|
||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
|
||||
.id_source("main_scroll")
|
||||
.show(ui, |ui| {
|
||||
if let Some(step) = self.steps.get_mut(cur) {
|
||||
@@ -509,4 +548,91 @@ impl eframe::App for YaliApp {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use installer::StepRole;
|
||||
|
||||
/// Sihirbazın adım dizisi. main.rs::YaliApp::default() ile AYNI
|
||||
/// olmalı — test bu yüzden diziyi yeniden kuruyor.
|
||||
fn wizard() -> Vec<Box<dyn InstallerStep>> {
|
||||
vec![
|
||||
Box::new(WelcomeStep::default()),
|
||||
Box::new(LocationStep::default()),
|
||||
Box::new(KeyboardStep::default()),
|
||||
Box::new(NetworkStep::default()),
|
||||
Box::new(PartitionStep::default()),
|
||||
Box::new(UsersStep::default()),
|
||||
Box::new(BootloaderStep::default()),
|
||||
Box::new(SummaryStep),
|
||||
Box::new(ExecutionStep::default()),
|
||||
Box::new(FinishStep),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wizard_has_one_execution_and_one_finish_step() {
|
||||
let steps = wizard();
|
||||
assert_eq!(
|
||||
steps.iter().filter(|s| s.role() == StepRole::Execution).count(),
|
||||
1,
|
||||
"tam olarak bir ExecutionStep olmalı"
|
||||
);
|
||||
assert_eq!(
|
||||
steps.iter().filter(|s| s.role() == StepRole::Finish).count(),
|
||||
1,
|
||||
"tam olarak bir FinishStep olmalı"
|
||||
);
|
||||
}
|
||||
|
||||
/// Konumsal tespit yerine role() kullanıldığı için, Execution ve
|
||||
/// Finish listenin SONU olmak zorunda değil. Bu test yine de
|
||||
/// beklenen sırayı sabitler; bir adım yanlış yere eklendiğinde
|
||||
/// (ör. Finish'ten sonra yeni bir adım) kırılır.
|
||||
#[test]
|
||||
fn execution_and_finish_are_the_last_two_steps() {
|
||||
let steps = wizard();
|
||||
let n = steps.len();
|
||||
assert_eq!(steps[n - 2].role(), StepRole::Execution);
|
||||
assert_eq!(steps[n - 1].role(), StepRole::Finish);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_ordinary_steps_precede_the_execution_step() {
|
||||
let steps = wizard();
|
||||
let first_exec = steps.iter().position(|s| s.role() == StepRole::Execution)
|
||||
.expect("ExecutionStep bulunmalı");
|
||||
assert!(
|
||||
steps[..first_exec].iter().all(|s| s.role() == StepRole::Normal),
|
||||
"ExecutionStep'tan önceki tüm adımlar Normal olmalı"
|
||||
);
|
||||
}
|
||||
|
||||
/// Yeni bir adım eklemek rol algısını bozmamalı. Bu, 22de7d5
|
||||
/// sonrası yaşanan regresyon sınıfının tersidir: orada liste
|
||||
/// değişti ve konumsal `cur == n_steps - 2` tespiti sessizce
|
||||
/// kaydı.
|
||||
///
|
||||
/// Örnek olarak `NetinstallStep` kullanıldı: bu adım squashfs
|
||||
/// kurulumunda bilinçli olarak listede değil, ama netinstall
|
||||
/// projesi başladığında aynen buraya eklenebilmeli.
|
||||
#[test]
|
||||
fn roles_survive_inserting_a_step_before_execution() {
|
||||
let mut steps = wizard();
|
||||
let exec_at = steps.iter().position(|s| s.role() == StepRole::Execution).unwrap();
|
||||
|
||||
// Yeni bir Normal adım ekle
|
||||
steps.insert(exec_at, Box::new(steps::netinstall::NetinstallStep));
|
||||
let n = steps.len();
|
||||
|
||||
// Hâlâ doğru adımlar son ikide mi?
|
||||
assert_eq!(steps[n - 2].role(), StepRole::Execution);
|
||||
assert_eq!(steps[n - 1].role(), StepRole::Finish);
|
||||
// Role() sayesinde Execution'ın KONUMU değil KENDİSİ bulunuyor:
|
||||
// konum bir kayar, ama Execution yine n-2'de.
|
||||
let exec_at2 = steps.iter().position(|s| s.role() == StepRole::Execution).unwrap();
|
||||
assert_eq!(exec_at2, exec_at + 1, "eklenen adım Execution'ı bir kaydırmalı");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,104 @@ use crate::installer::{GlobalState, InstallerStep};
|
||||
use crate::ui::theme;
|
||||
use crate::funct::{form_label, form_input_text, form_input_password};
|
||||
|
||||
/// Bir diskin bölüm sayısını `sysfs`'ten okur (alt süreç yok).
|
||||
///
|
||||
/// `lsblk` çalıştırmak kare başına olsaydı her karede süreç doğardu;
|
||||
/// `on_enter`'da bile gereksiz. sysfs'te her bölüm
|
||||
/// `/sys/block/<disk>/<disk><N>`, `…/start` dosyasıyla listelenir.
|
||||
fn disk_partition_count(disk: &str) -> usize {
|
||||
let base = crate::steps::partition::base_disk_name(disk);
|
||||
if base.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let dir = format!("/sys/block/{}", base);
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else { return 0 };
|
||||
rd.flatten().filter(|e| {
|
||||
let name = e.file_name().to_string_lossy().to_string();
|
||||
// Yalnızca "<base><rakam>" biçimindeki girdiler bölümdür.
|
||||
name.strip_prefix(&base)
|
||||
.map(|rest| !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()))
|
||||
.unwrap_or(false)
|
||||
}).count()
|
||||
}
|
||||
|
||||
/// Önyükleyici için varsayılan aygıtı seçer.
|
||||
///
|
||||
/// Kural: **KÖR diski seçme; zaten işletim sistemi kurulu olan diski
|
||||
/// seç.** Çünkü BIOS modunda `grub-install` seçilen diskin MBR'sine
|
||||
/// yazar ve MBR'yi okuyan aygıt firmware'in açılış sırasındaki ilk
|
||||
/// diski olmak ZORUNDA. Çift önyüklemeli bir makinede bu, mevcut
|
||||
/// işletim sisteminin diski — kökün kurulduğu disk DEĞİL.
|
||||
///
|
||||
/// Somut olarak: sda'da Windows, sdb'de PisiLinux kuruluyor. GRUB
|
||||
/// sdb'nin MBR'sine yazılırsa firmware sdb'yi açmaz, PisiLinux hiç
|
||||
/// görünmez. sda'nın MBR'sine yazılırsa firmware GRUB'a düşer, GRUB
|
||||
/// da ikisini de listeler.
|
||||
///
|
||||
/// "İşletim sistemi kurulu"nun en güvenilir ve en ucuz göstergesi:
|
||||
/// **kök diski dışında, hâlihazırda bölüm tablosu olan bir disk**
|
||||
/// (yani disk dolu değil, içinde tanım var).
|
||||
///
|
||||
/// Sıralama:
|
||||
/// 1. Çıkarılabilir aygıt (canlı ISO'nun USB'i) — ASLA hedef değil
|
||||
/// 2. Kök diski dışında bölümü olan ilk dahili disk → mevcut OS diski
|
||||
/// 3. Kök diski
|
||||
/// 4. Aktif düzenleme diski (`selected_disk`) — son çare
|
||||
///
|
||||
/// Tek diskli kurulumda 3. adım çalışır: başka disk yoktur, davranış
|
||||
/// kök diskiyle aynıdır.
|
||||
pub fn default_boot_device<R, F>(
|
||||
root_disk: Option<&str>,
|
||||
disks: &[crate::installer::DiskInfo],
|
||||
is_removable: R,
|
||||
has_partitions: F,
|
||||
) -> Option<String>
|
||||
where
|
||||
R: Fn(&str) -> bool,
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
// (1) çıkarılabilir aygıtlar ve (2) kök diski ele
|
||||
disks.iter()
|
||||
.filter(|d| !is_removable(&d.name))
|
||||
.filter(|d| Some(d.name.as_str()) != root_disk)
|
||||
.find(|d| has_partitions(&d.name))
|
||||
.map(|d| d.name.clone())
|
||||
.or_else(|| root_disk.map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct BootloaderStep {
|
||||
show_password: bool,
|
||||
timeout_str: String,
|
||||
}
|
||||
|
||||
/// Seçili aygıt, "zaten işletim sistemi olan disk" kuralıyla mı
|
||||
/// belirlendi? Değilse (kullanıcı elle değiştirdiyse ya da tek disk
|
||||
/// varsa) `None` döner ve ipucu gösterilmez.
|
||||
fn boot_device_reason(state: &GlobalState, current: &str) -> Option<String> {
|
||||
let root_disk = state
|
||||
.custom_partitions
|
||||
.iter()
|
||||
.find(|p| p.mountpoint == "/" && !p.delete)
|
||||
.map(|p| p.disk.clone())
|
||||
.or_else(|| state.partition_plan.as_ref().map(|p| p.disk.clone()));
|
||||
|
||||
// Aynı kuralı bir kez daha çalıştırıp sonucun aygıtla örtüştüğünü
|
||||
// doğruluyoruz. Kural tek kaynakta; burada yalnızca UI metni var.
|
||||
let expected = default_boot_device(
|
||||
root_disk.as_deref(),
|
||||
&state.available_disks,
|
||||
crate::steps::partition::is_removable,
|
||||
|d| disk_partition_count(d) > 0,
|
||||
)?;
|
||||
|
||||
if expected == current {
|
||||
Some(t!("bootloader_device_why_existing").to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallerStep for BootloaderStep {
|
||||
fn name(&self) -> String {
|
||||
t!("bootloader").to_string()
|
||||
@@ -19,9 +111,33 @@ impl InstallerStep for BootloaderStep {
|
||||
if self.timeout_str.is_empty() {
|
||||
self.timeout_str = state.bootloader_timeout.to_string();
|
||||
}
|
||||
// Varsayılan GRUB aygıtı: zaten işletim sistemi kurulu olan
|
||||
// disk. Detaylı gerekçe `default_boot_device` yorumunda.
|
||||
//
|
||||
// Önceki davranışlar ve nedenleri:
|
||||
// - `selected_disk` → manuel modda "aktif düzenleme diski";
|
||||
// /home eklemek için sdb'ye geçince GRUB sessizce oraya
|
||||
// gidiyordu.
|
||||
// - yalnızca kök diski → çift önyüklemeli makinede firmware'in
|
||||
// açmadığı diske MBR yazılıyor, PisiLinux hiç görünmüyor.
|
||||
if state.bootloader_device.is_empty() {
|
||||
if let Some(ref disk) = state.selected_disk {
|
||||
state.bootloader_device = disk.clone();
|
||||
let root_disk = state
|
||||
.custom_partitions
|
||||
.iter()
|
||||
.find(|p| p.mountpoint == "/" && !p.delete)
|
||||
.map(|p| p.disk.clone())
|
||||
.or_else(|| state.partition_plan.as_ref().map(|p| p.disk.clone()));
|
||||
|
||||
let chosen = default_boot_device(
|
||||
root_disk.as_deref(),
|
||||
&state.available_disks,
|
||||
crate::steps::partition::is_removable,
|
||||
|d| disk_partition_count(d) > 0,
|
||||
)
|
||||
.or_else(|| state.selected_disk.clone());
|
||||
|
||||
if let Some(d) = chosen {
|
||||
state.bootloader_device = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,9 +164,25 @@ impl InstallerStep for BootloaderStep {
|
||||
// Hedef aygıt
|
||||
form_label(ui, &t!("bootloader_device_label"));
|
||||
ui.horizontal(|ui| {
|
||||
// ⚠️ ÇIKARILABİLİR AYGITLAR LİSTEDE YOK.
|
||||
//
|
||||
// Canlı ISO'yu boot ettiğiniz USB `available_disks`
|
||||
// içinde yer alıyordu. Seçilirse BIOS modunda
|
||||
// `grub-install --target=i386-pc <usb>` MBR'yi o
|
||||
// aygıta yazar, kurulum biter ama sistem AÇILMAZ.
|
||||
//
|
||||
// Liste dolduysa (tamamen çıkarılabilir aygıt
|
||||
// görünürse) mevcut değer yine de gösterilir ki
|
||||
// ComboBox boş kalmasın.
|
||||
let mut device_list: Vec<String> = state.available_disks.iter()
|
||||
.filter(|d| !crate::steps::partition::is_removable(&d.name))
|
||||
.map(|d| d.name.clone())
|
||||
.collect();
|
||||
if device_list.is_empty() {
|
||||
device_list = state.available_disks.iter()
|
||||
.map(|d| d.name.clone())
|
||||
.collect();
|
||||
}
|
||||
if device_list.is_empty() {
|
||||
device_list.push(state.bootloader_device.clone());
|
||||
}
|
||||
@@ -65,6 +197,26 @@ impl InstallerStep for BootloaderStep {
|
||||
}
|
||||
});
|
||||
});
|
||||
// Varsayılanın NEDEN bu aygıt olduğunu söyle. Sessiz
|
||||
// bir varsayılan sürpriz yaratmasın: kullanıcı
|
||||
// nedenini anlamazsa dual-boot'ta yanlış aygıtta
|
||||
// kalır ve açılışta Windows görünmez.
|
||||
if let Some(why) = boot_device_reason(&state, &state.bootloader_device) {
|
||||
ui.label(
|
||||
egui::RichText::new(why)
|
||||
.size(11.0)
|
||||
.color(theme::c_accent()),
|
||||
);
|
||||
}
|
||||
// Çift önyükleme ipucu — kullanıcı bu adımda en çok
|
||||
// zorlanan kuralı burada öğreniyor.
|
||||
if std::path::Path::new("/sys/firmware/efi").exists() {
|
||||
ui.label(
|
||||
egui::RichText::new(t!("bootloader_device_hint_dualboot"))
|
||||
.size(11.0)
|
||||
.color(theme::c_accent()),
|
||||
);
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new(t!("bootloader_device_hint"))
|
||||
.size(11.0)
|
||||
@@ -107,3 +259,128 @@ impl InstallerStep for BootloaderStep {
|
||||
!state.bootloader_device.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::default_boot_device;
|
||||
use crate::installer::DiskInfo;
|
||||
|
||||
fn d(name: &str) -> DiskInfo {
|
||||
DiskInfo {
|
||||
name: name.to_string(),
|
||||
vendor: String::new(),
|
||||
model: "TEST".to_string(),
|
||||
drive_type: String::new(),
|
||||
size_gb: 100,
|
||||
size_bytes: 100 * 1_073_741_824,
|
||||
}
|
||||
}
|
||||
|
||||
/// `has_partitions` verilen aygıtın bölümü var mı diye bakar
|
||||
/// (test ortamında gerçek sysfs yok, bu yüzden dışarıdan verilir).
|
||||
fn parts_on(disks_with_parts: &[&str]) -> impl Fn(&str) -> bool {
|
||||
let v: Vec<String> = disks_with_parts.iter().map(|s| s.to_string()).collect();
|
||||
move |name: &str| v.iter().any(|p| p == name)
|
||||
}
|
||||
|
||||
/// Test ortamında HİÇBİR aygıt çıkarılabilir sayılır.
|
||||
///
|
||||
/// ⚠️ Gerçek `is_removable` çağrılırsa testler ortama bağımlı
|
||||
/// olur: bu geliştirme makinesinde `/sys/block/sda/removable == 1`
|
||||
/// olduğu için "/dev/sda" kullanan testler çıkarılabilir sanılıp
|
||||
/// kırılıyordu. Testler **şansa** geçiyordu.
|
||||
fn never_removable(_name: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// ⭐ Kullanıcının senaryosu: sda'da Windows, sdb'de PisiLinux
|
||||
/// kuruluyor. GRUB sda'ya yazılmalı — çünkü firmware açılışta
|
||||
/// sda'yı okuyor. Kök diski (sdb) seçilirse PisiLinux hiç görünmez.
|
||||
#[test]
|
||||
fn picks_the_disk_that_already_has_an_os() {
|
||||
let disks = vec![d("/dev/sda"), d("/dev/sdb"), d("/dev/sdc")];
|
||||
let got = default_boot_device(Some("/dev/sdb"), &disks, never_removable, parts_on(&["/dev/sda"]));
|
||||
assert_eq!(
|
||||
got.as_deref(),
|
||||
Some("/dev/sda"),
|
||||
"mevcut işletim sistemi olan disk seçilmeli"
|
||||
);
|
||||
}
|
||||
|
||||
/// Tek diskli kurulumda davranış KÖRK diskiyle aynıdır — başka
|
||||
/// aday yoktur.
|
||||
#[test]
|
||||
fn single_disk_install_falls_back_to_the_root_disk() {
|
||||
let disks = vec![d("/dev/sda")];
|
||||
assert_eq!(
|
||||
default_boot_device(Some("/dev/sda"), &disks, never_removable, parts_on(&["/dev/sda"])).as_deref(),
|
||||
Some("/dev/sda")
|
||||
);
|
||||
}
|
||||
|
||||
/// Çok diskli ama HİÇBİRİ bölümlü değilse (tamamen boş makineler)
|
||||
/// kök disk seçilir.
|
||||
#[test]
|
||||
fn no_existing_os_anywhere_falls_back_to_root() {
|
||||
let disks = vec![d("/dev/sda"), d("/dev/sdb")];
|
||||
assert_eq!(
|
||||
default_boot_device(Some("/dev/sdb"), &disks, never_removable, parts_on(&[])).as_deref(),
|
||||
Some("/dev/sdb")
|
||||
);
|
||||
}
|
||||
|
||||
/// Kök diskin kendisi bölümlü olsa bile "mevcut OS" sayılmaz —
|
||||
/// kurulum oraya yapılıyor, oraya varsayılan yazmak eski davranış.
|
||||
#[test]
|
||||
fn root_disk_is_never_treated_as_the_existing_os() {
|
||||
let disks = vec![d("/dev/sda"), d("/dev/sdb")];
|
||||
// sda'da kurulum, sdb boş → sda
|
||||
assert_eq!(
|
||||
default_boot_device(Some("/dev/sda"), &disks, never_removable, parts_on(&["/dev/sda"])).as_deref(),
|
||||
Some("/dev/sda")
|
||||
);
|
||||
}
|
||||
|
||||
/// Birden çok aday varsa SIRALAMA önemlidir: liste zaten
|
||||
/// kararlı sıralıdır, ilki seçilir.
|
||||
#[test]
|
||||
fn first_candidate_in_sorted_order_wins() {
|
||||
let disks = vec![d("/dev/sda"), d("/dev/sdb"), d("/dev/sdc")];
|
||||
assert_eq!(
|
||||
default_boot_device(Some("/dev/sdd"), &disks, never_removable, parts_on(&["/dev/sdb", "/dev/sdc"])).as_deref(),
|
||||
Some("/dev/sdb"),
|
||||
"sıralı listedeki ilk bölümlü disk seçilmeli"
|
||||
);
|
||||
}
|
||||
|
||||
/// Kök disk bilinmiyorsa (otomatik mod / henüz taranmamış) yine
|
||||
/// bölümlü disk seçilir; hiçbiri yoksa `None`.
|
||||
#[test]
|
||||
fn unknown_root_disk_is_handled() {
|
||||
let disks = vec![d("/dev/sda"), d("/dev/sdb")];
|
||||
assert_eq!(
|
||||
default_boot_device(None, &disks, never_removable, parts_on(&["/dev/sda"])).as_deref(),
|
||||
Some("/dev/sda")
|
||||
);
|
||||
assert_eq!(default_boot_device(None, &disks, never_removable, parts_on(&[])), None);
|
||||
}
|
||||
|
||||
/// Disk listesi boşken bile KÖRK disk biliniyorsa o kullanılır —
|
||||
/// liste yalnızca "mevcut işletim sistemi olan disk" arayışı için
|
||||
/// gerekiyordu, son çare kök disktir.
|
||||
#[test]
|
||||
fn empty_disk_list_still_uses_the_root_disk() {
|
||||
assert_eq!(
|
||||
default_boot_device(Some("/dev/sda"), &[], never_removable, parts_on(&["/dev/sda"])).as_deref(),
|
||||
Some("/dev/sda"),
|
||||
"liste boş olsa da bilinen kök disk kullanılmalı"
|
||||
);
|
||||
}
|
||||
|
||||
/// Ne disk ne de kök biliniyorsa `None` — `on_enter` bu durumda
|
||||
/// `selected_disk`'e düşer, o da yoksa aygıt seçilemez.
|
||||
#[test]
|
||||
fn nothing_known_yields_none() {
|
||||
assert_eq!(default_boot_device(None, &[], never_removable, parts_on(&[])), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use eframe::egui;
|
||||
use rust_i18n::t;
|
||||
|
||||
@@ -15,6 +16,20 @@ use crate::jobs::{self, InstallMessage, UiSender};
|
||||
use crate::ui::{ErrorScreen, Slideshow};
|
||||
use crate::ui::theme;
|
||||
|
||||
/// "Kurulum devam ediyor, lütfen bekleyin…" uyarısı gösterilmeli mi?
|
||||
///
|
||||
/// `copy_active` iken GÖSTERİLMEZ: çubuğun yanında yüzde ve kopyalanan
|
||||
/// dosyanın adı zaten yazılı, "bekleyin" demek yerine ne olduğunu
|
||||
/// söyleyen çok daha fazla bilgi var. Ek olarak bu etiket çubuğun
|
||||
/// altında ikinci kez görünüyordu.
|
||||
///
|
||||
/// Kopyalama bitince `copy_active` false olur ve uyarı geri gelir:
|
||||
/// fstab, GRUB, temizlik gibi adımlarda ilerleme çubuğu yoktur, orada
|
||||
/// kullanıcının beklemesi gerektiğini bilmesi doğrudur.
|
||||
fn show_wait_notice(running: bool, copy_active: bool) -> bool {
|
||||
running && !copy_active
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExecutionStep {
|
||||
rx: Option<mpsc::Receiver<InstallMessage>>,
|
||||
@@ -22,6 +37,10 @@ pub struct ExecutionStep {
|
||||
finished: bool,
|
||||
error_screen: Option<ErrorScreen>,
|
||||
slideshow: Option<Slideshow>,
|
||||
/// rsync kopyalama fazı aktifken tek ilerleme çubuğu gösterilir.
|
||||
copy_active: bool,
|
||||
/// Kurulum bitince tamamlandı ekranına otomatik geçiş için zaman damgası.
|
||||
finished_at: Option<Instant>,
|
||||
}
|
||||
|
||||
impl InstallerStep for ExecutionStep {
|
||||
@@ -33,14 +52,19 @@ impl InstallerStep for ExecutionStep {
|
||||
self.tx = Some(tx);
|
||||
self.finished = false;
|
||||
self.error_screen = None;
|
||||
self.finished_at = None;
|
||||
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_else(|_| "default".to_string())
|
||||
.to_lowercase();
|
||||
self.slideshow = Some(Slideshow::new(&desktop, state.branding_icons.clone(), state.branding_slides.clone()));
|
||||
let cwd = std::env::current_dir().map(|d| d.display().to_string()).unwrap_or_default();
|
||||
let ss = Slideshow::new(&desktop, &state.language);
|
||||
state.install_log.push(format!("Slideshow: cwd={}, lang={}, desktop={}, slayt={}", cwd, state.language, desktop, ss.slide_count()));
|
||||
self.slideshow = Some(ss);
|
||||
|
||||
state.install_progress = 0.0;
|
||||
state.install_sub_progress = 0.0;
|
||||
self.copy_active = false;
|
||||
state.install_log.clear();
|
||||
state.install_current_file = None;
|
||||
state.install_log.push(t!("install_starting").to_string());
|
||||
@@ -99,6 +123,7 @@ impl InstallerStep for ExecutionStep {
|
||||
jobs::JobQueueConfig {
|
||||
mount: "/mnt".to_string(),
|
||||
source: src,
|
||||
language,
|
||||
locale,
|
||||
timezone,
|
||||
username,
|
||||
@@ -139,6 +164,7 @@ impl InstallerStep for ExecutionStep {
|
||||
jobs::JobQueueConfig {
|
||||
mount: "/mnt".to_string(),
|
||||
source: src,
|
||||
language,
|
||||
locale,
|
||||
timezone,
|
||||
username,
|
||||
@@ -186,12 +212,19 @@ impl InstallerStep for ExecutionStep {
|
||||
Ok(InstallMessage::SubProgress(p)) => {
|
||||
state.install_sub_progress = p;
|
||||
}
|
||||
Ok(InstallMessage::CopyStarted) => {
|
||||
self.copy_active = true;
|
||||
}
|
||||
Ok(InstallMessage::CopyFinished) => {
|
||||
self.copy_active = false;
|
||||
}
|
||||
Ok(InstallMessage::CurrentFile(f)) => {
|
||||
state.install_current_file = Some(f);
|
||||
}
|
||||
Ok(InstallMessage::Done) => {
|
||||
state.install_progress = 1.0;
|
||||
self.finished = true;
|
||||
self.finished_at = Some(Instant::now());
|
||||
}
|
||||
Ok(InstallMessage::Error(e)) => {
|
||||
self.error_screen = Some(ErrorScreen::new(
|
||||
@@ -226,76 +259,76 @@ impl InstallerStep for ExecutionStep {
|
||||
}
|
||||
|
||||
// ── Normal kurulum görünümü ────────────────────────────
|
||||
// Üst: Slayt gösterisi | Alt: ilerleme + log
|
||||
// Üst: Slayt gösterisi (kalan alanı doldurur) | Alt: ilerleme durumu
|
||||
let available_w = ui.available_width();
|
||||
let available_h = ui.available_height();
|
||||
|
||||
ui.vertical(|ui| {
|
||||
// Üst kısım — slayt gösterisi
|
||||
let slide_h = (available_h * 0.45).max(280.0);
|
||||
ui.allocate_ui(egui::vec2(available_w, slide_h), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(8.0);
|
||||
theme::section_heading(ui, &t!("execution_title"));
|
||||
ui.label(
|
||||
egui::RichText::new(t!("execution_description"))
|
||||
.color(theme::c_text_dim())
|
||||
.size(13.0),
|
||||
);
|
||||
ui.add_space(16.0);
|
||||
// ── Üst kısım — başlık + slayt gösterisi (kalan alanı doldurur) ──
|
||||
ui.add_space(8.0);
|
||||
theme::section_heading(ui, &t!("execution_title"));
|
||||
ui.label(
|
||||
egui::RichText::new(t!("execution_description"))
|
||||
.color(theme::c_text_dim())
|
||||
.size(13.0),
|
||||
);
|
||||
ui.add_space(16.0);
|
||||
|
||||
if let Some(ref mut ss) = self.slideshow {
|
||||
ss.show(ui);
|
||||
ui.add_space(10.0);
|
||||
// Manuel ileri/geri butonları
|
||||
ui.horizontal(|ui| {
|
||||
if ui.small_button("‹").clicked() { ss.previous(); }
|
||||
if ui.small_button("›").clicked() { ss.advance(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
// Alt kısım (ilerleme + durum) için ayrılan yükseklik.
|
||||
// bottom_up layout en sonda olduğu için slayt alanını kalan
|
||||
// yükseklikle sınırlandırıyoruz; aksi halde slayt gösterisi
|
||||
// ekranın dışına itilir.
|
||||
let bottom_h = 170.0;
|
||||
let slide_h = (ui.available_height() - bottom_h).max(200.0);
|
||||
ui.allocate_ui(egui::vec2(available_w, slide_h), |ui| {
|
||||
ui.set_min_height(slide_h);
|
||||
if let Some(ref mut ss) = self.slideshow {
|
||||
ss.show(ui);
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(16.0);
|
||||
ui.separator();
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Alt kısım — ilerleme + log
|
||||
let log_h = available_h - slide_h - 50.0;
|
||||
ui.allocate_ui(egui::vec2(available_w, log_h), |ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(8.0);
|
||||
// ── Alt kısım — ilerleme + durum (en altta) ──────────
|
||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::Min), |ui| {
|
||||
ui.add_space(8.0);
|
||||
|
||||
// İlerleme çubuğu
|
||||
let running = !self.finished
|
||||
&& self.error_screen.is_none()
|
||||
&& state.install_progress > 0.0;
|
||||
// İlerleme çubuğu — yalnızca rsync kopyalama fazında
|
||||
// tek bar gösterilir (eski YALI düzeni). Diğer adımlar
|
||||
// (fstab, GRUB, temizlik vb.) metin + spinner olarak
|
||||
// görünür.
|
||||
let running = !self.finished
|
||||
&& self.error_screen.is_none()
|
||||
&& (self.copy_active || state.install_progress > 0.0);
|
||||
|
||||
egui::Frame::none()
|
||||
.fill(theme::c_bg_widget())
|
||||
.rounding(egui::Rounding::same(8.0))
|
||||
.inner_margin(egui::Margin::same(12.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(available_w - 4.0);
|
||||
egui::Frame::none()
|
||||
.fill(theme::c_bg_widget())
|
||||
.rounding(egui::Rounding::same(8.0))
|
||||
.inner_margin(egui::Margin::same(12.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(available_w - 4.0);
|
||||
let step_label = state.install_log.last()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
ui.label(
|
||||
egui::RichText::new(&step_label)
|
||||
.size(12.0)
|
||||
.color(theme::c_text_dim()),
|
||||
);
|
||||
|
||||
// Yüzde + adım etiketi
|
||||
let step_label = state.install_log.last()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
format!("{:.0}%", state.install_progress * 100.0)
|
||||
)
|
||||
.strong()
|
||||
.color(theme::c_accent()),
|
||||
);
|
||||
ui.label(
|
||||
egui::RichText::new(&step_label)
|
||||
.size(12.0)
|
||||
.color(theme::c_text_dim()),
|
||||
);
|
||||
});
|
||||
if self.copy_active {
|
||||
let pct = format!("{:.0}%", state.install_sub_progress * 100.0);
|
||||
let bar = ui.add(
|
||||
egui::ProgressBar::new(state.install_sub_progress)
|
||||
.animate(running)
|
||||
.desired_height(20.0),
|
||||
);
|
||||
ui.painter().text(
|
||||
bar.rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
pct,
|
||||
egui::FontId::proportional(14.0),
|
||||
theme::c_text(),
|
||||
);
|
||||
|
||||
if let Some(ref current_file) = state.install_current_file {
|
||||
ui.add_space(4.0);
|
||||
@@ -305,58 +338,29 @@ impl InstallerStep for ExecutionStep {
|
||||
.color(theme::c_text_dim()),
|
||||
);
|
||||
}
|
||||
|
||||
} else if running {
|
||||
ui.add_space(4.0);
|
||||
ui.add(
|
||||
egui::ProgressBar::new(state.install_progress)
|
||||
.animate(running)
|
||||
.desired_height(10.0),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.colored_label(theme::c_text_dim(), t!("install_in_progress"));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
ui.add_space(8.0);
|
||||
if self.finished {
|
||||
theme::success_box(ui, &t!("install_done"));
|
||||
} else if show_wait_notice(running, self.copy_active) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.colored_label(theme::c_text_dim(), t!("install_in_progress"));
|
||||
});
|
||||
}
|
||||
|
||||
// Durum etiketi
|
||||
ui.add_space(8.0);
|
||||
if self.finished {
|
||||
theme::success_box(ui, &t!("install_done"));
|
||||
} else if running {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.colored_label(theme::c_text_dim(), t!("install_in_progress"));
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Log terminali
|
||||
let terminal_h = if self.finished { log_h - 60.0 } else { log_h - 80.0 };
|
||||
egui::ScrollArea::vertical()
|
||||
.max_height(terminal_h.max(100.0))
|
||||
.stick_to_bottom(true)
|
||||
.id_source("exec_log")
|
||||
.show(ui, |ui| {
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgb(12, 12, 20))
|
||||
.rounding(egui::Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(available_w - 8.0);
|
||||
for line in &state.install_log {
|
||||
let color = log_color(line);
|
||||
ui.colored_label(color, line);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(16.0);
|
||||
ui.separator();
|
||||
ui.add_space(16.0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,6 +368,22 @@ impl InstallerStep for ExecutionStep {
|
||||
if !self.finished && self.error_screen.is_none() {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
// Kurulum tamamlanınca kısa bir bekleyişten sonra otomatik olarak
|
||||
// tamamlandı ekranına geç
|
||||
if self.finished && self.error_screen.is_none() {
|
||||
if let Some(at) = self.finished_at {
|
||||
if at.elapsed() >= Duration::from_secs(2) {
|
||||
state.current_step += 1;
|
||||
} else {
|
||||
ui.ctx().request_repaint_after(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn role(&self) -> crate::installer::StepRole {
|
||||
crate::installer::StepRole::Execution
|
||||
}
|
||||
|
||||
fn is_complete(&self, _state: &GlobalState) -> bool {
|
||||
@@ -371,16 +391,50 @@ impl InstallerStep for ExecutionStep {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_color(line: &str) -> egui::Color32 {
|
||||
if line.starts_with('✗') {
|
||||
theme::c_error()
|
||||
} else if line.starts_with('✓') {
|
||||
theme::c_success()
|
||||
} else if line.starts_with('▶') {
|
||||
theme::c_accent()
|
||||
} else if line.starts_with('⚠') {
|
||||
theme::c_warning()
|
||||
} else {
|
||||
egui::Color32::from_rgb(185, 185, 205)
|
||||
// fn log_color(line: &str) -> egui::Color32 {
|
||||
// if line.starts_with('✗') {
|
||||
// theme::c_error()
|
||||
// } else if line.starts_with('✓') {
|
||||
// theme::c_success()
|
||||
// } else if line.starts_with('▶') {
|
||||
// theme::c_accent()
|
||||
// } else if line.starts_with('⚠') {
|
||||
// theme::c_warning()
|
||||
// } else {
|
||||
// egui::Color32::from_rgb(185, 185, 205)
|
||||
// }
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::show_wait_notice;
|
||||
|
||||
/// Kopyalama başlayınca "bekleyin" uyarısı KAYBOLMALI.
|
||||
///
|
||||
/// Kullanıcı isteği: kopyalanan dosyalar gösterilmeye başladığında
|
||||
/// uyarı gereksiz — çubuk zaten yüzde ve dosya adını gösteriyor.
|
||||
#[test]
|
||||
fn wait_notice_hidden_while_copying() {
|
||||
assert!(
|
||||
!show_wait_notice(true, true),
|
||||
"kopyalama sırasında uyarı gösterilmemeli"
|
||||
);
|
||||
}
|
||||
|
||||
/// Çubuk olmayan adımlarda (fstab, GRUB, temizlik) uyarı kalmalı —
|
||||
/// orada kullanıcının beklemesi gerektiğini bilmesi doğru.
|
||||
#[test]
|
||||
fn wait_notice_shown_after_copy_finishes() {
|
||||
assert!(
|
||||
show_wait_notice(true, false),
|
||||
"kopyalama bitince uyarı geri gelmeli"
|
||||
);
|
||||
}
|
||||
|
||||
/// Kurulum başlamadan / bitince uyarı olmaz.
|
||||
#[test]
|
||||
fn wait_notice_absent_when_not_running() {
|
||||
assert!(!show_wait_notice(false, false), "çalışmıyorken uyarı olmamalı");
|
||||
assert!(!show_wait_notice(false, true), "çalışmıyorken uyarı olmamalı");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ impl InstallerStep for FinishStep {
|
||||
ui.vertical_centered(|ui| {
|
||||
// Büyük onay simgesi
|
||||
ui.label(
|
||||
egui::RichText::new("✔")
|
||||
egui::RichText::new("\u{EB7B}")
|
||||
.size(72.0)
|
||||
.color(theme::c_success()),
|
||||
);
|
||||
@@ -37,9 +37,10 @@ impl InstallerStep for FinishStep {
|
||||
// Yeniden Başlat — ana eylem butonu
|
||||
if theme::primary_button(ui, &t!("finish_restart")).clicked() {
|
||||
let _ = std::process::Command::new("sync").status();
|
||||
let _ = std::process::Command::new("reboot")
|
||||
.arg("-f")
|
||||
.spawn();
|
||||
// Temiz kapanma: init (sysvinit) dosya sistemlerini unmount eder.
|
||||
// `-f` kullanılmaz — zorla reboot FAT backup boot sektörünü
|
||||
// güncellemeden sistemi kesip fsck.fat hatası bırakıyordu.
|
||||
let _ = std::process::Command::new("reboot").spawn();
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
|
||||
@@ -52,5 +53,9 @@ impl InstallerStep for FinishStep {
|
||||
});
|
||||
}
|
||||
|
||||
fn role(&self) -> crate::installer::StepRole {
|
||||
crate::installer::StepRole::Finish
|
||||
}
|
||||
|
||||
fn is_complete(&self, _state: &GlobalState) -> bool { true }
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ impl InstallerStep for LicenseStep {
|
||||
}
|
||||
|
||||
fn is_complete(&self, state: &GlobalState) -> bool {
|
||||
state.license_accepted
|
||||
// Demo modu gerçek kurulum yapmadığı için kabul zorunlu değil;
|
||||
// demo gezintisinin tıklanmadan ilerleyebilmesi gerekir
|
||||
// (README'deki "--demo ... arayüz testi" amacı).
|
||||
state.license_accepted || state.demo_mode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,31 @@ impl Default for LocationStep {
|
||||
}
|
||||
|
||||
impl LocationStep {
|
||||
/// Ülke kodundan dil koduna çevirir.
|
||||
///
|
||||
/// YALIDIR: yalnızca `locales/` altında GERÇEKTEN bulunan diller
|
||||
/// döndürür. Daha önce 8 ülke eşleniyordu (de/fr/it/es/ru/nl/pl) ama
|
||||
/// projede yalnızca tr.toml ve en.toml var. `set_locale("de")`
|
||||
/// çağrıldığında rust-i18n sessizce anahtarın kendisini döndürür —
|
||||
/// yani Alman kullanıcı "location_title", "next", "partition" gibi
|
||||
/// çıplak anahtarlarla dolu bir arayüz görüyordu.
|
||||
fn language_for_country(country: &str) -> Option<&'static str> {
|
||||
match country {
|
||||
"TR" => Some("tr"),
|
||||
// AB'nin İngilizce konuşulan bölümleri ve İngilizce resmî
|
||||
// dili olan ülkeler. Bunlar en.toml'a düşer.
|
||||
//
|
||||
// Not: Güney Afrika (ZA) bilinçli olarak listede değil — 11
|
||||
// resmî dili var ve İngilizce baskın değil. Benzer durumdaki
|
||||
// ülkeler de (Kanada, İsviçre) yalnızca İngilizce resmî dile
|
||||
// sahip oldukları için listede.
|
||||
"GB" | "IE" | "MT" | "US" | "CA" | "AU" | "NZ" | "IN" | "SG" |
|
||||
"NG" | "KE" | "GH" | "HK" | "JM" | "BS" | "BB" |
|
||||
"BZ" | "GY" | "TT" | "BW" | "ZW" | "ZM" | "FJI" => Some("en"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn geoip_lookup(state: &mut GlobalState) {
|
||||
let out = std::process::Command::new("curl")
|
||||
.args(["-s", "--connect-timeout", "3", "http://ip-api.com/json"])
|
||||
@@ -111,16 +136,10 @@ impl LocationStep {
|
||||
})
|
||||
};
|
||||
if let Some(country) = extract("countryCode") {
|
||||
match country.as_str() {
|
||||
"TR" => state.language = "tr".to_string(),
|
||||
"DE" => state.language = "de".to_string(),
|
||||
"FR" => state.language = "fr".to_string(),
|
||||
"IT" => state.language = "it".to_string(),
|
||||
"ES" => state.language = "es".to_string(),
|
||||
"RU" => state.language = "ru".to_string(),
|
||||
"NL" => state.language = "nl".to_string(),
|
||||
"PL" => state.language = "pl".to_string(),
|
||||
_ => {}
|
||||
// Mevcut locale dosyası olmayan bir dile geçilmezse
|
||||
// kullanıcının seçtiği dil korunur.
|
||||
if let Some(lang) = Self::language_for_country(&country) {
|
||||
state.language = lang.to_string();
|
||||
}
|
||||
}
|
||||
if let Some(tz) = extract("timezone") {
|
||||
@@ -183,6 +202,38 @@ impl InstallerStep for LocationStep {
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Seçilen zamana göre tarih ve saat gösterimi
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(t!("datetime_current")).strong().color(theme::c_text()));
|
||||
ui.add_space(8.0);
|
||||
|
||||
if state.use_ntp {
|
||||
// NTP modunda seçilen zaman dilimine göre zamanı göster
|
||||
let now = chrono::Utc::now();
|
||||
let formatted = if let Ok(tz) = state.timezone.parse::<chrono_tz::Tz>() {
|
||||
let local_time = now.with_timezone(&tz);
|
||||
local_time.format("%Y-%m-%d %H:%M:%S").to_string()
|
||||
} else {
|
||||
// Zaman dilimi parse edilemezse UTC göster
|
||||
now.format("%Y-%m-%d %H:%M:%S UTC").to_string()
|
||||
};
|
||||
ui.label(egui::RichText::new(formatted).color(theme::c_success()));
|
||||
} else {
|
||||
// Manuel modunda seçilen değerleri göster
|
||||
let formatted = format!(
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}",
|
||||
state.manual_year,
|
||||
state.manual_month,
|
||||
state.manual_day,
|
||||
state.manual_hour,
|
||||
state.manual_minute
|
||||
);
|
||||
ui.label(egui::RichText::new(formatted).color(theme::c_text()));
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
|
||||
let (mut cur_region, mut cur_zone) = Self::split_tz(&state.timezone);
|
||||
|
||||
if !self.regions.contains(&cur_region) {
|
||||
@@ -315,3 +366,54 @@ impl InstallerStep for LocationStep {
|
||||
!state.timezone.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn geoip_only_maps_to_locales_that_exist() {
|
||||
// Regresyon: de/fr/it/es/ru/nl/pl eşleniyordu ama locales/ altında
|
||||
// yalnızca tr.toml ve en.toml var. set_locale("de") sessizce
|
||||
// kabul edildiği için Alman kullanıcı tüm arayüzü çıplak
|
||||
// anahtar olarak görüyordu.
|
||||
for code in ["DE", "FR", "IT", "ES", "RU", "NL", "PL"] {
|
||||
let mapped = LocationStep::language_for_country(code);
|
||||
if let Some(lang) = mapped {
|
||||
assert!(
|
||||
crate::funct::AVAILABLE_LANGUAGES.contains(&lang),
|
||||
"{} -> {} eşlendi ama {} locale dosyası yok",
|
||||
code, lang, lang
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turkey_maps_to_turkish() {
|
||||
assert_eq!(LocationStep::language_for_country("TR"), Some("tr"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn english_speaking_countries_map_to_english() {
|
||||
for code in ["GB", "US", "IE", "AU", "CA", "NZ", "IN"] {
|
||||
assert_eq!(
|
||||
LocationStep::language_for_country(code),
|
||||
Some("en"),
|
||||
"{} İngilizce'ye eşlenmeli", code
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_countries_leave_language_alone() {
|
||||
// Eşleme yok → None → kullanıcının seçtiği dil korunur.
|
||||
for code in ["JP", "CN", "BR", "ZA", "XX", ""] {
|
||||
assert_eq!(
|
||||
LocationStep::language_for_country(code),
|
||||
None,
|
||||
"{} eşlenmemeli", code
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
//! Sihirbaz adımları.
|
||||
//!
|
||||
//! ## Aktif akış
|
||||
//!
|
||||
//! YALI, **KURULU SİSTEMİ squashfs'ten diske kopyalar** (`jobs::CopyFilesJob`
|
||||
//! rsync). Bunun temel sonucu: ISO'da hangi paketler, ekran yöneticisi ve
|
||||
//! masaüstü ortamı varsa, kurulum onları ZATEN içerir. Bu yüzden sihirbaz
|
||||
//! paket seçimi, ekran yöneticisi seçimi veya lisans kabulü sunmaz —
|
||||
//! sunduğu ayarları kopyalama zaten ezdiği için yanıltıcı olurdu.
|
||||
//!
|
||||
//! Aktif sıra (`main.rs::YaliApp::default`):
|
||||
//! Welcome → Location → Keyboard → Network → Partition → Users →
|
||||
//! Bootloader → Summary → Execution → Finish
|
||||
|
||||
pub mod keyboard;
|
||||
pub mod users;
|
||||
pub mod summary;
|
||||
@@ -7,8 +21,6 @@ pub mod partition;
|
||||
pub mod bootloader;
|
||||
pub mod network;
|
||||
pub mod rescue;
|
||||
//pub mod display_manager;
|
||||
pub mod netinstall;
|
||||
pub mod location;
|
||||
|
||||
pub use keyboard::KeyboardStep;
|
||||
@@ -21,3 +33,43 @@ pub use bootloader::BootloaderStep;
|
||||
pub use network::NetworkStep;
|
||||
pub use rescue::RescueStep;
|
||||
pub use location::LocationStep;
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// DORMANT ADIMLAR — squashfs akışının parçası DEĞİL
|
||||
//
|
||||
// Derleniyorlar ve hazırlar, ama sihirbaza BAĞLI DEĞİLLER. main.rs'deki
|
||||
// `steps` vektörüne eklenmedikleri için kullanıcıya hiç gösterilmezler.
|
||||
// Eklenmeleri gerektiğinde tek satır yeterlidir: `InstallerStep::role()`
|
||||
// varsayılan `Normal` döndüğü için konumsal bir güncelleme gerekmez.
|
||||
//
|
||||
// NOT — `#[allow(dead_code)]` yalnızca bu üç modüle, yalnızca aşağıdaki
|
||||
// kasıtlı uyku için konmuştur. 22de7d5 bu gerekçeyi vermeden genel
|
||||
// `#[allow(dead_code)]` serpiştirmiş ve 1804 satırlık bir silmeyi
|
||||
// "dead code temizliği" diye gizlemişti. Buradaki izin yalnızca bu
|
||||
// modüllere ve yalnızca aşağıdaki nedenle sınırlıdır; modül dışında
|
||||
// taze bir ölü kod yine uyarı üretir.
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
/// Ekran yöneticisi / masaüstü ortamı seçimi.
|
||||
/// ISO'nun ekran yöneticisi squashfs'ten geldiği için seçim anlamsız —
|
||||
/// seçilen değer `ConfigureAutologinJob` tarafından ISO'dakini ezerdi.
|
||||
/// Etkinleştirmek için: main.rs'de Users ile Bootloader arasına
|
||||
/// `steps::display_manager::DisplayManagerStep` ekle.
|
||||
#[allow(dead_code)]
|
||||
pub mod display_manager;
|
||||
|
||||
/// Ağdan ek paket kurma (netinstall).
|
||||
/// Squashfs kurulumuyla çelişir: paketler zaten görüntüde. Netinstall
|
||||
/// ayrı bir proje olarak başlatıldığında bu adım `main.rs`'de
|
||||
/// Bootloader ile Summary arasına
|
||||
/// `steps::netinstall::NetinstallStep` olarak eklenmeli,
|
||||
/// `JobQueueConfig::selected_package_groups` doldurulmalı ve
|
||||
/// `InstallExtraPackagesJob` kuyruğa alınmalı. `src/jobs/netinstall.rs`
|
||||
/// bu iş için hazır.
|
||||
#[allow(dead_code)]
|
||||
pub mod netinstall;
|
||||
|
||||
/// GPL lisans metni ve kabul onayı.
|
||||
/// Bu kurulum akışında lisans kabulü bir koşul değil.
|
||||
#[allow(dead_code)]
|
||||
pub mod license;
|
||||
|
||||
@@ -14,7 +14,6 @@ pub const PACKAGE_GROUPS: &[(&str, &str, &[&str])] = &[
|
||||
];
|
||||
|
||||
#[derive(Default)]
|
||||
#[allow(dead_code)]
|
||||
pub struct NetinstallStep;
|
||||
|
||||
impl InstallerStep for NetinstallStep {
|
||||
|
||||
@@ -9,6 +9,9 @@ pub struct NetworkStep {
|
||||
scanning: bool,
|
||||
scan_done: bool,
|
||||
proxy_enabled: bool,
|
||||
/// `nmcli dev status` çıktısı. Adıma girişte bir kez okunur —
|
||||
/// show() içinde okunursa her karede alt süreç doğar.
|
||||
status_text: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -66,6 +69,15 @@ impl NetworkStep {
|
||||
Err(String::from_utf8_lossy(&out.stderr).trim().to_string())
|
||||
}
|
||||
}
|
||||
/// `nmcli dev status` çıktısını tazeler. Adıma girişte ve Wi-Fi
|
||||
/// bağlantı denemelerinden sonra çağrılır.
|
||||
fn refresh_status(&mut self) {
|
||||
self.status_text = std::process::Command::new("nmcli")
|
||||
.args(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "dev", "status"])
|
||||
.output()
|
||||
.ok()
|
||||
.map(|out| String::from_utf8_lossy(&out.stdout).into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallerStep for NetworkStep {
|
||||
@@ -73,28 +85,41 @@ impl InstallerStep for NetworkStep {
|
||||
t!("network").to_string()
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, _state: &mut GlobalState) {
|
||||
self.refresh_status();
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
|
||||
ui.heading(t!("network_title"));
|
||||
ui.label(t!("network_description"));
|
||||
ui.add_space(12.0);
|
||||
|
||||
// Mevcut bağlantı durumu
|
||||
if let Ok(out) = std::process::Command::new("nmcli")
|
||||
.args(["-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "dev", "status"])
|
||||
.output()
|
||||
{
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
let connected: Vec<&str> = text.lines()
|
||||
.filter(|l| l.contains(":connected:") || l.contains(":connecting:"))
|
||||
.collect();
|
||||
if !connected.is_empty() {
|
||||
//
|
||||
// `nmcli dev status` bir alt süreçtir ve ~50-100 ms sürer. Burada
|
||||
// show() içinde, yani saniyede ~60 kez çağrılıyordu. Durum
|
||||
// adımlara girişte bir kez okunur; bağlantı değişikliği
|
||||
// `scan_wifi`/`connect` sonrası elle tazelenir.
|
||||
match &self.status_text {
|
||||
Some(text) => {
|
||||
let connected: Vec<&str> = text.lines()
|
||||
.filter(|l| l.contains(":connected:") || l.contains(":connecting:"))
|
||||
.collect();
|
||||
if !connected.is_empty() {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(80, 170, 80),
|
||||
format!("{} {}", t!("network_connected"), connected.join(", ")),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(200, 140, 40),
|
||||
t!("network_disconnected"),
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(80, 170, 80),
|
||||
format!("{} {}", t!("network_connected"), connected.join(", ")),
|
||||
);
|
||||
} else {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(200, 140, 40),
|
||||
egui::Color32::from_rgb(120, 120, 140),
|
||||
t!("network_disconnected"),
|
||||
);
|
||||
}
|
||||
@@ -128,6 +153,7 @@ impl InstallerStep for NetworkStep {
|
||||
if !self.scan_done {
|
||||
if ui.add(egui::Button::new(t!("network_scan"))).clicked() {
|
||||
self.scan_wifi();
|
||||
self.refresh_status();
|
||||
}
|
||||
} else {
|
||||
ui.horizontal(|ui| {
|
||||
@@ -154,24 +180,25 @@ impl InstallerStep for NetworkStep {
|
||||
.show(ui, |ui| {
|
||||
for (i, net) in self.scan_results.clone().iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
let lock = if net.secured { "🔒" } else { "🌐" };
|
||||
let lock = if net.secured { "\u{EECE}" } else { "\u{EDCF}" };
|
||||
let sig = net.signal.parse::<i32>().unwrap_or(0);
|
||||
let bars = if sig > -50 { "▂▄▆█" } else if sig > -67 { "▂▄▆" } else if sig > -80 { "▂▄" } else { "▂" };
|
||||
let active = if net.active { " ✓" } else { "" };
|
||||
let bars = if sig > -50 { "\u{F133}" } else if sig > -67 { "\u{F12F}" } else if sig > -80 { "\u{F12B}" } else { "\u{F134}" };
|
||||
let active = if net.active { " \u{EB7B}" } else { "" };
|
||||
|
||||
ui.label(format!("{} {}{}", lock, net.ssid, active));
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(bars)
|
||||
.size(10.0)
|
||||
.size(16.0)
|
||||
.color(egui::Color32::from_rgb(120, 120, 140)),
|
||||
);
|
||||
if net.secured && !net.active {
|
||||
ui.add(egui::TextEdit::singleline(&mut state.network_wifi_password)
|
||||
.password(!self.show_password.get(i).copied().unwrap_or(false))
|
||||
.hint_text(t!("network_password_hint"))
|
||||
.desired_width(120.0));
|
||||
if ui.small_button("👁").clicked() {
|
||||
.desired_width(120.0)
|
||||
.text_color(egui::Color32::from_rgb(120, 120, 140)));
|
||||
if ui.add_sized([24.0, 24.0], egui::Button::new(egui::RichText::new("\u{ECB5}").size(14.0))).clicked() {
|
||||
if self.show_password.len() <= i {
|
||||
self.show_password.resize(i + 1, false);
|
||||
}
|
||||
@@ -184,6 +211,9 @@ impl InstallerStep for NetworkStep {
|
||||
Ok(msg) => {
|
||||
state.install_log.push(format!("Wi-Fi: {} — {}", net.ssid, msg));
|
||||
self.scan_done = false;
|
||||
// Bağlantı durumu değişti; üstteki
|
||||
// "bağlı/bağlı değil" satırı tazelensin.
|
||||
self.refresh_status();
|
||||
}
|
||||
Err(e) => {
|
||||
state.install_log.push(format!("Wi-Fi error: {}", e));
|
||||
|
||||
@@ -84,10 +84,10 @@ impl RescueStep {
|
||||
if let Ok(s) = r {
|
||||
if s.success() {
|
||||
self.mount_done = true;
|
||||
self.log.push(format!("✓ {} → /mnt bağlandı.", dev));
|
||||
self.log.push(format!("\u{EB7B} {} → /mnt bağlandı.", dev));
|
||||
return true;
|
||||
} else {
|
||||
self.log.push(format!("⚠ {} bağlanamadı (zaten bağlı veya hata).", dev));
|
||||
self.log.push(format!("\u{EA6B} {} bağlanamadı (zaten bağlı veya hata).", dev));
|
||||
self.mount_done = true; // Belki zaten bağlı
|
||||
return true;
|
||||
}
|
||||
@@ -126,11 +126,20 @@ impl RescueStep {
|
||||
let op: u32 = op_str.parse().unwrap_or(0);
|
||||
|
||||
// Tarih: "[2024-03-15 12:00:01]"
|
||||
let date = if let Some(start) = remainder.find('[') {
|
||||
if let Some(end) = remainder.find(']') {
|
||||
remainder[start+1..end].to_string()
|
||||
} else { String::new() }
|
||||
} else { String::new() };
|
||||
// `]` ve `[` bağımsız aranırsa, `]` önce gelen bir girdide
|
||||
// `end < start` olur ve `remainder[start+1..end]` PANİK verir
|
||||
// ("slice index starts at N but ends at M"). Kapanış
|
||||
// parantez, açılıştan SONRA aranmalı.
|
||||
let date = match remainder.find('[') {
|
||||
None => String::new(),
|
||||
Some(open) => {
|
||||
let inner = open + 1;
|
||||
match remainder[inner..].find(']') {
|
||||
Some(rel_close) => remainder[inner..inner + rel_close].to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tür + özet: ": install [vim, curl]"
|
||||
let (op_type, summary) = if let Some(colon) = remainder.rfind("]: ") {
|
||||
@@ -194,13 +203,13 @@ impl RescueStep {
|
||||
Ok(o) if o.status.success() => {
|
||||
self.pisi.result_ok = Some(true);
|
||||
self.pisi.result_msg = Some(t!("rescue_pisi_history_ok").to_string());
|
||||
self.log.push(format!("✓ pisi takeback {} başarılı.", op));
|
||||
self.log.push(format!("\u{EB7B} pisi takeback {} başarılı.", op));
|
||||
}
|
||||
Ok(o) => {
|
||||
let err = String::from_utf8_lossy(&o.stderr).trim().to_string();
|
||||
self.pisi.result_ok = Some(false);
|
||||
self.pisi.result_msg = Some(t!("rescue_pisi_history_err", err = err).to_string());
|
||||
self.log.push(format!("❌ pisi takeback {}: {}", op, err));
|
||||
self.log.push(format!("\u{EA21} pisi takeback {}: {}", op, err));
|
||||
}
|
||||
Err(e) => {
|
||||
self.pisi.result_ok = Some(false);
|
||||
@@ -249,8 +258,8 @@ impl RescueStep {
|
||||
if self.mount_done && resp.clicked() {
|
||||
self.log.push(t!("rescue_chroot_start").to_string());
|
||||
match Self::run_rescue_cmd("chroot", &["/mnt", "/bin/bash", "-c", "echo 'chroot OK'"]) {
|
||||
Ok(msg) => self.log.push(format!("✓ {}", msg)),
|
||||
Err(e) => self.log.push(format!("❌ {}", e)),
|
||||
Ok(msg) => self.log.push(format!("\u{EB7B} {}", msg)),
|
||||
Err(e) => self.log.push(format!("\u{EA21} {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +277,7 @@ impl RescueStep {
|
||||
match Self::run_rescue_cmd(cmd, args) {
|
||||
Ok(m) if !m.is_empty() => self.log.push(m),
|
||||
Ok(_) => {},
|
||||
Err(e) => self.log.push(format!("⚠ {}", e)),
|
||||
Err(e) => self.log.push(format!("\u{EA6B} {}", e)),
|
||||
}
|
||||
}
|
||||
// grub2-install
|
||||
@@ -278,13 +287,13 @@ impl RescueStep {
|
||||
a.extend_from_slice(&grub_args);
|
||||
a
|
||||
}) {
|
||||
Ok(m) => self.log.push(format!("✓ grub2-install: {}", m)),
|
||||
Err(e) => self.log.push(format!("❌ grub2-install: {}", e)),
|
||||
Ok(m) => self.log.push(format!("\u{EB7B} grub2-install: {}", m)),
|
||||
Err(e) => self.log.push(format!("\u{EA21} grub2-install: {}", e)),
|
||||
}
|
||||
// grub2-mkconfig
|
||||
match Self::run_rescue_cmd("chroot", &["/mnt", "grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"]) {
|
||||
Ok(m) => self.log.push(format!("✓ grub2-mkconfig: {}", m)),
|
||||
Err(e) => self.log.push(format!("❌ grub2-mkconfig: {}", e)),
|
||||
Ok(m) => self.log.push(format!("\u{EB7B} grub2-mkconfig: {}", m)),
|
||||
Err(e) => self.log.push(format!("\u{EA21} grub2-mkconfig: {}", e)),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -372,11 +381,16 @@ impl RescueStep {
|
||||
ui.colored_label(type_color, &entry.op_type);
|
||||
|
||||
// Özet — truncate
|
||||
let summary = if entry.summary.len() > 50 {
|
||||
format!("{}…", &entry.summary[..50])
|
||||
} else {
|
||||
entry.summary.clone()
|
||||
};
|
||||
// `len()` bayt sayar, `&s[..50]` ise bayt
|
||||
// sınırı ister. `pisi history` çıktısı paket
|
||||
// adlarından gelir ve Türkçe karakter içerir
|
||||
// ("İ", "ş", "ğ" 2 bayt): `[..50]` karakter
|
||||
// sınırının ortasına denk gelirse PANİK.
|
||||
// Karakter sayarak kırpmak gerekir.
|
||||
let mut summary: String = entry.summary.chars().take(50).collect();
|
||||
if entry.summary.chars().count() > 50 {
|
||||
summary.push('…');
|
||||
}
|
||||
ui.label(egui::RichText::new(summary).size(11.0).color(theme::c_text_dim()));
|
||||
ui.end_row();
|
||||
}
|
||||
@@ -479,11 +493,11 @@ impl InstallerStep for RescueStep {
|
||||
.inner_margin(egui::Margin::same(8.0))
|
||||
.show(ui, |ui| {
|
||||
for line in &self.log {
|
||||
let color = if line.starts_with("✓") {
|
||||
let color = if line.starts_with("\u{EB7B}") {
|
||||
theme::c_success()
|
||||
} else if line.starts_with("❌") {
|
||||
} else if line.starts_with("\u{EA21}") {
|
||||
theme::c_error()
|
||||
} else if line.starts_with("⚠") {
|
||||
} else if line.starts_with("\u{EA6B}") {
|
||||
theme::c_warning()
|
||||
} else {
|
||||
theme::c_text_dim()
|
||||
@@ -501,7 +515,7 @@ impl InstallerStep for RescueStep {
|
||||
|
||||
// Geri dön butonu
|
||||
ui.add_space(8.0);
|
||||
if theme::secondary_button(ui, &t!("rescue_return")).clicked() {
|
||||
if theme::secondary_button(ui, &format!("\u{EB7C} {}", t!("rescue_return"))).clicked() {
|
||||
state.rescue_mode = false;
|
||||
}
|
||||
}
|
||||
@@ -510,3 +524,65 @@ impl InstallerStep for RescueStep {
|
||||
false // Rescue modu her zaman "tamamlanmamış" — normal kuruluma dönmek için rescue_mode=false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Özet kırpma: Türkçe karakterler 2 bayt olduğu için bayt tabanlı
|
||||
/// dilimleme karakter sınırının ortasına denk gelip panik verirdi.
|
||||
fn truncate(s: &str, max: usize) -> String {
|
||||
let mut out: String = s.chars().take(max).collect();
|
||||
if s.chars().count() > max {
|
||||
out.push('…');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_handles_multibyte_without_panicking() {
|
||||
// "şğüöç" 5 karakter × 20 = 100 karakter, 200 bayt. 50 karakterlik
|
||||
// kesme bayt sınırının tam ortasına denk gelirdi ve `&s[..50]`
|
||||
// PANİK verirdi.
|
||||
let s = "şğüöç".repeat(20);
|
||||
assert_eq!(s.chars().count(), 100);
|
||||
let t = truncate(&s, 50);
|
||||
assert_eq!(t.chars().count(), 51, "50 karakter + '…'");
|
||||
assert!(t.ends_with('…'));
|
||||
assert!(t.chars().take(50).all(|c| "şğüöç".contains(c)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_leaves_short_strings_untouched() {
|
||||
assert_eq!(truncate("kısa", 50), "kısa");
|
||||
assert_eq!(truncate("", 50), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_history_handles_stray_close_bracket() {
|
||||
// "] Operation ..." — ']' '[' tan önce geliyor. Eski kod
|
||||
// remainder[start+1..end] ile PANİK verirdi.
|
||||
let out = RescueStep::parse_pisi_history("Operation 5 ] bogus: install [vim]");
|
||||
assert_eq!(out.len(), 1, "girdi hâlâ işlenmeli");
|
||||
assert_eq!(out[0].op, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_history_reads_date_and_summary() {
|
||||
let raw = "Operation 42 [2024-03-15 12:00:01]: install [vim, curl]";
|
||||
let out = RescueStep::parse_pisi_history(raw);
|
||||
assert_eq!(out.len(), 1);
|
||||
assert_eq!(out[0].op, 42);
|
||||
assert_eq!(out[0].date, "2024-03-15 12:00:01");
|
||||
assert_eq!(out[0].op_type, "install");
|
||||
// Özet, "]: " sonrasındaki ilk boşluktan itibaren alınır; paket
|
||||
// listesi köşeli parantezleriyle birlikte kalır (mevcut davranış).
|
||||
assert_eq!(out[0].summary, "[vim, curl]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_history_tolerates_unterminated_bracket() {
|
||||
let out = RescueStep::parse_pisi_history("Operation 7 [2024-03-15 12:00:01: install [x]");
|
||||
assert_eq!(out.len(), 1, "panik vermemeli, kayıt üretmeli");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,20 @@ impl InstallerStep for SummaryStep {
|
||||
ui.add_space(12.0);
|
||||
|
||||
egui::Frame::group(ui.style()).show(ui, |ui| {
|
||||
// Frame'in tüm genişliği kaplamasını sağlıyoruz
|
||||
ui.set_width(ui.available_width());
|
||||
|
||||
// Grid genişlik ayarları
|
||||
let available_width = ui.available_width();
|
||||
let col1_width = 160.0; // Sol sütun (etiketler)
|
||||
let col2_width = (available_width - col1_width - 20.0).max(100.0); // Sağ sütun kalan alan
|
||||
|
||||
egui::Grid::new("summary_grid")
|
||||
.num_columns(2)
|
||||
.spacing([20.0, 8.0])
|
||||
.striped(true)
|
||||
.spacing([12.0, 8.0])
|
||||
.min_col_width(col1_width)
|
||||
.max_col_width(col2_width)
|
||||
.show(ui, |ui| {
|
||||
summary_row(ui, t!("summary_language"), &state.language);
|
||||
summary_row(ui, t!("summary_timezone"), &state.timezone);
|
||||
@@ -35,15 +43,51 @@ impl InstallerStep for SummaryStep {
|
||||
};
|
||||
summary_row(ui, t!("summary_keyboard"), &kb);
|
||||
|
||||
let disk_str = match &state.selected_disk {
|
||||
Some(d) => {
|
||||
if state.erase_disk {
|
||||
format!("{} — {}", d, t!("summary_erase_mode"))
|
||||
} else {
|
||||
format!("{} — {}", d, t!("summary_manual_mode"))
|
||||
let disk_str = if state.erase_disk {
|
||||
match &state.selected_disk {
|
||||
Some(d) => format!("{} \u{EA6B} {}", d, t!("summary_erase_mode")),
|
||||
None => t!("summary_not_selected").to_string(),
|
||||
}
|
||||
} else if !state.custom_partitions.is_empty() {
|
||||
// Manuel modda tanım yapılan TÜM diskleri listele.
|
||||
// KÖKün olduğu disk işaretlenir: GRUB varsayılanı
|
||||
// ona kurulur ve kullanıcı hangi diske önyükleyici
|
||||
// yazılacağını buradan görebilmeli.
|
||||
let mut disks: Vec<&str> = Vec::new();
|
||||
for part in &state.custom_partitions {
|
||||
if !disks.contains(&part.disk.as_str()) {
|
||||
disks.push(&part.disk);
|
||||
}
|
||||
}
|
||||
None => t!("summary_not_selected").to_string(),
|
||||
let root_disk = state
|
||||
.custom_partitions
|
||||
.iter()
|
||||
.find(|p| p.mountpoint == "/" && !p.delete)
|
||||
.map(|p| p.disk.as_str());
|
||||
let listed: Vec<String> = disks
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|d| {
|
||||
if Some(d) == root_disk {
|
||||
// ⭐ gibi bir emoji kullanılmıyor:
|
||||
// setup_fonts yalnızca sistem +
|
||||
// remixicon yüklüyor, renkli emoji
|
||||
// fontu yorum satırında — kutu (tofu)
|
||||
// çıkar. Düz metin işaret hem her
|
||||
// fontta var hem de çeviriyle uyumlu.
|
||||
format!("{} [{}]", d, t!("summary_root_disk_tag"))
|
||||
} else {
|
||||
d.to_string()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("{} \u{EA6B} {}",
|
||||
listed.join(", "), t!("summary_manual_mode"))
|
||||
} else {
|
||||
match &state.selected_disk {
|
||||
Some(d) => format!("{} \u{EA6B} {}", d, t!("summary_manual_mode")),
|
||||
None => t!("summary_not_selected").to_string(),
|
||||
}
|
||||
};
|
||||
summary_row(ui, t!("summary_disk"), &disk_str);
|
||||
|
||||
@@ -65,15 +109,72 @@ impl InstallerStep for SummaryStep {
|
||||
}
|
||||
} else if !state.custom_partitions.is_empty() {
|
||||
let mut plan_lines = Vec::new();
|
||||
// Bölümleri ait oldukları diske göre gruplu listele
|
||||
let mut seen_disks: Vec<&str> = Vec::new();
|
||||
for part in &state.custom_partitions {
|
||||
let size_str = if part.size_mb == 0 {
|
||||
t!("mp_size_remaining").to_string()
|
||||
} else if part.size_mb >= 1024 {
|
||||
format!("{:.1} GB", part.size_mb as f64 / 1024.0)
|
||||
if !seen_disks.contains(&part.disk.as_str()) {
|
||||
seen_disks.push(&part.disk);
|
||||
}
|
||||
}
|
||||
for d in seen_disks {
|
||||
// Disk başlığı + kapasite. Çoklu disk
|
||||
// kurulumunda kullanıcının hangi bölümün
|
||||
// hangi diskte olduğunu tek ekranda görmesi
|
||||
// gerekiyor; yalnızca "/dev/sdb" yazması
|
||||
// yetersiz.
|
||||
let cap_mb = state.available_disks.iter()
|
||||
.find(|dk| dk.name == d)
|
||||
.map(|dk| dk.size_bytes / 1_048_576)
|
||||
.unwrap_or(0);
|
||||
let used_mb: u64 = state.custom_partitions.iter()
|
||||
.filter(|p| p.disk == d && !p.delete)
|
||||
.map(|p| p.size_mb)
|
||||
.sum();
|
||||
let hdr = if cap_mb >= 1024 {
|
||||
format!("\u{EDFB} {} ({:.0} GB)", d, cap_mb as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{} MB", part.size_mb)
|
||||
format!("\u{EDFB} {} ({} MB)", d, cap_mb)
|
||||
};
|
||||
plan_lines.push(format!("{} ➔ {} ({}, {})", part.device, part.mountpoint, part.fstype, size_str));
|
||||
plan_lines.push(hdr);
|
||||
for part in state.custom_partitions.iter().filter(|p| p.disk == d) {
|
||||
let size_str = if part.size_mb == 0 {
|
||||
// Sembolik boyut: kalan kapasiteyi göster
|
||||
let others: u64 = state.custom_partitions.iter()
|
||||
.filter(|q| q.disk == part.disk && !q.delete && q.size_mb > 0)
|
||||
.map(|q| q.size_mb)
|
||||
.sum();
|
||||
let cap = state.available_disks.iter()
|
||||
.find(|dk| dk.name == part.disk)
|
||||
.map(|dk| dk.size_bytes / 1_048_576)
|
||||
.unwrap_or(0);
|
||||
let rem = cap.saturating_sub(others);
|
||||
if rem >= 1024 {
|
||||
format!("{:.1} GB", rem as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{} MB", rem)
|
||||
}
|
||||
} else if part.size_mb >= 1024 {
|
||||
format!("{:.1} GB", part.size_mb as f64 / 1024.0)
|
||||
} else {
|
||||
format!("{} MB", part.size_mb)
|
||||
};
|
||||
if part.delete {
|
||||
plan_lines.push(format!("{} \u{EA6B} {} ({}, {}) \u{ECA1} {}",
|
||||
part.device, "—", "—", size_str, t!("mp_will_delete")));
|
||||
} else {
|
||||
plan_lines.push(format!("{} \u{EA6B} {} ({}, {})",
|
||||
part.device, part.mountpoint, part.fstype, size_str));
|
||||
}
|
||||
}
|
||||
// Diskin kullanılmayan boş alanı
|
||||
let free_mb = cap_mb.saturating_sub(used_mb);
|
||||
if free_mb > 0 {
|
||||
if free_mb >= 1024 {
|
||||
plan_lines.push(format!(" — boş: {:.1} GB", free_mb as f64 / 1024.0));
|
||||
} else {
|
||||
plan_lines.push(format!(" — boş: {} MB", free_mb));
|
||||
}
|
||||
}
|
||||
}
|
||||
summary_row(ui, t!("summary_custom_partitions"), &plan_lines.join("\n"));
|
||||
}
|
||||
@@ -89,18 +190,30 @@ impl InstallerStep for SummaryStep {
|
||||
summary_row(ui, t!("summary_datetime"), &dt_str);
|
||||
|
||||
// Bootloader
|
||||
let bl_str = format!("{} ({}s timeout{})",
|
||||
state.bootloader_device,
|
||||
state.bootloader_timeout,
|
||||
if state.bootloader_password.is_empty() { "" } else { ", password protected" }
|
||||
let pw_suffix = if state.bootloader_password.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(", {}", t!("summary_bootloader_password"))
|
||||
};
|
||||
let bl_str = format!(
|
||||
"{} ({}s timeout{})",
|
||||
state.bootloader_device, state.bootloader_timeout, pw_suffix
|
||||
);
|
||||
summary_row(ui, t!("summary_bootloader"), &bl_str);
|
||||
|
||||
// Display Manager
|
||||
let dm_str = format!("{} / {} ({})",
|
||||
state.display_manager,
|
||||
state.desktop_environment,
|
||||
if state.autologin { t!("summary_autologin") } else { std::borrow::Cow::Borrowed("manual login") }
|
||||
// Oturum yöneticisi ve masaüstü ortamı
|
||||
//
|
||||
// Kullanıcı bunları SEÇEMEZ: sistem squashfs'ten
|
||||
// kopyalandığı için ISO'nun kendi DM'i gelir ve
|
||||
// ConfigureAutologinJob kurulum anında tespit eder.
|
||||
// `state.display_manager` GlobalState'in sabit "sddm"
|
||||
// varsayılanıdır; burada göstermek yanlış bilgi olurdu.
|
||||
// Gösterilebilen tek şey, kullanıcının belirlediği
|
||||
// otomatik giriş tercihi.
|
||||
let dm_str = format!(
|
||||
"{} — {}",
|
||||
if state.autologin { t!("summary_autologin_on") } else { t!("summary_autologin_off") },
|
||||
t!("summary_desktop_from_image")
|
||||
);
|
||||
summary_row(ui, t!("summary_display_manager"), &dm_str);
|
||||
|
||||
@@ -118,16 +231,18 @@ impl InstallerStep for SummaryStep {
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Uyarı kutusu
|
||||
// Uyarı kutusu (\u{EA6B} uyarısı eklenmiş)
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgba_unmultiplied(200, 60, 40, 30))
|
||||
.rounding(6.0)
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(180, 60, 40),
|
||||
t!("summary_warning"),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(
|
||||
egui::Color32::from_rgb(255, 255, 255),
|
||||
format!("\u{EA21} {}", t!("summary_warning")),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,6 +254,9 @@ impl InstallerStep for SummaryStep {
|
||||
|
||||
fn summary_row(ui: &mut egui::Ui, label: impl Into<String>, value: &str) {
|
||||
ui.label(egui::RichText::new(label.into()).strong().color(crate::ui::theme::c_text()));
|
||||
ui.label(egui::RichText::new(value).color(crate::ui::theme::c_text()));
|
||||
|
||||
// Değer sütununun taşmasını engellemek ve hizalamayı korumak için wrap() kullanıyoruz
|
||||
ui.add(egui::Label::new(egui::RichText::new(value).color(crate::ui::theme::c_text())).wrap(true));
|
||||
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,10 @@ fn derive_username(real_name: &str) -> String {
|
||||
/// Hata varsa hata mesajını döndürür, yoksa None döndürür.
|
||||
fn validate_username(s: &str) -> Option<String> {
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
// Boş kullanıcı adı GEÇERLİ DEĞİLDİR. Daha önce burada `None`
|
||||
// (hata yok) dönüyordu, bu yüzden geçerli bir hostname ile boş
|
||||
// kullanıcı adı sihirbazı geçip `useradd ""` çalıştırıyordu.
|
||||
return Some(t!("username_empty").to_string());
|
||||
}
|
||||
if s.len() > 32 {
|
||||
return Some(t!("username_too_long").to_string());
|
||||
@@ -211,16 +214,16 @@ impl InstallerStep for UsersStep {
|
||||
|
||||
|
||||
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())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EB7B}")
|
||||
.size(18.0)
|
||||
.color(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())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EA21}")
|
||||
.size(18.0)
|
||||
.color(theme::c_error())
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -238,16 +241,16 @@ impl InstallerStep for UsersStep {
|
||||
form_input_text(ui, &mut state.username);
|
||||
if !state.username.is_empty() {
|
||||
if let Some(_err) = validate_username(&state.username) {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(theme::c_error())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EA21}")
|
||||
.size(18.0)
|
||||
.color(theme::c_error())
|
||||
);
|
||||
} else {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(theme::c_success())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EB7B}")
|
||||
.size(18.0)
|
||||
.color(theme::c_success())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -290,16 +293,16 @@ impl InstallerStep for UsersStep {
|
||||
form_input_password(ui, &mut state.password_confirm, show_pwd);
|
||||
if !state.password_confirm.is_empty() {
|
||||
if state.password == state.password_confirm {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(theme::c_success())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EB7B}")
|
||||
.size(18.0)
|
||||
.color(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())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EA21}")
|
||||
.size(18.0)
|
||||
.color(theme::c_error())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -316,16 +319,16 @@ impl InstallerStep for UsersStep {
|
||||
form_input_text(ui, &mut state.hostname);
|
||||
if !state.hostname.is_empty() {
|
||||
if is_valid_hostname(&state.hostname) {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(theme::c_success())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EB7B}")
|
||||
.size(18.0)
|
||||
.color(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())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EA21}")
|
||||
.size(18.0)
|
||||
.color(theme::c_error())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -377,16 +380,16 @@ impl InstallerStep for UsersStep {
|
||||
form_input_password(ui, &mut state.root_password_confirm, show_root_pwd_conf);
|
||||
if !state.root_password_confirm.is_empty() {
|
||||
if state.root_password == state.root_password_confirm {
|
||||
ui.add(
|
||||
egui::Image::new(egui::include_image!("../../assets/icons/check.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(theme::c_success())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EB7B}")
|
||||
.size(18.0)
|
||||
.color(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())
|
||||
ui.label(
|
||||
egui::RichText::new("\u{EA21}")
|
||||
.size(18.0)
|
||||
.color(theme::c_error())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -410,16 +413,98 @@ impl InstallerStep for UsersStep {
|
||||
|
||||
let user_ok = is_valid_username(&state.username)
|
||||
&& is_valid_hostname(&state.hostname)
|
||||
&& state.password.len() >= 8
|
||||
// DÜZELTME: Kullanıcı şifresi en az 3 karakter olmalı
|
||||
&& state.password.len() >= 3
|
||||
&& state.password == state.password_confirm;
|
||||
|
||||
if self.use_same_for_admin {
|
||||
user_ok
|
||||
} else {
|
||||
// DÜZELTME: Root şifresi boşsa veya 3 karakterden uzunsa ve eşleşiyorsa geçerli
|
||||
user_ok && (
|
||||
state.root_password.is_empty()
|
||||
|| (state.root_password.len() >= 8 && state.root_password == state.root_password_confirm)
|
||||
|| (state.root_password.len() >= 3 && state.root_password == state.root_password_confirm)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_username_is_rejected() {
|
||||
// Regresyon: validate_username("") None (hata yok) dönüyordu, bu
|
||||
// yüzden geçerli bir hostname ile boş kullanıcı adı sihirbazı
|
||||
// geçip CreateUserJob'a ulaşıyordu.
|
||||
assert!(
|
||||
validate_username("").is_some(),
|
||||
"boş kullanıcı adı hata dönmeli"
|
||||
);
|
||||
assert!(!is_valid_username(""), "boş kullanıcı adı geçerli olmamalı");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_usernames_pass() {
|
||||
for name in ["user", "ahmet", "a", "_root_", "user-1", "u_2026"] {
|
||||
assert!(
|
||||
validate_username(name).is_none(),
|
||||
"{} geçerli olmalı", name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_usernames_are_forbidden() {
|
||||
for name in ["root", "daemon", "nobody", "sshd", "www-data"] {
|
||||
assert!(
|
||||
validate_username(name).is_some(),
|
||||
"{} yasaklı olmalı", name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn username_start_and_charset_rules() {
|
||||
assert!(validate_username("1user").is_some(), "rakamla başlayamaz");
|
||||
assert!(validate_username("Auser").is_some(), "büyük harfle başlayamaz");
|
||||
assert!(validate_username("user name").is_some(), "boşluk olmaz");
|
||||
assert!(validate_username(&"a".repeat(33)).is_some(), "32'den uzun olmaz");
|
||||
assert!(validate_username(&"a".repeat(32)).is_none(), "32 kabul edilir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostname_rules() {
|
||||
assert!(is_valid_hostname("pisi-pc"));
|
||||
assert!(is_valid_hostname("Pisi1"));
|
||||
assert!(!is_valid_hostname(""), "boş olmaz");
|
||||
assert!(!is_valid_hostname("-pisi"), "tire ile başlayamaz");
|
||||
assert!(!is_valid_hostname("pisi-"), "tire ile bitemez");
|
||||
assert!(!is_valid_hostname("pisi pc"), "boşluk olmaz");
|
||||
assert!(!is_valid_hostname("pisi_pc"), "alt çizgi olmaz");
|
||||
assert!(!is_valid_hostname(&"a".repeat(64)), "63'ten uzun olmaz");
|
||||
assert!(is_valid_hostname(&"a".repeat(63)), "63 kabul edilir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_username_transliterates_turkish() {
|
||||
assert_eq!(derive_username("Şükrü Çelik"), "sukru-celik");
|
||||
assert_eq!(derive_username("Çağrı"), "cagri");
|
||||
assert_eq!(derive_username("İpek"), "ipek");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_username_never_returns_empty() {
|
||||
// Yalnızca geçersiz karakterlerden oluşan isimde boş dönmemeli.
|
||||
assert_eq!(derive_username("!!!"), "user");
|
||||
assert_eq!(derive_username(" "), "user");
|
||||
assert_eq!(derive_username(""), "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_username_caps_length() {
|
||||
let long = "a".repeat(50);
|
||||
assert_eq!(derive_username(&long).len(), 32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ 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(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0, actual_border));
|
||||
ui.painter().rect(rect, rounding, Color32::TRANSPARENT, Stroke::new(1.0_f32, actual_border));
|
||||
|
||||
ui.painter().galley(
|
||||
egui::pos2(rect.center().x - galley.size().x / 2.0, rect.center().y - galley.size().y / 2.0),
|
||||
|
||||
@@ -7,13 +7,13 @@ pub fn apply_visuals(v: &mut egui::Visuals) {
|
||||
v.widgets.hovered.weak_bg_fill = Color32::from_rgb(200, 200, 200);
|
||||
v.widgets.active.weak_bg_fill = Color32::from_rgb(180, 180, 180);
|
||||
|
||||
v.widgets.inactive.fg_stroke = Stroke::new(1.0, Color32::from_rgb(50, 50, 50));
|
||||
v.widgets.hovered.fg_stroke = Stroke::new(1.5, Color32::WHITE);
|
||||
v.widgets.active.fg_stroke = Stroke::new(2.0, Color32::WHITE);
|
||||
v.widgets.inactive.fg_stroke = Stroke::new(1.0_f32, Color32::from_rgb(50, 50, 50));
|
||||
v.widgets.hovered.fg_stroke = Stroke::new(1.5_f32, Color32::WHITE);
|
||||
v.widgets.active.fg_stroke = Stroke::new(2.0_f32, Color32::WHITE);
|
||||
|
||||
v.widgets.inactive.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
|
||||
v.widgets.hovered.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
|
||||
v.widgets.active.bg_stroke = Stroke::new(1.0, Color32::from_rgb(155, 155, 155));
|
||||
v.widgets.inactive.bg_stroke = Stroke::new(1.0_f32, Color32::from_rgb(155, 155, 155));
|
||||
v.widgets.hovered.bg_stroke = Stroke::new(1.0_f32, Color32::from_rgb(155, 155, 155));
|
||||
v.widgets.active.bg_stroke = Stroke::new(1.0_f32, Color32::from_rgb(155, 155, 155));
|
||||
}
|
||||
|
||||
fn is_dark_ui(ui: &egui::Ui) -> bool {
|
||||
|
||||
@@ -68,7 +68,7 @@ impl ErrorScreen {
|
||||
// ── Hata mesajı ───────────────────────────────
|
||||
egui::Frame::none()
|
||||
.fill(egui::Color32::from_rgba_unmultiplied(0xF4, 0x43, 0x36, 18))
|
||||
.stroke(egui::Stroke::new(1.0, theme::c_error()))
|
||||
.stroke(egui::Stroke::new(1.0_f32, theme::c_error()))
|
||||
.rounding(egui::Rounding::same(8.0))
|
||||
.inner_margin(egui::Margin::same(12.0))
|
||||
.show(ui, |ui| {
|
||||
|
||||
@@ -2,327 +2,91 @@
|
||||
//!
|
||||
//! Her slayt: başlık, açıklama metni ve isteğe bağlı bir simge karakteri.
|
||||
//! Slaytlar otomatik olarak 6 saniyede bir değişir.
|
||||
//! Masaüstü ortamına göre farklı slayt setleri yüklenebilir.
|
||||
//! Slaytlar harici `slides/slides_{lang}.toml` dosyasından yüklenir.
|
||||
|
||||
use eframe::egui;
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
use rust_i18n::t;
|
||||
use std::borrow::Cow;
|
||||
|
||||
const SLIDE_INTERVAL: Duration = Duration::from_secs(6);
|
||||
|
||||
pub struct Slide {
|
||||
pub icon: &'static str,
|
||||
pub icon: String,
|
||||
pub title: Cow<'static, str>,
|
||||
pub description: Cow<'static, str>,
|
||||
}
|
||||
|
||||
/// Masaüstü ortamına göre slayt seti döner.
|
||||
/// ISO meta verisi ileride `/etc/livecd-desktop` gibi bir dosyadan okunabilir.
|
||||
pub fn slides_for_desktop(desktop: &str, branding_slides: &[crate::branding::SlideItem]) -> Vec<Slide> {
|
||||
if !branding_slides.is_empty() {
|
||||
branding_slides
|
||||
.iter()
|
||||
.filter(|item| item.desktop.is_empty() || item.desktop.to_lowercase() == desktop.to_lowercase())
|
||||
.map(|item| Slide {
|
||||
icon: Box::leak(item.icon.clone().into_boxed_str()),
|
||||
title: Cow::Owned(item.title.clone()),
|
||||
description: Cow::Owned(item.description.clone()),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let mut slides = default_slides();
|
||||
let specific_slides = match desktop {
|
||||
"budgie" => budgie_slides(),
|
||||
"cinnamon" => cinnamon_slides(),
|
||||
"gnome" => gnome_slides(),
|
||||
"kde" | "plasma" => kde_slides(),
|
||||
"lxde" => lxde_slides(),
|
||||
"lxqt" => lxqt_slides(),
|
||||
"lumina" => lumina_slides(),
|
||||
"mate" => mate_slides(),
|
||||
"xfce" => xfce_slides(),
|
||||
_ => vec![],
|
||||
};
|
||||
slides.extend(specific_slides);
|
||||
slides
|
||||
}
|
||||
}
|
||||
|
||||
fn default_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "pisilogo",
|
||||
title: t!("welcome_slide_title"),
|
||||
description: t!("welcome_slide_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "pisi",
|
||||
title: t!("package_manager_title"),
|
||||
description: t!("package_manager_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "community",
|
||||
title: t!("community_title"),
|
||||
description: t!("community_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "pisi_speed",
|
||||
title: t!("speed_title"),
|
||||
description: t!("speed_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "securety",
|
||||
title: t!("security_title"),
|
||||
description: t!("security_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "customization",
|
||||
title: t!("customization_title"),
|
||||
description: t!("customization_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn budgie_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "budgie",
|
||||
title: t!("budgie_title"),
|
||||
description: t!("budgie_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "budgie_applets",
|
||||
title: t!("budgie_applets_title"),
|
||||
description: t!("budgie_applets_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "budgie_raven",
|
||||
title: t!("budgie_raven_title"),
|
||||
description: t!("budgie_raven_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "budgie_gtk_theme",
|
||||
title: t!("budgie_gtk_theme_title"),
|
||||
description: t!("budgie_gtk_theme_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn cinnamon_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "cinnamon",
|
||||
title: t!("cinnamon_title"),
|
||||
description: t!("cinnamon_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "cinnamon_panel",
|
||||
title: t!("cinnamon_panel_title"),
|
||||
description: t!("cinnamon_panel_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "cinnamon_settings",
|
||||
title: t!("cinnamon_settings_title"),
|
||||
description: t!("cinnamon_settings_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "cinnamon_desklets",
|
||||
title: t!("cinnamon_desklets_title"),
|
||||
description: t!("cinnamon_desklets_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn gnome_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "gnome",
|
||||
title: t!("gnome_title"),
|
||||
description: t!("gnome_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "gnome_search",
|
||||
title: t!("gnome_search_title"),
|
||||
description: t!("gnome_search_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "gnome_extensions",
|
||||
title: t!("gnome_extensions_title"),
|
||||
description: t!("gnome_extensions_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn kde_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "kde",
|
||||
title: t!("kde_title"),
|
||||
description: t!("kde_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("dolphin_title"),
|
||||
description: t!("dolphin_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "kde_connect",
|
||||
title: t!("kde_connect_title"),
|
||||
description: t!("kde_connect_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "discover",
|
||||
title: t!("discover_title"),
|
||||
description: t!("discover_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn lxde_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "lxde",
|
||||
title: t!("lxde_title"),
|
||||
description: t!("lxde_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lxde_openbox",
|
||||
title: t!("lxde_openbox_title"),
|
||||
description: t!("lxde_openbox_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("lxde_pcmanfm_title"),
|
||||
description: t!("lxde_pcmanfm_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lxde_lxpanel",
|
||||
title: t!("lxde_lxpanel_title"),
|
||||
description: t!("lxde_lxpanel_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn lxqt_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "lxqt",
|
||||
title: t!("lxqt_title"),
|
||||
description: t!("lxqt_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lxqt_panel",
|
||||
title: t!("lxqt_panel_title"),
|
||||
description: t!("lxqt_panel_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("lxqt_pcmanfm_qt_title"),
|
||||
description: t!("lxqt_pcmanfm_qt_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn lumina_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "lumina",
|
||||
title: t!("lumina_title"),
|
||||
description: t!("lumina_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lumina_applications",
|
||||
title: t!("lumina_applications_title"),
|
||||
description: t!("lumina_applications_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lumina_theme",
|
||||
title: t!("lumina_theme_title"),
|
||||
description: t!("lumina_theme_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "lumina_panel",
|
||||
title: t!("lumina_panel_title"),
|
||||
description: t!("lumina_panel_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn mate_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "mate",
|
||||
title: t!("mate_title"),
|
||||
description: t!("mate_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("mate_caja_title"),
|
||||
description: t!("mate_caja_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "mate_settings",
|
||||
title: t!("mate_settings_title"),
|
||||
description: t!("mate_settings_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "mate_applications",
|
||||
title: t!("mate_applications_title"),
|
||||
description: t!("mate_applications_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn xfce_slides() -> Vec<Slide> {
|
||||
vec![
|
||||
Slide {
|
||||
icon: "xfce",
|
||||
title: t!("xfce_title"),
|
||||
description: t!("xfce_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "xfce_panel",
|
||||
title: t!("xfce_panel_title"),
|
||||
description: t!("xfce_panel_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "file_manager",
|
||||
title: t!("xfce_thunar_title"),
|
||||
description: t!("xfce_thunar_description"),
|
||||
},
|
||||
Slide {
|
||||
icon: "xfce_settings",
|
||||
title: t!("xfce_settings_title"),
|
||||
description: t!("xfce_settings_description"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
// ─── Slayt gösterisi widget'ı ───────────────────────────────────────────
|
||||
|
||||
pub struct Slideshow {
|
||||
slides: Vec<Slide>,
|
||||
current: usize,
|
||||
last_advance: Instant,
|
||||
/// branding.toml'dan gelen ikon-adı → `file://` URI eşlemesi
|
||||
icons: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Slideshow {
|
||||
pub fn new(desktop: &str, icons: HashMap<String, String>, branding_slides: Vec<crate::branding::SlideItem>) -> Self {
|
||||
pub fn new(desktop: &str, lang: &str) -> Self {
|
||||
let slides = Self::load_slides(desktop, lang);
|
||||
Self {
|
||||
slides: slides_for_desktop(desktop, &branding_slides),
|
||||
current: 0,
|
||||
slides,
|
||||
current: 0,
|
||||
last_advance: Instant::now(),
|
||||
icons,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_slides(desktop: &str, lang: &str) -> Vec<Slide> {
|
||||
let filenames = [
|
||||
format!("slides_{}.toml", lang),
|
||||
"slides.toml".to_string(),
|
||||
];
|
||||
let exe_dir = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.to_path_buf()))
|
||||
.unwrap_or_default();
|
||||
let search_dirs = [
|
||||
std::env::current_dir().unwrap_or_default(),
|
||||
exe_dir,
|
||||
std::path::PathBuf::from("/usr/share/yali"),
|
||||
std::path::PathBuf::from("/etc/yali"),
|
||||
];
|
||||
for dir in &search_dirs {
|
||||
for fname in &filenames {
|
||||
let path = dir.join("slides").join(fname);
|
||||
let content = match std::fs::read_to_string(&path) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
match toml::from_str::<crate::branding::SlidesConfig>(&content) {
|
||||
Ok(cfg) => {
|
||||
let slides: Vec<Slide> = cfg.items
|
||||
.into_iter()
|
||||
.filter(|item| item.desktop.is_empty() || item.desktop.to_lowercase() == desktop.to_lowercase())
|
||||
.map(|item| Slide {
|
||||
icon: item.icon,
|
||||
title: Cow::Owned(item.title),
|
||||
description: Cow::Owned(item.description),
|
||||
})
|
||||
.collect();
|
||||
eprintln!("[slides] {} -> {} slayt yüklendi", path.display(), slides.len());
|
||||
return slides;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[slides] {} ayrıştırılamadı: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("[slides] hiçbir slayt dosyası yüklenemedi");
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Her frame'de çağrılır. Otomatik ilerlemeyi ve çizimi yönetir.
|
||||
pub fn show(&mut self, ui: &mut egui::Ui) {
|
||||
if self.slides.is_empty() {
|
||||
ui.label(egui::RichText::new("Slayt yüklenemedi").size(14.0));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otomatik ilerleme
|
||||
if self.last_advance.elapsed() >= SLIDE_INTERVAL {
|
||||
self.current = (self.current + 1) % self.slides.len();
|
||||
@@ -338,15 +102,21 @@ impl Slideshow {
|
||||
|
||||
let slide = &self.slides[self.current];
|
||||
|
||||
// İkona karşılık gelen URI'yi branding haritasından al
|
||||
let resolved_uri = self.icons.get(slide.icon).cloned();
|
||||
// İkon: doğrudan dosya yolundan file:// URI'sine çevir
|
||||
let resolved_uri = if !slide.icon.is_empty() {
|
||||
Some(crate::branding::path_to_file_uri(&slide.icon))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Kart, kendisine ayrılan dikey alanı doldursun.
|
||||
let fill_h = ui.available_height().max(180.0);
|
||||
egui::Frame::none()
|
||||
.fill(crate::ui::theme::c_bg_widget())
|
||||
.rounding(egui::Rounding::same(10.0))
|
||||
.inner_margin(egui::Margin::same(24.0))
|
||||
.show(ui, |ui| {
|
||||
ui.set_min_height(180.0);
|
||||
ui.set_min_height(fill_h);
|
||||
ui.vertical_centered(|ui| {
|
||||
// Simge veya Görsel
|
||||
if let Some(uri) = resolved_uri {
|
||||
@@ -357,7 +127,7 @@ impl Slideshow {
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
egui::RichText::new(slide.icon)
|
||||
egui::RichText::new("\u{1f5bc}\u{fe0f}")
|
||||
.size(48.0),
|
||||
);
|
||||
}
|
||||
@@ -379,41 +149,11 @@ impl Slideshow {
|
||||
.color(crate::ui::theme::c_text_dim()),
|
||||
);
|
||||
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Nokta göstergesi
|
||||
ui.horizontal(|ui| {
|
||||
let dot_size = egui::vec2(8.0, 8.0);
|
||||
for i in 0..self.slides.len() {
|
||||
let color = if i == self.current {
|
||||
crate::ui::theme::c_accent()
|
||||
} else {
|
||||
crate::ui::theme::c_border()
|
||||
};
|
||||
let (rect, response) = ui.allocate_exact_size(dot_size, egui::Sense::click());
|
||||
ui.painter().circle_filled(rect.center(), 4.0, color);
|
||||
if response.clicked() {
|
||||
self.current = i;
|
||||
self.last_advance = Instant::now();
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn advance(&mut self) {
|
||||
self.current = (self.current + 1) % self.slides.len();
|
||||
self.last_advance = Instant::now();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.current == 0 {
|
||||
self.current = self.slides.len() - 1;
|
||||
} else {
|
||||
self.current -= 1;
|
||||
}
|
||||
self.last_advance = Instant::now();
|
||||
pub fn slide_count(&self) -> usize {
|
||||
self.slides.len()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ pub fn c_sidebar_text() -> Color32 { color_from_hex(SIDEBAR_TEXT.load(Ordering::
|
||||
pub fn c_footer_bg() -> Color32 { color_from_hex(BG_FOOTER.load(Ordering::Relaxed)) }
|
||||
pub fn c_root_bg() -> Color32 { color_from_hex(BG_ROOT.load(Ordering::Relaxed)) }
|
||||
pub fn c_keyboard_bg() -> Color32 { color_from_hex(KEYBOARD_BG.load(Ordering::Relaxed)) }
|
||||
pub fn select_disk_label() -> Color32 { color_from_hex(0xFFFFFF) }
|
||||
|
||||
pub fn set_theme_mode(is_dark: bool) {
|
||||
if is_dark {
|
||||
@@ -108,10 +109,85 @@ pub fn apply(ctx: &egui::Context, is_dark: bool) {
|
||||
ctx.set_visuals(build_visuals(is_dark));
|
||||
}
|
||||
|
||||
// fn setup_fonts(ctx: &egui::Context) {
|
||||
// let mut fonts = egui::FontDefinitions::default();
|
||||
|
||||
// // 1. ADIM: Sadece Metin (Text) Fontunu Bul ve Yükle
|
||||
// let text_font_paths = [
|
||||
// "/usr/share/fonts/TTF/OpenSans-Regular.ttf",
|
||||
// "/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
// "/usr/share/fonts/exo-1/Exo-Regular.otf",
|
||||
// "/usr/share/fonts/exo-1/Exo2-Medium.ttf",
|
||||
// "/usr/share/fonts/exo-1/Exo-Medium.otf",
|
||||
// ];
|
||||
|
||||
// for path in text_font_paths {
|
||||
// if let Ok(data) = std::fs::read(path) {
|
||||
// fonts.font_data.insert(
|
||||
// "system_text_font".to_owned(),
|
||||
// egui::FontData::from_owned(data),
|
||||
// );
|
||||
|
||||
// // Metin fontunu en başa (0. indeks) koyuyoruz ki varsayılan olarak bu kullanılsın
|
||||
// fonts
|
||||
// .families
|
||||
// .entry(egui::FontFamily::Proportional)
|
||||
// .or_default()
|
||||
// .insert(0, "system_text_font".to_owned());
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 2. ADIM: Renkli Emoji Fontunu Ara ve "Fallback" (Yedek) Olarak Sona Ekle
|
||||
// // ISO ortamlarında Noto Color Emoji genellikle bu yollardan birinde olur
|
||||
// let emoji_font_paths = [
|
||||
// "/usr/share/fonts/noto/NotoColorEmoji.ttf",
|
||||
// ];
|
||||
|
||||
// for path in emoji_font_paths {
|
||||
// if let Ok(data) = std::fs::read(path) {
|
||||
// fonts.font_data.insert(
|
||||
// "system_emoji_font".to_owned(),
|
||||
// egui::FontData::from_owned(data),
|
||||
// );
|
||||
|
||||
// // ÖNEMLİ: Emojiyi listenin SONUNA (push ile) ekliyoruz.
|
||||
// // Böylece egui normal harfleri sistem fontuyla yazacak,
|
||||
// // harf sistem fontunda yoksa (yani emojiyse) gidip bu fonttan alacak.
|
||||
// fonts
|
||||
// .families
|
||||
// .entry(egui::FontFamily::Proportional)
|
||||
// .or_default()
|
||||
// .push("system_emoji_font".to_owned());
|
||||
|
||||
// fonts
|
||||
// .families
|
||||
// .entry(egui::FontFamily::Monospace)
|
||||
// .or_default()
|
||||
// .insert(0, "system_emoji_font".to_owned());
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 3. ADIM: Remixicon ikon fontunu projeye göm (PUA bölgesindeki karakterler için fallback)
|
||||
// let remixicon_data = include_bytes!("../../assets/remixicon.ttf");
|
||||
// fonts.font_data.insert(
|
||||
// "remixicon".to_owned(),
|
||||
// egui::FontData::from_owned(remixicon_data.to_vec()),
|
||||
// );
|
||||
// fonts
|
||||
// .families
|
||||
// .entry(egui::FontFamily::Proportional)
|
||||
// .or_default()
|
||||
// .push("remixicon".to_owned());
|
||||
|
||||
// ctx.set_fonts(fonts);
|
||||
// }
|
||||
|
||||
fn setup_fonts(ctx: &egui::Context) {
|
||||
let mut fonts = egui::FontDefinitions::default();
|
||||
|
||||
// 1. ADIM: Sadece Metin (Text) Fontunu Bul ve Yükle
|
||||
// 1. ADIM: Metin ve Monospace için Sistem Fontu Yükleme
|
||||
let text_font_paths = [
|
||||
"/usr/share/fonts/TTF/OpenSans-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
@@ -120,25 +196,59 @@ fn setup_fonts(ctx: &egui::Context) {
|
||||
"/usr/share/fonts/exo-1/Exo-Medium.otf",
|
||||
];
|
||||
|
||||
let mut _text_font_loaded = false;
|
||||
|
||||
for path in text_font_paths {
|
||||
if let Ok(data) = std::fs::read(path) {
|
||||
fonts.font_data.insert(
|
||||
"system_text_font".to_owned(),
|
||||
egui::FontData::from_owned(data),
|
||||
egui::FontData::from_owned(data).into(), // Sürüme göre .into() veya std::sync::Arc::new(...)
|
||||
);
|
||||
|
||||
// Metin fontunu en başa (0. indeks) koyuyoruz ki varsayılan olarak bu kullanılsın
|
||||
|
||||
// Proportional (Orantılı) fontun başına ekle
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.insert(0, "system_text_font".to_owned());
|
||||
|
||||
// Monospace için de bu fontu varsayılan olarak ekle (özel bir monospace bulunamadıysa)
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "system_text_font".to_owned());
|
||||
|
||||
_text_font_loaded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. ADIM: Renkli Emoji Fontunu Ara ve "Fallback" (Yedek) Olarak Sona Ekle
|
||||
// ISO ortamlarında Noto Color Emoji genellikle bu yollardan birinde olur
|
||||
// Ek Güvence: Özel bir Monospace fontu varsa öncelikli olarak onu ara
|
||||
let mono_font_paths = [
|
||||
"/usr/share/fonts/TTF/Hack-Regular.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
||||
"/usr/share/fonts/noto/NotoSansMono-Regular.ttf",
|
||||
];
|
||||
|
||||
for path in mono_font_paths {
|
||||
if let Ok(data) = std::fs::read(path) {
|
||||
fonts.font_data.insert(
|
||||
"system_mono_font".to_owned(),
|
||||
egui::FontData::from_owned(data).into(),
|
||||
);
|
||||
|
||||
// Monospace ailesinin 0. indeksine yerleştir
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "system_mono_font".to_owned());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. ADIM: Color Emoji Fontunu Yedek (Fallback) Olarak EKLE
|
||||
let emoji_font_paths = [
|
||||
"/usr/share/fonts/noto/NotoColorEmoji.ttf",
|
||||
];
|
||||
@@ -147,27 +257,44 @@ fn setup_fonts(ctx: &egui::Context) {
|
||||
if let Ok(data) = std::fs::read(path) {
|
||||
fonts.font_data.insert(
|
||||
"system_emoji_font".to_owned(),
|
||||
egui::FontData::from_owned(data),
|
||||
egui::FontData::from_owned(data).into(),
|
||||
);
|
||||
|
||||
// ÖNEMLİ: Emojiyi listenin SONUNA (push ile) ekliyoruz.
|
||||
// Böylece egui normal harfleri sistem fontuyla yazacak,
|
||||
// harf sistem fontunda yoksa (yani emojiyse) gidip bu fonttan alacak.
|
||||
// Her iki aileye de SONA (push) ekle, böylece metinler bozulmaz
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.push("system_emoji_font".to_owned());
|
||||
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.insert(0, "system_emoji_font".to_owned());
|
||||
.push("system_emoji_font".to_owned()); // insert(0, ...) yerine push yapıldı!
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ADIM: Remixicon İkon Fontunu Göm (PUA)
|
||||
let remixicon_data = include_bytes!("../../assets/remixicon.ttf");
|
||||
fonts.font_data.insert(
|
||||
"remixicon".to_owned(),
|
||||
egui::FontData::from_owned(remixicon_data.to_vec()).into(),
|
||||
);
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Proportional)
|
||||
.or_default()
|
||||
.push("remixicon".to_owned());
|
||||
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Monospace)
|
||||
.or_default()
|
||||
.push("remixicon".to_owned());
|
||||
|
||||
ctx.set_fonts(fonts);
|
||||
}
|
||||
|
||||
@@ -175,7 +302,7 @@ fn setup_fonts(ctx: &egui::Context) {
|
||||
|
||||
fn build_visuals(is_dark: bool) -> Visuals {
|
||||
let mut v = if is_dark { Visuals::dark() } else { Visuals::light() };
|
||||
|
||||
|
||||
// Genel arka planlar
|
||||
v.window_fill = c_bg_dark();
|
||||
v.panel_fill = c_bg_panel();
|
||||
@@ -187,7 +314,7 @@ fn build_visuals(is_dark: bool) -> Visuals {
|
||||
v.override_text_color = None;
|
||||
|
||||
// ─── KENARLIKLAR (Calamares Uyumu için İnce İnce Yumuşatıldı) ───────────────────
|
||||
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, c_border());
|
||||
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0_f32, c_border());
|
||||
|
||||
// ─── WIDGET DOLGULARI (Burası ComboBox ve Butonların Kaderini Belirliyor) ───────
|
||||
v.widgets.noninteractive.bg_fill = c_bg_widget();
|
||||
@@ -205,7 +332,7 @@ fn build_visuals(is_dark: bool) -> Visuals {
|
||||
crate::ui::combobox_stil::apply_visuals(&mut v);
|
||||
|
||||
// ─── METİN RENKLERİ ────────────────────────────────────────────────────────────
|
||||
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, c_text());
|
||||
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0_f32, c_text());
|
||||
|
||||
// ─── YUVARLAK KÖŞELER (Zaten 6.0'dı, Tam İstediğin Calamares Kıvamında) ─────────
|
||||
v.widgets.noninteractive.rounding = Rounding::same(6.0);
|
||||
@@ -215,13 +342,13 @@ fn build_visuals(is_dark: bool) -> Visuals {
|
||||
|
||||
// Seçim rengi
|
||||
v.selection.bg_fill = c_accent_dim();
|
||||
v.selection.stroke = Stroke::new(1.0, c_accent());
|
||||
v.selection.stroke = Stroke::new(1.0_f32, c_accent());
|
||||
|
||||
// Hyperlink
|
||||
v.hyperlink_color = c_accent();
|
||||
|
||||
// Pencere kenarlığı
|
||||
v.window_stroke = Stroke::new(1.0, c_border());
|
||||
v.window_stroke = Stroke::new(1.0_f32, c_border());
|
||||
v.window_rounding = Rounding::same(8.0);
|
||||
|
||||
v.interact_cursor = Some(egui::CursorIcon::PointingHand);
|
||||
@@ -231,13 +358,14 @@ fn build_visuals(is_dark: bool) -> Visuals {
|
||||
|
||||
fn build_style() -> egui::Style {
|
||||
let mut style = egui::Style::default();
|
||||
|
||||
|
||||
// Genel boşluk ve boyutlar
|
||||
style.spacing.item_spacing = Vec2::new(8.0, 6.0);
|
||||
style.spacing.button_padding = Vec2::new(12.0, 6.0);
|
||||
style.spacing.indent = 16.0;
|
||||
style.spacing.scroll.bar_width = 14.0;
|
||||
|
||||
|
||||
// Metin boyutları
|
||||
style.text_styles = [
|
||||
(egui::TextStyle::Small, FontId::proportional(11.0)),
|
||||
@@ -268,7 +396,7 @@ pub fn section_heading(ui: &mut egui::Ui, text: &str) {
|
||||
let (rect, _) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 1.0), egui::Sense::hover());
|
||||
ui.painter().line_segment(
|
||||
[rect.left_center(), rect.right_center()],
|
||||
Stroke::new(1.0, c_accent_dim()),
|
||||
Stroke::new(1.0_f32, c_accent_dim()),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
@@ -277,14 +405,16 @@ pub fn section_heading(ui: &mut egui::Ui, text: &str) {
|
||||
pub fn error_box(ui: &mut egui::Ui, msg: &str) {
|
||||
egui::Frame::none()
|
||||
.fill(Color32::from_rgba_unmultiplied(0xF4, 0x43, 0x36, 20))
|
||||
.stroke(Stroke::new(1.0, c_error()))
|
||||
.stroke(Stroke::new(1.0_f32, c_error()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::Image::new(egui::include_image!("../../assets/icons/fail-icon.svg"))
|
||||
.max_width(18.0).max_height(18.0)
|
||||
.tint(c_error()));
|
||||
ui.label(
|
||||
egui::RichText::new("\u{F4C6}")
|
||||
.color(c_error())
|
||||
.size(18.0) // SVG max_width/height yerine font boyutu
|
||||
);
|
||||
ui.colored_label(c_error(), msg);
|
||||
});
|
||||
});
|
||||
@@ -294,7 +424,7 @@ pub fn error_box(ui: &mut egui::Ui, msg: &str) {
|
||||
pub fn warning_box(ui: &mut egui::Ui, msg: &str) {
|
||||
egui::Frame::none()
|
||||
.fill(Color32::from_rgba_unmultiplied(0xFF, 0x98, 0x00, 20))
|
||||
.stroke(Stroke::new(1.0, c_warning()))
|
||||
.stroke(Stroke::new(1.0_f32, c_warning()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
@@ -311,7 +441,7 @@ pub fn warning_box(ui: &mut egui::Ui, msg: &str) {
|
||||
pub fn success_box(ui: &mut egui::Ui, msg: &str) {
|
||||
egui::Frame::none()
|
||||
.fill(Color32::from_rgba_unmultiplied(0x4C, 0xAF, 0x50, 20))
|
||||
.stroke(Stroke::new(1.0, c_success()))
|
||||
.stroke(Stroke::new(1.0_f32, c_success()))
|
||||
.rounding(Rounding::same(6.0))
|
||||
.inner_margin(egui::Margin::same(10.0))
|
||||
.show(ui, |ui| {
|
||||
|
||||