This commit is contained in:
2026-05-22 23:35:27 +03:00
parent 70c0304fea
commit a1ae4efede
45 changed files with 6769 additions and 93 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "yali-rs"
version = "0.2.0"
edition = "2021"
[dependencies]
eframe = "0.27"
egui_extras = { version = "0.27", features = ["image"] }
image = { version = "0.24", features = ["png"] }
serde = { version = "1.0", features = ["derive"] }
toml = "0.8" # answer file ayrıştırma
zbus = "4.0"
tokio = { version = "1.0", features = ["full"] }
sysinfo = "0.30"
rust-i18n = "3.1"
lazy_static = "1.4"
async-trait = "0.1"
[patch.crates-io]
# Yerel mudur veya comar-api kütüphaneleri buraya eklenebilir
+217
View File
@@ -0,0 +1,217 @@
# ───────────────────────────────────────────────────────────────────────────
# Yali-RS — PisiLinux Kurulum Aracı
# Makefile (GNU Make 4.x)
# ───────────────────────────────────────────────────────────────────────────
BINARY := yali-rs
VERSION := 0.2.0
# Kurulum yolları (DESTDIR ile paket yöneticisi uyumlu)
PREFIX ?= /usr
BINDIR ?= $(PREFIX)/bin
DATADIR ?= $(PREFIX)/share/$(BINARY)
LOCALEDIR ?= $(DATADIR)/locales
ASSETSDIR ?= $(DATADIR)/assets
DESKTOPDIR ?= $(PREFIX)/share/applications
PIXMAPDIR ?= $(PREFIX)/share/pixmaps
# Cargo hedefleri
RELEASE_BIN := target/release/$(BINARY)
DEBUG_BIN := target/debug/$(BINARY)
# Renk çıktısı (terminal destekliyorsa)
BOLD := $(shell tput bold 2>/dev/null)
GREEN := $(shell tput setaf 2 2>/dev/null)
YELLOW := $(shell tput setaf 3 2>/dev/null)
RESET := $(shell tput sgr0 2>/dev/null)
.PHONY: all build release debug run demo auto check fmt lint test \
install uninstall clean distclean help
# ── Varsayılan hedef ────────────────────────────────────────────────────────
all: release
# ── Derleme ─────────────────────────────────────────────────────────────────
## Üretim ikili dosyasını derle (release modu, LTO etkin)
release:
@echo "$(BOLD)$(GREEN)▶ Derleniyor: release$(RESET)"
cargo build --release
## Geliştirme ikili dosyasını derle (debug modu, hızlı derleme)
debug:
@echo "$(BOLD)$(YELLOW)▶ Derleniyor: debug$(RESET)"
cargo build
## 'build' takma adı → debug derleme
build: debug
# ── Çalıştırma ───────────────────────────────────────────────────────────────
## Grafik arayüzü başlat (release)
run: release
@echo "$(BOLD)$(GREEN)▶ Başlatılıyor: GUI modu$(RESET)"
./$(RELEASE_BIN)
## Grafik arayüzü debug modda başlat
run-debug: debug
./$(DEBUG_BIN)
## Demo modu: gerçek komutlar çalıştırılmaz; UI akışını test etmek için kullanılır.
## Kurulum ekranında slaytlar arasında 5 sn beklenir.
demo: release
@echo "$(BOLD)$(GREEN)▶ Başlatılıyor: Demo modu (--demo)$(RESET)"
./$(RELEASE_BIN) --demo
## Demo modu (debug derlemesiyle — daha hızlı başlar, geliştirme için)
demo-debug: debug
./$(DEBUG_BIN) --demo
## Otomatik kurulum modu: answer-example.toml kullanılır (DİKKAT: gerçek disk!)
auto: release
@echo "$(BOLD)$(YELLOW)▶ Başlatılıyor: Otomatik kurulum modu$(RESET)"
@echo "$(YELLOW)UYARI: answer-example.toml içindeki diski kontrol edin!$(RESET)"
./$(RELEASE_BIN) --auto-install answer-example.toml
## Otomatik kurulum — yalnızca demo; gerçek komutlar çalışmaz
auto-demo: release
./$(RELEASE_BIN) --auto-install answer-example.toml --demo
# ── Kalite Kontrol ───────────────────────────────────────────────────────────
## Kod derleme kontrolü (ikili üretmez)
check:
@echo "$(BOLD)▶ Kontrol ediliyor$(RESET)"
cargo check
## Kod formatlama (yerinde düzenler)
fmt:
@echo "$(BOLD)▶ Formatlanıyor$(RESET)"
cargo fmt --all
## Formatlama kontrolü (CI için; değişiklik yapılmaz)
fmt-check:
cargo fmt --all -- --check
## Clippy lint (uyarıları hata say)
lint:
@echo "$(BOLD)▶ Lint: clippy$(RESET)"
cargo clippy -- -D warnings
## Birim testler
test:
@echo "$(BOLD)▶ Testler çalıştırılıyor$(RESET)"
cargo test
## Tam CI kontrolü: fmt-check + clippy + test
ci: fmt-check lint test
@echo "$(BOLD)$(GREEN)✓ CI geçti$(RESET)"
# ── Kurulum ──────────────────────────────────────────────────────────────────
## Sisteme kur (sudo gerekebilir; DESTDIR ile paket kurulumu desteklenir)
install: release
@echo "$(BOLD)$(GREEN)▶ Kuruluyor: $(DESTDIR)$(BINDIR)/$(BINARY)$(RESET)"
# İkili dosya
install -Dm755 $(RELEASE_BIN) $(DESTDIR)$(BINDIR)/$(BINARY)
# Varlıklar (görüntüler, SVG'ler)
install -dm755 $(DESTDIR)$(ASSETSDIR)
cp -r assets/* $(DESTDIR)$(ASSETSDIR)/
# Dil dosyaları
install -dm755 $(DESTDIR)$(LOCALEDIR)
install -m644 locales/*.toml $(DESTDIR)$(LOCALEDIR)/
# .desktop girdisi
install -dm755 $(DESTDIR)$(DESKTOPDIR)
@printf '[Desktop Entry]\nName=YALI Installer\nComment=PisiLinux Kurulum Aracı\nExec=$(BINDIR)/$(BINARY)\nIcon=yali-installer\nType=Application\nCategories=System;\nTerminal=false\n' \
> $(DESTDIR)$(DESKTOPDIR)/$(BINARY).desktop
chmod 644 $(DESTDIR)$(DESKTOPDIR)/$(BINARY).desktop
# Logo (pixmap)
install -dm755 $(DESTDIR)$(PIXMAPDIR)
install -m644 assets/pisi-logo-light.png $(DESTDIR)$(PIXMAPDIR)/yali-installer.png
@echo "$(BOLD)$(GREEN)✓ Kurulum tamamlandı$(RESET)"
## Sistemden kaldır
uninstall:
@echo "$(BOLD)$(YELLOW)▶ Kaldırılıyor$(RESET)"
rm -f $(DESTDIR)$(BINDIR)/$(BINARY)
rm -rf $(DESTDIR)$(DATADIR)
rm -f $(DESTDIR)$(DESKTOPDIR)/$(BINARY).desktop
rm -f $(DESTDIR)$(PIXMAPDIR)/yali-installer.png
@echo "$(BOLD)$(GREEN)✓ Kaldırıldı$(RESET)"
# ── Temizlik ─────────────────────────────────────────────────────────────────
## Derleme çıktılarını temizle (Cargo önbelleği korunur)
clean:
cargo clean
## Derin temizlik: derleme çıktıları + Cargo registry önbelleği
distclean: clean
rm -rf ~/.cargo/registry/cache
# ── Paketleme ────────────────────────────────────────────────────────────────
## Kaynak arşivi oluştur (git archive veya tar)
dist:
@echo "$(BOLD)▶ Arşiv oluşturuluyor: $(BINARY)-$(VERSION).tar.gz$(RESET)"
@if git rev-parse --git-dir > /dev/null 2>&1; then \
git archive --format=tar.gz --prefix=$(BINARY)-$(VERSION)/ HEAD \
-o $(BINARY)-$(VERSION).tar.gz; \
else \
tar -czf $(BINARY)-$(VERSION).tar.gz \
--transform 's|^|$(BINARY)-$(VERSION)/|' \
--exclude='.git' --exclude='target' \
.; \
fi
@echo "$(BINARY)-$(VERSION).tar.gz"
## Sürüm bilgisini yazdır
version:
@echo "$(BINARY) $(VERSION)"
@cargo --version
@rustc --version
# ── Yardım ───────────────────────────────────────────────────────────────────
## Bu yardım metnini göster
help:
@echo ""
@echo "$(BOLD)Yali-RS $(VERSION) — PisiLinux Kurulum Aracı$(RESET)"
@echo ""
@echo "$(BOLD)Derleme:$(RESET)"
@echo " make release Üretim ikili dosyasını derle (varsayılan)"
@echo " make debug Debug modda derle"
@echo " make check Derleme kontrolü (ikili üretmez)"
@echo ""
@echo "$(BOLD)Çalıştırma:$(RESET)"
@echo " make run GUI başlat (release)"
@echo " make demo Demo modu — gerçek komutlar çalışmaz"
@echo " make demo-debug Demo modu — debug derlemesiyle"
@echo " make auto Otomatik kurulum (answer-example.toml)"
@echo " make auto-demo Otomatik kurulum demo modu"
@echo ""
@echo "$(BOLD)Kalite Kontrol:$(RESET)"
@echo " make fmt Kodu formatla"
@echo " make fmt-check Formatlama kontrolü (CI)"
@echo " make lint Clippy lint"
@echo " make test Testleri çalıştır"
@echo " make ci Tam CI: fmt-check + lint + test"
@echo ""
@echo "$(BOLD)Kurulum:$(RESET)"
@echo " make install Sisteme kur (DESTDIR destekler)"
@echo " make uninstall Sistemden kaldır"
@echo ""
@echo "$(BOLD)Paketleme:$(RESET)"
@echo " make dist Kaynak arşivi oluştur"
@echo " make version Sürüm bilgisini göster"
@echo ""
@echo "$(BOLD)Örnekler:$(RESET)"
@echo " make install DESTDIR=/tmp/pkg PREFIX=/usr"
@echo " make auto # answer-example.toml diskini kontrol edin!"
@echo ""
+3
View File
@@ -0,0 +1,3 @@
# YALI (Yet Another Linux Installer) Rewrite
Rust and egui based YALI which written python and qt originally. It is the installer that sets up the system by installing packages from the installation media (eg. CD, network, Internet) and carrying out the basic configuration. YALI is intended to be smooth in operation and fast in performance.
-93
View File
@@ -1,93 +0,0 @@
# yali-rs
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
* [Create](https://docs.gitlab.com/user/project/repository/web_editor/#create-a-file) or [upload](https://docs.gitlab.com/user/project/repository/web_editor/#upload-a-file) files
* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.com/ayhanyalcinsoy/yali-rs.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools
* [Set up project integrations](https://gitlab.com/ayhanyalcinsoy/yali-rs/-/settings/integrations)
## Collaborate with your team
* [Invite team members and collaborators](https://docs.gitlab.com/user/project/members/)
* [Create a new merge request](https://docs.gitlab.com/user/project/merge_requests/creating_merge_requests/)
* [Automatically close issues from merge requests](https://docs.gitlab.com/user/project/issues/managing_issues/#closing-issues-automatically)
* [Enable merge request approvals](https://docs.gitlab.com/user/project/merge_requests/approvals/)
* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
## Test and Deploy
Use the built-in continuous integration in GitLab.
* [Get started with GitLab CI/CD](https://docs.gitlab.com/ci/quick_start/)
* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/user/application_security/sast/)
* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/topics/autodevops/requirements/)
* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/user/clusters/agent/)
* [Set up protected environments](https://docs.gitlab.com/ci/environments/protected_environments/)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
+76
View File
@@ -0,0 +1,76 @@
# 🛠 Yali-RS (Rust Installer) Yol Haritası
Bu dosya, PisiLinux için geliştirilen yeni Rust tabanlı yükleyicinin (Yali) gelişim sürecini takip eder.
## 1. Temel Altyapı
- [x] Proje iskeletinin oluşturulması (egui + eframe)
- [x] `InstallerStep` trait yapısının kurulması
- [x] Global State yönetimi (Dil, kullanıcı, disk bilgileri)
- [x] **i18n (Çoklu Dil) desteğinin entegre edilmesi**
- [x] Navigasyon sistemi (Geri/İleri/İptal butonları yönetimi)
## 2. Kurulum Modülleri (Calamares Benzeri)
- [x] **Hoşgeldiniz (Welcome):** Sistem gereksinim kontrolü ve dil seçimi.
- [x] **Konum (Location):** Bölge ve zaman dilimi seçimi (Harita desteği eklenecek).
- [x] **Klavye (Keyboard):** Klavye düzeni seçimi ve test alanı.
- [x] **Bölümleme (Partition):**
- [x] Disklerin listelenmesi (sysinfo/block-utils).
- [x] Otomatik bölümleme (Tüm diski sil).
- [x] Manuel bölümleme (GParted/KPMCore benzeri yapı).
- [x] Mevcut bölümleri tablo olarak göster.
- [x] Bölüm silme / oluşturma (diyalog penceresi).
- [x] Bağlama noktası (mountpoint) ve dosya sistemi seçimi.
- [x] EFI, swap, kök (/) bölümlerinin manuel atanması.
- [x] Değişiklik planı gösterimi (özet tablo).
- [x] GlobalState'e `custom_partitions: Vec<CustomPartition>` eklenmesi.
- [ ] Yeniden boyutlandırma (mevcut bölümleri koruyarak) — gelecek.
- [x] **Kullanıcılar (Users):** Kullanıcı adı, parola ve hostname ayarları.
- [x] **Özet (Summary):** Yapılacak işlemlerin son kontrolü.
- [x] **Kurulum (Execution):** Gerçek işlemlerin (rsync, chroot, grub) yapıldığı ekran.
- [x] **Sonuç (Finish):** Yeniden başlatma veya masaüstüne dönme.
## 3. Sistem Arka Plan İşlemleri (Job Queue)
- [x] Disklerin formatlanması (mkfs.ext4, mkswap vb.).
- [x] Sistem dosyalarının kopyalanması (`rsync` veya `squashfs` açma).
- [x] Chroot içine girerek yapılandırma yapılması.
- [x] Bootloader (GRUB) kurulumu.
- [x] COMAR üzerinden servislerin ve tetikleyicilerin çalıştırılması.
## 4. Görsel ve Kullanıcı Deneyimi (UX/UI)
- [x] PisiLinux temasına uygun egui stillerinin uygulanması.
- [x] Kurulum sırasında slayt gösterisi (Slideshow).
- [x] Slayt gösterisi öncelikle ortak slaytları göstermeli, sonra masaüstü ortamına özel slaytları göstermeli.
- [x] ISO hangi masaüstü ortamını içeriyorsa onun slaytı gösterilmeli.
- [x] Hata yakalama ve kullanıcıya bildirme ekranları.
## 5. Test ve Dağıtım
- [x] --demo parameteresi ile komutlar aktif çalışmadan sadece görsel arayüzde başlangıçtan bitişe kadar gezinme, slaytlar arasında 5 sn kadar bekleme süresi. Böylece arayüz testi yapılmış olur.
- [ ] ISO ortamında çalışma testleri.
- [ ] Farklı disk yapılarında (UEFI/Legacy, NVMe/SATA) testler.
- [x] Otomatik kurulum (Auto-install/Answer file) desteği.
## 6. Kritik Düzeltmeler (Gerçek Kurulum İçin Zorunlu)
> Bu bölüm, gerçek Live ortamda başarılı kurulum için düzeltilmesi gereken kritik sorunları içerir.
- [x] **[KRİTİK] `arch-chroot` bağımlılığını kaldır:**
- [x] `MountBindJob` eklendi: `/proc`, `/sys`, `/dev`, `/dev/pts` bind mount'ları yapılıyor.
- [x] `UnmountBindJob` eklendi: kurulum sonunda bind mount'lar temizleniyor.
- [x] Tüm `run_cmd("arch-chroot", ...)` çağrıları `run_chroot(...)` helper ile değiştirildi.
- [x] Job Queue'ya `MountBindJob` başına, `UnmountBindJob` sonuna eklendi.
- [x] **[KRİTİK] initramfs oluşturma adımı:**
- [x] `GenerateInitramfsJob` eklendi: önce `dracut`, hata olursa `mkinitcpio` deneniyor.
- [x] GRUB kurulumundan önce Job Queue'ya eklendi.
- [x] **[ÖNEMLİ] Kaynak dizin hardcode — dinamik hale getirildi:**
- [x] `detect_source_dir()` fonksiyonu yazıldı (birden fazla aday dizin deneniyor).
- [x] `build_full_job_queue()` içinde otomatik kullanılıyor.
- [x] **[ÖNEMLİ] GRUB kurulumu chroot içine taşındı:**
- [x] `grub-install` artık chroot içinde çalışıyor (hem UEFI hem BIOS modu).
- [x] `grub-mkconfig` de chroot içinde çalışıyor.
- [x] **[ÖNEMLİ] Lokalizasyon anahtarları güncellendi:**
- [x] Yeni job isimleri (mount_bind, umount_bind, initramfs, bootloader) eklendi.
- [x] Manuel bölümleme UI anahtarları eklendi (en.toml + tr.toml).
- [x] **[ÖNEMLİ] Manuel bölümleme için Job desteği — state hazır:**
- [x] `CustomPartition`, `FsType` tipleri `installer.rs`'e eklendi.
- [x] `GlobalState`'e `custom_partitions: Vec<CustomPartition>` eklendi.
- [ ] `CustomPartitionJob`: kullanıcının seçtiği planı uygulayan Job — gelecek.
- [ ] `build_custom_job_queue()` fabrika fonksiyonu — gelecek.
+38
View File
@@ -0,0 +1,38 @@
# Yali-RS Otomatik Kurulum — Örnek Answer File
# Kullanım: yali-rs --auto-install answer-example.toml
[install]
# Hedef disk aygıtı (zorunlu)
# Örnekler: "/dev/sda" "/dev/nvme0n1" "/dev/vda"
disk = "/dev/sda"
# Zaman dilimi (varsayılan: Europe/Istanbul)
timezone = "Europe/Istanbul"
# Sistem locale (varsayılan: tr_TR.UTF-8)
locale = "tr_TR.UTF-8"
# Klavye düzeni (varsayılan: tr)
keyboard = "tr"
# Klavye varyantı — isteğe bağlı
# "f" → Türkçe F | "" → Q (varsayılan)
keyboard_variant = ""
# Kaynak dizin — canlı sistem (genellikle değiştirilmez)
source = "/run/livecd/squashfs-root/"
# Bağlama noktası (genellikle değiştirilmez)
mount = "/mnt"
[user]
# Kullanıcı adı: küçük harf, rakam, - ve _ ; max 32 karakter
username = "pisi"
# Şifre: minimum 6 karakter
# UYARI: Bu dosyayı güvenli tutun; şifre düz metin olarak saklanır.
password = "changeme123"
# Bilgisayar adı: harf, rakam ve - ; tire ile başlayamaz/bitemez
hostname = "pisilinux"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+1054
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Name=YALI Installation Tool
Name[tr]=YALI Kurulum Aracı
GenericName=Yali
Comment=YALI Installation Tool
Comment[tr]=YALI Kurulum Aracı
Icon=yali
Type=Application
Exec=yali-rs
Categories=System;
Terminal=false
StartupNotify=true
+331
View File
@@ -0,0 +1,331 @@
# ── Navigation ────────────────────────────────────────────
steps = "Steps"
back = "Back"
next = "Next"
cancel = "Cancel"
confirm = "Confirm"
# ── Welcome ───────────────────────────────────────────────
welcome = "Welcome"
welcome_title = "Welcome to YALI Installation Tool"
welcome_description = "This tool will guide you through the installation process."
select_language = "Select Language:"
# ── Location ──────────────────────────────────────────────
location = "Location"
location_title = "Region & Timezone"
location_description = "Select your region and timezone."
search_timezone = "Search timezone:"
# ── Keyboard ──────────────────────────────────────────────
keyboard = "Keyboard"
keyboard_title = "Keyboard Layout"
keyboard_description = "Select your keyboard layout and test it below."
keyboard_layout_label = "Layout"
keyboard_test_label = "Test Area"
keyboard_test_hint = "Type here to test your keyboard…"
keyboard_selected = "Selected"
# ── Partition ─────────────────────────────────────────────
partition = "Partitioning"
partition_title = "Disk Selection"
partition_description = "Select the disk you want to install on."
select_disk_label = "Select disk:"
no_disks_found = "No disks found. Please attach a storage device."
rescan_disks = "Rescan"
erase_disk_label = "Use entire disk (automatic)"
manual_partition_label = "Manual partitioning"
manual_partition_wip = "Manual partitioning is under development."
erase_warning = "WARNING: All data on the selected disk will be erased"
# ── Users ─────────────────────────────────────────────────
users = "Users"
users_title = "User Account"
users_description = "Create a user account for your system."
username_label = "Username"
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."
hostname_invalid = "Invalid hostname. Only letters, digits, and hyphens are allowed."
passwords_no_match = "Passwords do not match"
# ── Summary ───────────────────────────────────────────────
summary = "Summary"
summary_title = "Installation Summary"
summary_description = "Review your choices. Clicking Next will start the installation."
summary_language = "Language"
summary_timezone = "Timezone"
summary_keyboard = "Keyboard layout"
summary_disk = "Disk"
summary_username = "Username"
summary_hostname = "Computer name"
summary_erase_mode = "Entire disk will be erased"
summary_manual_mode = "Manual partitioning"
summary_not_selected = "Not selected"
summary_warning = "⚠ Warning: Clicking Next will permanently erase all data on the selected disk!"
# ── Execution ─────────────────────────────────────────────
execution = "Installation"
execution_title = "Installing…"
execution_description = "Please wait. This may take a few minutes."
start_install = "Start Installation"
install_starting = "Starting installation…"
# ── Slides for PISILINUX ────────────────────────────────────────────
welcome_slide_title = "Welcome to Pisi GNU/Linux"
welcome_slide_description = "Pisi is a Turkish-supported GNU/Linux distribution that combines speed, stability, and ease of use."
package_manager_title = "PISI Package Manager"
package_manager_description = "Access thousands of packages with a single command. Install anything you need easily with `pisi install <package>`."
community_title = "Community Support"
community_description = "Our active community at forum.pisilinux.org is ready to answer your questions. Our wiki pages are always accessible."
speed_title = "Speed & Performance"
speed_description = "Thanks to an optimized kernel and startup services, PisiLinux boots in seconds and uses minimal resources."
security_title = "Security First"
security_description = "Keep your system secure with regular security updates, AppArmor profiles, and secure default settings."
customization_title = "Customization"
customization_description = "Tailor everything to your taste — from the desktop environment and theme colors to panel layout and keyboard shortcuts."
# ── Slides for BUDGIE ────────────────────────────────────────────
budgie_title = "PisiLinux Budgie"
budgie_description = "A modern, clean, and elegant Budgie desktop. Built on the GNOME stack and user-friendly."
budgie_applets_title = "Budgie Desktop Applets"
budgie_applets_description = "Easily add tools like weather, system monitor, and calendar via Budgie's panel applets."
budgie_raven_title = "Raven Notification Center"
budgie_raven_description = "Access volume, media controls, notifications, and quick settings from the Raven panel."
budgie_gtk_theme_title = "GTK Theme Support"
budgie_gtk_theme_description = "Budgie runs GTK themes and GNOME applications seamlessly, ensuring visual consistency."
# ── Slides for CINNAMON ────────────────────────────────────────────
cinnamon_title = "PisiLinux Cinnamon"
cinnamon_description = "Cinnamon combines the traditional desktop experience with modern features, offering a powerful and flexible environment."
cinnamon_panel_title = "Cinnamon Panel"
cinnamon_panel_description = "Customize your panel with applets, launchers, and system indicators for a classic yet powerful desktop."
cinnamon_settings_title = "Settings Manager"
cinnamon_settings_description = "Fine-tune every aspect of your Cinnamon desktop through the comprehensive Settings Manager."
cinnamon_desklets_title = "Cinnamon Desklets"
cinnamon_desklets_description = "Keep notes, clocks, and system info always visible on your desktop with Cinnamon Desklets."
# ── Slides for GNOME ────────────────────────────────────────────
gnome_title = "PisiLinux GNOME"
gnome_description = "Reduce distractions and focus on your work with the clean and focused GNOME desktop environment."
gnome_search_title = "GNOME Search"
gnome_search_description = "Press the Super key and start typing. Instantly reach applications, files, and settings."
gnome_extensions_title = "GNOME Extensions"
gnome_extensions_description = "Shape your desktop to your needs with hundreds of extensions from extensions.gnome.org."
# ── Slides for KDE/Plasma ────────────────────────────────────────────
kde_title = "PisiLinux KDE Plasma"
kde_description = "Meet the modern, customizable, and powerful KDE Plasma desktop. Everything is at your fingertips."
dolphin_title = "Dolphin File Manager"
dolphin_description = "Dolphin makes managing your files easy with tabbed browsing, dual-panel view, and powerful search."
kde_connect_title = "KDE Connect"
kde_connect_description = "Connect your phone to your computer. Notifications, file transfer, and clipboard sharing — all with KDE Connect."
discover_title = "Discover Software Center"
discover_description = "Easily find, install, and update applications from the visual interface with Discover."
# ── Slides for LXQt ────────────────────────────────────────────
lxqt_title = "PisiLinux LXQt"
lxqt_description = "A lightweight, fast, and modern LXQt desktop environment. Low resource consumption, high performance."
lxqt_panel_title = "LXQt Panel"
lxqt_panel_description = "The traditional panel layout puts the taskbar, application launcher, and system tray always within reach."
lxqt_pcmanfm_qt_title = "PCManFM-Qt File Manager"
lxqt_pcmanfm_qt_description = "Manage files effortlessly with the lightweight and fast PCManFM-Qt. Tabbed navigation and a customizable interface."
lxqt_speed_title = "LXQt Speed"
lxqt_speed_description = "LXQt is engineered for speed. Boot faster, respond quicker, and make the most of even older hardware."
# ── Slides for LXDE ────────────────────────────────────────────
lxde_title = "PisiLinux LXDE"
lxde_description = "The legendary lightweight LXDE desktop. Known for extremely low resource usage and a reliable build."
lxde_openbox_title = "Openbox-Based"
lxde_openbox_description = "LXDE uses the Openbox window manager. Fine-tune it for a smooth and responsive experience."
lxde_pcmanfm_title = "PCManFM File Manager"
lxde_pcmanfm_description = "PCManFM speeds up your file operations with its fast and lightweight build, supporting tree view and tabs."
lxde_lxpanel_title = "LXPanel"
lxde_lxpanel_description = "LXPanel provides a taskbar, application menu, and system tray in a classic desktop layout."
# ── Slides for LUMINA ────────────────────────────────────────────
lumina_title = "PisiLinux Lumina"
lumina_description = "A lightweight and independent Lumina desktop. Qt-based, designed to run cleanly on BSD and Linux systems."
lumina_applications_title = "Application Support"
lumina_applications_description = "Lumina works well with core utilities and many popular Qt apps, providing a practical everyday experience."
lumina_theme_title = "Theme Customization"
lumina_theme_description = "Easily customize color schemes and interface styles with Lumina's flexible theme system."
lumina_panel_title = "Lumina Panel"
lumina_panel_description = "A clean panel with a taskbar, application launcher, and system tray keeps your desktop organized."
# ── Slides for MATE ────────────────────────────────────────────
mate_title = "PisiLinux MATE"
mate_description = "MATE for those who love the traditional desktop layout. Developed with GTK+ 3 for a modern and smooth experience."
mate_caja_title = "Caja File Manager"
mate_caja_description = "Manage your files easily with MATE's powerful Caja. Dual panel, tabbed navigation, and rich features."
mate_settings_title = "MATE Settings"
mate_settings_description = "Customize your MATE desktop with comprehensive settings for appearance, display, keyboard, and more."
mate_applications_title = "Rich Application Suite"
mate_applications_description = "MATE comes with a rich set of classic applications — from text editors to image viewers, all well-integrated."
# ── Slides for XFCE ────────────────────────────────────────────
xfce_title = "PisiLinux XFCE"
xfce_description = "A lightweight, fast, and user-friendly XFCE desktop environment. Low resource consumption, high performance."
xfce_panel_title = "XFCE Panel"
xfce_panel_description = "The traditional panel layout puts the taskbar, application menu, and system tray always within reach."
xfce_thunar_title = "Thunar File Manager"
xfce_thunar_description = "Manage your files easily with the fast and lightweight Thunar. Tree view, tabbed navigation, and customizable features."
xfce_settings_title = "Settings Manager"
xfce_settings_description = "A modern alternative to the classic application menu. Find apps easily and organize your favorites with Whisker Menu."
# ── Finish ────────────────────────────────────────────────
finish = "Finished"
finish_title = "Installation Complete"
# ── Execution (extra) ─────────────────────────────────────
install_failed = "Installation failed"
install_done = "Installation complete! Click Next to continue."
retry_install = "Retry"
finish_next = "Finish →"
# ── Finish ────────────────────────────────────────────────
finish_description = "PisiLinux has been installed successfully. You can now restart your system."
finish_restart = "🔄 Restart Now"
finish_live = "Return to Live Desktop"
install_in_progress = "Installation in progress, please wait…"
# ── Partition (extra) ─────────────────────────────────────
partition_plan_title = "Partition Plan"
plan_col_part = "Partition"
plan_col_fs = "Filesystem"
plan_col_size = "Size"
partition_table_type = "Partition table"
disk_too_small = "Disk too small"
summary_partition_plan = "Partitioning"
summary_custom_partitions = "Manual Partitions"
mp_size_remaining = "Remaining Space"
# ── UI / General ──────────────────────────────────────────
installer = "YALI Installation Tool"
os_name = "PiSi GNU/LINUX"
# ── Error screen ──────────────────────────────────────────
error_show_log = "▼ Show Log Details"
error_hide_log = "▲ Hide Log Details"
error_copy_log = "📋 Copy Log"
error_copied = "✓ Copied!"
error_retry = "Retry"
error_quit = "Quit"
# ── Welcome / System checks ───────────────────────────────
system_checks_title = "System Requirements"
check_fail_warning = "One or more critical requirements are not met. Please resolve the issues."
check_warn_notice = "Some recommended requirements are not met. Installation can continue."
check_all_pass = "All system requirements are satisfied."
recheck = "🔄 Re-check"
# ── Users (ek) ────────────────────────────────────────────
pwd_very_weak = "Very weak"
pwd_weak = "Weak"
pwd_medium = "Medium"
pwd_strong = "Strong"
pwd_very_strong = "Very strong"
# ── Checker ───────────────────────────────────────────────
check_disk_space = "Disk Space"
check_disk_fail = "Maximum disk size is %{size} GB. Minimum 10 GB is required for installation."
check_disk_warn = "%{size} GB disk available. 20 GB or more recommended."
check_disk_pass = "%{size} GB disk available."
check_internet = "Internet"
check_net_pass = "Internet connection available."
check_net_warn = "Internet connection not found. Offline installation can continue, but updates and extra packages will not be downloaded."
check_boot_mode = "Boot Mode"
check_uefi_pass = "UEFI mode detected. GPT will be used."
check_bios_pass = "BIOS/Legacy mode detected. MBR will be used."
check_live_sys = "Live System"
check_live_fail = "PisiLinux Live environment not detected (is pisilinux-install package installed?). Installation can only be performed from a Live USB."
check_live_pass = "Live environment verified."
# ── Jobs ──────────────────────────────────────────────────
job_copy_files = "Copying system files"
job_partition = "Partitioning disk"
job_mount = "Mounting partitions"
job_mount_bind = "Preparing chroot environment"
job_umount_bind = "Cleaning up chroot mounts"
job_initramfs = "Generating initramfs"
job_bootloader = "Installing bootloader (GRUB)"
job_comar = "COMAR configuration"
job_fstab = "Generating fstab"
job_hostname = "Configuring hostname"
job_locale = "Configuring locale"
job_timezone = "Setting timezone"
job_keyboard = "Configuring keyboard layout"
job_user = "Creating user account"
# ── Manual Partition UI ────────────────────────────────────
manual_partition_title = "Manual Partitioning"
manual_partition_description = "Configure your disk layout manually."
mp_device = "Device"
mp_size = "Size"
mp_fstype = "Filesystem"
mp_mountpoint = "Mount Point"
mp_action = "Action"
mp_add = "Add Partition"
mp_delete = "Delete"
mp_apply = "Apply Changes"
mp_no_efi = "⚠️ No EFI partition assigned. Required for UEFI boot."
mp_no_root = "⚠️ No root (/) partition assigned."
mp_confirm_delete = "Delete partition %{dev}? This cannot be undone."
mp_plan_title = "Change Summary"
mp_fs_ext4 = "ext4"
mp_fs_btrfs = "btrfs"
mp_fs_xfs = "xfs"
mp_fs_fat32 = "FAT32 (EFI)"
mp_fs_swap = "swap"
mp_size_gb = "%{n} GB"
mp_size_mb = "%{n} MB"
check_ram = "RAM"
check_ram_fail = "%{mb} MB RAM available. Minimum 1 GB required."
check_ram_warn = "%{mb} MB RAM available. 2 GB or more recommended."
check_ram_pass = "%{mb} MB RAM available."
check_cpu = "CPU Architecture"
check_cpu_pass = "x86_64 (SSE2 supported)."
check_cpu_warn = "x86_64 but SSE2 support not detected. Some packages may not work."
check_cpu_fail = "'%{arch}' architecture is not supported. Only x86_64 is supported."
check_nvme = "NVMe Support"
check_nvme_pass1 = "NVMe disk(s) detected and driver loaded."
check_nvme_pass2 = "NVMe driver loaded (no device)."
check_nvme_pass3 = "No NVMe disk detected. Using SATA/virtio."
# ── Auto-Install / CLI ────────────────────────────────────
answer_file = "Answer file"
error = "Error"
disk = "Disk"
system_checks = "System Checks"
installation_started = "Installation started"
installation_done = "Installation complete"
installation_done_description = "You may now restart the system."
progress = "Progress"
done = "Done"
partition_plan_could_not_be_created = "Could not create partition plan for disk"
# ── Manual Partition Jobs ─────────────────────────────────
job_custom_partition = "Applying manual partition layout"
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_plan_valid = "✓ Partition plan is valid. You can proceed."
mp_select_disk_first = "← Select a disk first."
mp_add_dialog_hint = "/ or /home or swap"
mp_add_btn = "✓ Add"
mp_size_hint = "Size (MB, 0=remaining):"
mp_delete_confirm_yes = "✓ Yes, Delete"
plan_root_suffix = " (root)"
disk_min_required = " ↳ %{err} (min. %{n} GB required)"
error_partition_plan_not_found = "Partition plan not found."
error_target_disk_not_selected = "No target disk selected."
error_thread_terminated = "Install thread terminated unexpectedly."
error_thread_unexpectedly_terminated = "Install thread terminated unexpectedly."
+331
View File
@@ -0,0 +1,331 @@
# ── Navigasyon ────────────────────────────────────────────
steps = "Adımlar"
back = "Geri"
next = "İleri"
cancel = "İptal"
confirm = "Onayla"
# ── Welcome ───────────────────────────────────────────────
welcome = "Hoş Geldiniz"
welcome_title = "YALI Kurulum Aracına Hoş Geldiniz"
welcome_description = "Bu araç size kurulum sürecinde rehberlik edecektir."
select_language = "Dil Seçiniz:"
# ── Location ──────────────────────────────────────────────
location = "Konum"
location_title = "Bölge ve Zaman Dilimi"
location_description = "Bulunduğunuz bölgeyi ve zaman dilimini seçin."
search_timezone = "Zaman dilimi ara:"
# ── Keyboard ──────────────────────────────────────────────
keyboard = "Klavye"
keyboard_title = "Klavye Düzeni"
keyboard_description = "Kullanmak istediğiniz klavye düzenini seçin ve aşağıdan test edin."
keyboard_layout_label = "Düzen"
keyboard_test_label = "Test Alanı"
keyboard_test_hint = "Buraya yazarak klavyenizi test edin…"
keyboard_selected = "Seçili"
# ── Partition ─────────────────────────────────────────────
partition = "Disk Bölümleme"
partition_title = "Disk Seçimi"
partition_description = "Kurulumu yapacağınız diski seçin."
select_disk_label = "Disk seçin:"
no_disks_found = "Disk bulunamadı. Lütfen bir depolama aygıtı bağlayın."
rescan_disks = "Yeniden Tara"
erase_disk_label = "Tüm diski kullan (otomatik)"
manual_partition_label = "Manuel bölümleme"
manual_partition_wip = "Manuel bölümleme geliştirme aşamasındadır."
erase_warning = "UYARI: Seçilen diskteki tüm veriler silinecektir"
# ── Users ─────────────────────────────────────────────────
users = "Kullanıcı"
users_title = "Kullanıcı Bilgileri"
users_description = "Sisteminiz için bir kullanıcı hesabı oluşturun."
username_label = "Kullanıcı adı"
password_label = "Şifre"
password_confirm_label = "Şifre (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."
hostname_invalid = "Geçersiz bilgisayar adı. Yalnızca harf, rakam ve tire içerebilir."
passwords_no_match = "Şifreler eşleşmiyor"
# ── Summary ───────────────────────────────────────────────
summary = "Özet"
summary_title = "Kurulum Özeti"
summary_description = "Aşağıdaki seçimlerinizi kontrol edin. İleri'ye tıklayınca kurulum başlayacaktır."
summary_language = "Dil"
summary_timezone = "Zaman dilimi"
summary_keyboard = "Klavye düzeni"
summary_disk = "Disk"
summary_username = "Kullanıcı adı"
summary_hostname = "Bilgisayar adı"
summary_erase_mode = "Tüm disk silinecek"
summary_manual_mode = "Manuel bölümleme"
summary_not_selected = "Seçilmedi"
summary_warning = "⚠️ Dikkat: İleri'ye basıldığında seçilen diskteki tüm veriler kalıcı olarak silinecektir!"
# ── Execution ─────────────────────────────────────────────
execution = "Kurulum"
execution_title = "Kurulum Yapılıyor"
execution_description = "Lütfen bekleyin. Bu işlem birkaç dakika sürebilir."
start_install = "Kurulumu Başlat"
install_starting = "Kurulum başlatılıyor…"
# ── Slides for PISILINUX ────────────────────────────────────────────
welcome_slide_title = "Pisi GNU/Linux'a Hoş Geldiniz"
welcome_slide_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."
package_manager_title = "PISI Paket Yöneticisi"
package_manager_description = "Binlerce pakete tek komutla erişin. `pisi install <paket>` ile ihtiyacınız olan her şeyi kolayca kurabilirsiniz."
community_title = "Topluluk Desteği"
community_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."
speed_title = "Hız ve Performans"
speed_description = "Optimize edilmiş çekirdek ve başlangıç servisleri sayesinde PisiLinux saniyeler içinde açılır ve minimum kaynak kullanır."
security_title = "Güvenlik Önce Gelir"
security_description = "Düzenli güvenlik güncellemeleri, AppArmor profilleri ve güvenli varsayılan ayarlarla sisteminizi koruyun."
customization_title = "Özelleştirme"
customization_description = "Masaüstü ortamından tema renklerine, panel düzeninden klavye kısayollarına kadar her şeyi zevkinize göre ayarlayın."
# ── Slides for BUDGIE ────────────────────────────────────────────
budgie_title = "PisiLinux Budgie"
budgie_description = "Modern, sade ve şık Budgie masaüstü. GNOME altyapısı üzerine inşa edilmiş, kullanıcı dostudur."
budgie_applets_title = "Budgie Desktop Applets"
budgie_applets_description = "Budgie'nin panel appletleri sayesinde hava durumu, sistem izleyici, takvim gibi araçları kolayca ekleyin."
budgie_raven_title = "Raven Bildirim Merkezi"
budgie_raven_description = "Raven panelinden ses seviyesi, medya kontrolleri, bildirimler ve hızlı ayarlara erişin."
budgie_gtk_theme_title = "GTK Tema Desteği"
budgie_gtk_theme_description = "Budgie, GTK temalarını ve GNOME uygulamalarını sorunsuz çalıştırır, görsel bütünlük sağlar."
# ── Slides for CINNAMON ────────────────────────────────────────────
cinnamon_title = "PisiLinux Cinnamon"
cinnamon_description = "Geleneksel masaüstü deneyimini modern özelliklerle birleştiren Cinnamon, güçlü ve esnek bir ortam sunar."
cinnamon_panel_title = "Cinnamon Paneli"
cinnamon_panel_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."
cinnamon_settings_title = "Ayarlar Yöneticisi"
cinnamon_settings_description = "Cinnamon'un kapsamlı Ayarlar Yöneticisi aracılığıyla masaüstünüzün her yönünü özelleştirin."
cinnamon_desklets_title = "Cinnamon Desklets"
cinnamon_desklets_description = "Masaüstü widget'ları ile notlar, saatler ve sistem bilgileri her zaman görünür kalsın."
# ── Slides for GNOME ────────────────────────────────────────────
gnome_title = "PisiLinux GNOME"
gnome_description = "Sade ve odaklı GNOME masaüstü ortamı ile dikkat dağıtıcı unsurları azaltın, işinize konsantre olun."
gnome_search_title = "GNOME Arama"
gnome_search_description = "Super tuşuna basıp yazmaya başlayın. Uygulama, dosya ve ayarlara anında ulaşın."
gnome_extensions_title = "GNOME Eklentileri"
gnome_extensions_description = "extensions.gnome.org adresinden yüzlerce eklentiyle masaüstünüzü ihtiyaçlarınıza göre şekillendirin."
# ── Slides for KDE/Plasma ────────────────────────────────────────────
kde_title = "PisiLinux KDE Plasma"
kde_description = "Modern, özelleştirilebilir ve güçlü KDE Plasma masaüstü ortamıyla tanışın. Her şey elinizin altında."
dolphin_title = "Dolphin Dosya Yöneticisi"
dolphin_description = "Sekmeli gezinme, ikili panel ve güçlü arama özelliğiyle Dolphin dosyalarınızı yönetmeyi kolaylaştırır."
kde_connect_title = "KDE Bağlan"
kde_connect_description = "Telefonunuzu bilgisayarınıza bağlayın. Bildirimler, dosya aktarımı ve klipboard paylaşımı KDE Connect ile mümkün."
discover_title = "Discover Yazılım Merkezi"
discover_description = "Discover ile uygulamaları görsel arayüzden kolayca bulun, kurun ve güncelleyin."
# ── Slides for LXQt ────────────────────────────────────────────
lxqt_title = "PisiLinux LXQt"
lxqt_description = "Hafif, hızlı ve modern LXQt masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
lxqt_panel_title = "LXQt Panel"
lxqt_panel_description = "Geleneksel panel düzeni ile görev çubuğu, uygulama başlatıcı ve sistem tepsisi her zaman elinizin altında."
lxqt_pcmanfm_qt_title = "PCManFM-Qt Dosya Yöneticisi"
lxqt_pcmanfm_qt_description = "Hafif ve hızlı PCManFM-Qt ile dosya işlemlerini kolayca yönetin. Sekmeli gezinme ve özelleştirilebilir arayüz sunar."
lxqt_speed_title = "LXQt Hızı"
lxqt_speed_description = "LXQt, hız için tasarlanmıştır. Daha hızlı açılır, daha çabuk yanıt verir ve eski donanımları en iyi şekilde kullanır."
# ── Slides for LXDE ────────────────────────────────────────────
lxde_title = "PisiLinux LXDE"
lxde_description = "Efsanevi hafif masaüstü LXDE. Çok düşük kaynak tüketimi ve güvenilir yapısıyla tanınır."
lxde_openbox_title = "Openbox Tabanlı"
lxde_openbox_description = "LXDE, Openbox pencere yöneticisini kullanır. İnce ayarlar ile akıcı bir deneyim sunar."
lxde_pcmanfm_title = "PCManFM Dosya Yöneticisi"
lxde_pcmanfm_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."
lxde_lxpanel_title = "LXPanel"
lxde_lxpanel_description = "LXPanel, geleneksel masaüstü düzeni ile görev çubuğu, uygulama menüsü ve sistem tepsisi gibi temel özellikleri sunar."
# ── Slides for LUMINA ────────────────────────────────────────────
lumina_title = "PisiLinux Lumina"
lumina_description = "Hafif ve bağımsız Lumina masa üstü. Qt tabanlı, BSD ve Linux sistemlerde düzgün çalışan minimal bir ortam."
lumina_applications_title = "Uygulama Desteği"
lumina_applications_description = "Lumina; temel uygulamalar ve birçok popüler Qt uygulamasıyla uyumlu çalışır, kullanışlı bir deneyim sunar."
lumina_theme_title = "Tema Özelleştirme"
lumina_theme_description = "Lumina'nın tema sistemiyle renk şemaları ve arayüz stilini kolayca özelleştirin."
lumina_panel_title = "Lumina Paneli"
lumina_panel_description = "Görev çubuğu, uygulama başlatıcı ve sistem tepsisini içeren sade panel, masa üstünüzü düzenli tutar."
# ── Slides for MATE ────────────────────────────────────────────
mate_title = "PisiLinux MATE"
mate_description = "Geleneksel masaüstü düzenini sevenler için MATE. GTK+ 3 ile geliştirilmiş, modern ve akıcı bir deneyim sunar."
mate_caja_title = "Caja Dosya Yöneticisi"
mate_caja_description = "MATE'in güçlü dosya yöneticisi Caja ile dosya işlemlerinizi kolayca yapın. Çift panel, sekmeli gezinme ve geniş özellikler sunar."
mate_settings_title = "MATE Ayarları"
mate_settings_description = "Görünüm, ekran, klavye ve daha fazlası için kapsamlı ayarlarla MATE masaüstünüzü özelleştirin."
mate_applications_title = "Zengin Uygulama Paketi"
mate_applications_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."
# ── Slides for XFCE ────────────────────────────────────────────
xfce_title = "PisiLinux XFCE"
xfce_description = "Hafif, hızlı ve kullanıcı dostu XFCE masaüstü ortamı. Kaynak tüketimi düşük, performansı yüksektir."
xfce_panel_title = "XFCE Panel"
xfce_panel_description = "Geleneksel panel düzeni ile görev çubuğu, uygulama menüsü ve sistem tepsisi her zaman elinizin altında."
xfce_thunar_title = "Thunar Dosya Yöneticisi"
xfce_thunar_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."
xfce_settings_title = "Ayarlar Merkezi"
xfce_settings_description = "Klasik uygulama menüsüne modern bir alternatif. Whisker Menu ile uygulamaları kolayca bulun, favorilerinizi düzenleyin."
# ── Finish ────────────────────────────────────────────────
finish = "Tamamlandı"
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."
retry_install = "Yeniden Dene"
finish_next = "Tamamlandı →"
# ── Finish ────────────────────────────────────────────────
finish_description = "PisiLinux başarıyla kuruldu. Şimdi sisteminizi yeniden başlatabilirsiniz."
finish_restart = "🔄 Yeniden Başlat"
finish_live = "Canlı Masaüstüne Dön"
install_in_progress = "Kurulum devam ediyor, lütfen bekleyin…"
# ── Partition (ek) ────────────────────────────────────────
partition_plan_title = "Bölümleme Planı"
plan_col_part = "Bölüm"
plan_col_fs = "Dosya Sistemi"
plan_col_size = "Boyut"
partition_table_type = "Bölüm tablosu"
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"
# ── UI / Genel ────────────────────────────────────────────
installer = "YALI Kurulum Aracı"
os_name = "PiSi GNU/LİNUX"
# ── Hata ekranı ───────────────────────────────────────────
error_show_log = "▼ Log Detaylarını Göster"
error_hide_log = "▲ Log Detaylarını Gizle"
error_copy_log = "📋 Logu Kopyala"
error_copied = "✓ Kopyalandı!"
error_retry = "Yeniden Dene"
error_quit = "Çıkış"
# ── Welcome / Sistem kontrolleri ──────────────────────────
system_checks_title = "Sistem Gereksinimleri"
check_fail_warning = "Bir veya daha fazla kritik gereksinim karşılanmıyor. Lütfen sorunları giderin."
check_warn_notice = "Bazı önerilen gereksinimler karşılanmıyor. Kurulum devam edebilir."
check_all_pass = "Tüm sistem gereksinimleri karşılandı."
recheck = "🔄 Yeniden Kontrol Et"
# ── Users (ek) ────────────────────────────────────────────
pwd_very_weak = "Çok zayıf"
pwd_weak = "Zayıf"
pwd_medium = "Orta"
pwd_strong = "Güçlü"
pwd_very_strong = "Çok güçlü"
# ── Checker ───────────────────────────────────────────────
check_disk_space = "Disk Alanı"
check_disk_fail = "Maksimum disk boyutu %{size} GB. Kurulum için minimum 10 GB gerekli."
check_disk_warn = "%{size} GB disk mevcut. 20 GB veya üzeri önerilir."
check_disk_pass = "%{size} GB disk mevcut."
check_internet = "İnternet"
check_net_pass = "İnternet bağlantısı mevcut."
check_net_warn = "İnternet bağlantısı bulunamadı. Çevrimdışı kurulum devam edebilir, ancak güncellemeler ve ek paketler indirilemeyecek."
check_boot_mode = "Önyükleme Modu"
check_uefi_pass = "UEFI modu tespit edildi. GPT kullanılacak."
check_bios_pass = "BIOS/Legacy modu tespit edildi. MBR kullanılacak."
check_live_sys = "Canlı Sistem"
check_live_fail = "PisiLinux Live ortamında çalışmıyor (pisilinux-install paketi kurulu mu?). Kurulum sadece Live USB üzerinden yapılabilir."
check_live_pass = "Live ortam doğrulandı."
# ── Jobs ──────────────────────────────────────────────────
job_copy_files = "Sistem dosyaları kopyalanıyor"
job_partition = "Disk bölümlendirme"
job_mount = "Bölümler bağlanıyor"
job_mount_bind = "Chroot ortamı hazırlanıyor"
job_umount_bind = "Chroot bağlantıları temizleniyor"
job_initramfs = "initramfs oluşturuluyor"
job_bootloader = "Önyükleyici kuruluyor (GRUB)"
job_comar = "COMAR yapılandırması"
job_fstab = "fstab oluşturuluyor"
job_hostname = "Hostname yapılandırılıyor"
job_locale = "Locale yapılandırılıyor"
job_timezone = "Zaman dilimi ayarlanıyor"
job_keyboard = "Klavye düzeni yapılandırılıyor"
job_user = "Kullanıcı hesabı oluşturuluyor"
# ── Manuel Bölümleme UI ────────────────────────────────────
manual_partition_title = "Manuel Bölümleme"
manual_partition_description = "Disk düzeninizi elle yapılandırın."
mp_device = "Aygıt"
mp_size = "Boyut"
mp_fstype = "Dosya Sistemi"
mp_mountpoint = "Bağlama Noktası"
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_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"
mp_fs_btrfs = "btrfs"
mp_fs_xfs = "xfs"
mp_fs_fat32 = "FAT32 (EFI)"
mp_fs_swap = "takas (swap)"
mp_size_gb = "%{n} GB"
mp_size_mb = "%{n} MB"
check_ram = "RAM"
check_ram_fail = "%{mb} MB RAM mevcut. Kurulum için minimum 1 GB gerekli."
check_ram_warn = "%{mb} MB RAM mevcut. 2 GB veya üzeri önerilir."
check_ram_pass = "%{mb} MB RAM mevcut."
check_cpu = "CPU Mimarisi"
check_cpu_pass = "x86_64 (SSE2 desteği mevcut)."
check_cpu_warn = "x86_64 fakat SSE2 desteği tespit edilemedi. Bazı paketler çalışmayabilir."
check_cpu_fail = "'%{arch}' mimarisi desteklenmiyor. Yalnızca x86_64 desteklenmektedir."
check_nvme = "NVMe Desteği"
check_nvme_pass1 = "NVMe disk(ler) tespit edildi ve sürücü yüklü."
check_nvme_pass2 = "NVMe sürücüsü yüklü (cihaz yok)."
check_nvme_pass3 = "NVMe disk tespit edilmedi. SATA/virtio modu kullanılıyor."
# ── Otomatik Kurulum / CLI ─────────────────────────────────
answer_file = "Answer dosyası"
error = "Hata"
disk = "Disk"
system_checks = "Sistem Kontrolleri"
installation_started = "Kurulum başlatıldı"
installation_done = "Kurulum tamamlandı"
installation_done_description = "Sisteminizi yeniden başlatabilirsiniz."
progress = "İlerleme"
done = "Tamamlandı"
partition_plan_could_not_be_created = "Bölümleme planı oluşturulamadı"
# ── Manuel Bölümleme Job'ları ──────────────────────────────
job_custom_partition = "Manuel bölümleme uygulanıyor"
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_select_disk_first = "← Önce bir disk seçin."
mp_add_dialog_hint = "/ veya /home veya swap"
mp_add_btn = "✓ Ekle"
mp_size_hint = "Boyut (MB, 0=kalan):"
mp_delete_confirm_yes = "✓ Evet, Sil"
plan_root_suffix = " (kök)"
disk_min_required = " ↳ %{err} (min. %{n} GB gerekli)"
error_partition_plan_not_found = "Bölümleme planı bulunamadı."
error_target_disk_not_selected = "Hedef disk seçilmedi."
error_thread_terminated = "İş parçacığı beklenmedik şekilde sonlandı."
error_thread_unexpectedly_terminated = "İş parçacığı beklenmedik şekilde sonlandı."
+189
View File
@@ -0,0 +1,189 @@
//! ISO ortamı ve donanım uyumluluk kontrolleri.
//!
//! WelcomeStep açılmadan önce çalışır.
//! Her kontrol bir `CheckResult` döner: Pass / Warn / Fail.
use sysinfo::System;
use std::path::Path;
use rust_i18n::t;
// ─────────────────────────────────────────────
// SONUÇ TİPİ
// ─────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq)]
pub enum CheckStatus { Pass, Warn, Fail }
#[derive(Debug, Clone)]
pub struct CheckResult {
pub name: String,
pub status: CheckStatus,
pub message: String,
}
impl CheckResult {
fn pass(name: impl Into<String>, msg: impl Into<String>) -> Self {
Self { name: name.into(), status: CheckStatus::Pass, message: msg.into() }
}
fn warn(name: impl Into<String>, msg: impl Into<String>) -> Self {
Self { name: name.into(), status: CheckStatus::Warn, message: msg.into() }
}
fn fail(name: impl Into<String>, msg: impl Into<String>) -> Self {
Self { name: name.into(), status: CheckStatus::Fail, message: msg.into() }
}
}
// ─────────────────────────────────────────────
// TÜM KONTROLLERİ ÇALIŞTIR
// ─────────────────────────────────────────────
pub fn run_all() -> Vec<CheckResult> {
vec![
check_ram(),
check_disk_space(),
check_internet(),
check_boot_mode(),
check_cpu_arch(),
check_live_env(),
check_nvme_support(),
]
}
/// Tüm kontroller geçti mi? (Fail yoksa true)
pub fn all_passed(results: &[CheckResult]) -> bool {
results.iter().all(|r| r.status != CheckStatus::Fail)
}
// ─────────────────────────────────────────────
// BİREYSEL KONTROLLER
// ─────────────────────────────────────────────
/// RAM kontrolü. Minimum 1 GB, önerilen 2 GB.
fn check_ram() -> CheckResult {
let mut sys = System::new();
sys.refresh_memory();
let mb = sys.total_memory() / 1024 / 1024;
if mb < 1024 {
CheckResult::fail(
t!("check_ram"),
t!("check_ram_fail", mb = mb)
)
} else if mb < 2048 {
CheckResult::warn(
t!("check_ram"),
t!("check_ram_warn", mb = mb)
)
} else {
CheckResult::pass(t!("check_ram"), t!("check_ram_pass", mb = mb))
}
}
/// En az 10 GB boş disk alanı kontrolü.
fn check_disk_space() -> CheckResult {
use sysinfo::Disks;
let disks = Disks::new_with_refreshed_list();
let max_gb = disks.iter()
.map(|d| d.total_space() / 1024 / 1024 / 1024)
.max()
.unwrap_or(0);
if max_gb < 10 {
CheckResult::fail(
t!("check_disk_space"),
t!("check_disk_fail", size = max_gb),
)
} else if max_gb < 20 {
CheckResult::warn(
t!("check_disk_space"),
t!("check_disk_warn", size = max_gb),
)
} else {
CheckResult::pass(t!("check_disk_space"), t!("check_disk_pass", size = max_gb))
}
}
/// İnternet bağlantısı kontrolü (isteğe bağlı).
fn check_internet() -> CheckResult {
// DNS çözümlemesi ile basit kontrol
let connected = std::net::TcpStream::connect_timeout(
&"8.8.8.8:53".parse().unwrap(),
std::time::Duration::from_secs(3),
).is_ok();
if connected {
CheckResult::pass(t!("check_internet"), t!("check_net_pass"))
} else {
CheckResult::warn(
t!("check_internet"),
t!("check_net_warn"),
)
}
}
/// UEFI mi BIOS mu?
fn check_boot_mode() -> CheckResult {
if Path::new("/sys/firmware/efi").exists() {
CheckResult::pass(t!("check_boot_mode"), t!("check_uefi_pass"))
} else {
CheckResult::pass(t!("check_boot_mode"), t!("check_bios_pass"))
}
}
/// CPU mimarisi kontrolü (yalnızca x86_64 destekleniyor).
fn check_cpu_arch() -> CheckResult {
let arch = std::env::consts::ARCH;
if arch == "x86_64" {
// CPU özelliklerini de kontrol et
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").unwrap_or_default();
let has_sse2 = cpuinfo.contains("sse2");
if has_sse2 {
CheckResult::pass(t!("check_cpu"), t!("check_cpu_pass"))
} else {
CheckResult::warn(t!("check_cpu"), t!("check_cpu_warn"))
}
} else {
CheckResult::fail(
t!("check_cpu"),
t!("check_cpu_fail", arch = arch)
)
}
}
/// Canlı sistem ortamı kontrolü.
fn check_live_env() -> CheckResult {
let source = "/run/livecd/squashfs-root";
if !Path::new(source).exists() {
CheckResult::fail(
t!("check_live_sys"),
t!("check_live_fail"),
)
} else {
CheckResult::pass(
t!("check_live_sys"),
t!("check_live_pass"),
)
}
}
/// NVMe ve diğer modern disk arayüzlerinin çekirdek desteğini kontrol eder.
fn check_nvme_support() -> CheckResult {
// /dev/nvme* cihazları var mı?
let has_nvme_dev = std::fs::read_dir("/dev")
.map(|dir| dir.flatten().any(|e| e.file_name().to_string_lossy().starts_with("nvme")))
.unwrap_or(false);
// Çekirdek modülü yüklü mü?
let modules = std::fs::read_to_string("/proc/modules").unwrap_or_default();
let nvme_loaded = modules.contains("nvme ");
if has_nvme_dev {
CheckResult::pass(t!("check_nvme"), t!("check_nvme_pass1"))
} else if nvme_loaded {
CheckResult::pass(t!("check_nvme"), t!("check_nvme_pass2"))
} else {
// NVMe yoksa bu bir sorun değil; SATA/virtio kullanılıyor olabilir
CheckResult::pass(t!("check_nvme"), t!("check_nvme_pass3"))
}
}
+216
View File
@@ -0,0 +1,216 @@
//! Otomatik kurulum (answer file) desteği.
//!
//! Kullanım:
//! yali-rs --auto-install /path/to/answer.toml
//!
//! Answer file formatı (TOML):
//!
//! [install]
//! disk = "/dev/sda"
//! timezone = "Europe/Istanbul"
//! locale = "tr_TR.UTF-8"
//! keyboard = "tr"
//!
//! [user]
//! username = "pisi"
//! password = "changeme"
//! hostname = "pisilinux"
use serde::Deserialize;
use std::path::Path;
pub mod checker;
// ─────────────────────────────────────────────
// ANSWER FILE YAPISI
// ─────────────────────────────────────────────
#[derive(Debug, Deserialize)]
pub struct AnswerFile {
pub install: InstallConfig,
pub user: UserConfig,
}
#[derive(Debug, Deserialize)]
pub struct InstallConfig {
/// Hedef disk aygıtı. Örn: "/dev/sda", "/dev/nvme0n1"
pub disk: String,
/// Zaman dilimi. Örn: "Europe/Istanbul"
#[serde(default = "default_timezone")]
pub timezone: String,
/// Sistem locale. Örn: "tr_TR.UTF-8"
#[serde(default = "default_locale")]
pub locale: String,
/// Klavye düzeni. Örn: "tr"
#[serde(default = "default_keyboard")]
pub keyboard: String,
/// Klavye varyantı. Örn: "f" (Türkçe F için)
#[serde(default)]
pub keyboard_variant: String,
/// Kaynak dizin (canlı sistem). Varsayılan: /run/livecd/squashfs-root/
#[serde(default = "default_source")]
pub source: String,
/// Hedef bağlama noktası. Varsayılan: /mnt
#[serde(default = "default_mount")]
pub mount: String,
}
#[derive(Debug, Deserialize)]
pub struct UserConfig {
pub username: String,
pub password: String,
pub hostname: String,
}
fn default_timezone() -> String { "Europe/Istanbul".into() }
fn default_locale() -> String { "tr_TR.UTF-8".into() }
fn default_keyboard() -> String { "tr".into() }
fn default_source() -> String { "/run/livecd/squashfs-root/".into() }
fn default_mount() -> String { "/mnt".into() }
// ─────────────────────────────────────────────
// DOĞRULAMA
// ─────────────────────────────────────────────
#[derive(Debug)]
pub enum AnswerError {
Io(std::io::Error),
Parse(toml::de::Error),
Validation(Vec<String>),
}
impl std::fmt::Display for AnswerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AnswerError::Io(e) => write!(f, "Dosya okuma hatası: {}", e),
AnswerError::Parse(e) => write!(f, "TOML ayrıştırma hatası: {}", e),
AnswerError::Validation(v) => write!(f, "Doğrulama hataları:\n{}", v.join("\n")),
}
}
}
/// Answer dosyasını okuyup ayrıştırır ve doğrular.
pub fn load(path: &Path) -> Result<AnswerFile, AnswerError> {
let content = std::fs::read_to_string(path)
.map_err(AnswerError::Io)?;
let af: AnswerFile = toml::from_str(&content)
.map_err(AnswerError::Parse)?;
validate(&af)?;
Ok(af)
}
fn validate(af: &AnswerFile) -> Result<(), AnswerError> {
let mut errors: Vec<String> = Vec::new();
// Disk kontrolü
if !af.install.disk.starts_with("/dev/") {
errors.push(format!(
"Geçersiz disk yolu '{}'. '/dev/' ile başlamalı.",
af.install.disk
));
}
if !Path::new(&af.install.disk).exists() {
errors.push(format!(
"Disk '{}' bulunamadı. Mevcut diskler için 'lsblk' komutunu çalıştırın.",
af.install.disk
));
}
// Kullanıcı adı doğrulaması (UsersStep ile aynı kural)
let un = &af.user.username;
if un.is_empty() || un.len() > 32 {
errors.push("Kullanıcı adı 132 karakter arasında olmalı.".into());
} else {
let valid = un.chars().next().map(|c| c.is_ascii_lowercase() || c == '_').unwrap_or(false)
&& un.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-');
if !valid {
errors.push(format!(
"Geçersiz kullanıcı adı '{}'. Yalnızca küçük harf, rakam, - ve _ kullanılabilir.",
un
));
}
}
// Şifre uzunluğu
if af.user.password.len() < 6 {
errors.push("Şifre en az 6 karakter olmalı.".into());
}
// Hostname
let hn = &af.user.hostname;
if hn.is_empty() || hn.len() > 63 || hn.starts_with('-') || hn.ends_with('-')
|| !hn.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
{
errors.push(format!("Geçersiz hostname '{}'.", hn));
}
if errors.is_empty() { Ok(()) } else { Err(AnswerError::Validation(errors)) }
}
// ─────────────────────────────────────────────
// GLOBALSTATE'E UYGULA
// ─────────────────────────────────────────────
use crate::installer::GlobalState;
use crate::steps::partition::PartitionStep;
use crate::installer::InstallerStep;
/// Answer file verilerini `GlobalState`'e yazar.
/// Bu işlemden sonra `PartitionStep::on_enter()` çağrılarak
/// disk listesi yenilenebilir ve plan oluşturulabilir.
pub fn apply_to_state(af: &AnswerFile, state: &mut GlobalState) {
state.timezone = af.install.timezone.clone();
state.keyboard_layout = af.install.keyboard.clone();
state.keyboard_variant = af.install.keyboard_variant.clone();
state.selected_disk = Some(af.install.disk.clone());
state.erase_disk = true;
state.username = af.user.username.clone();
state.password = af.user.password.clone();
state.password_confirm = af.user.password.clone();
state.hostname = af.user.hostname.clone();
// Locale'den dil kodunu çıkar: "tr_TR.UTF-8" → "tr"
state.language = af.install.locale
.split('_')
.next()
.unwrap_or("tr")
.to_string();
// Bölümleme planını hesapla
let is_uefi = Path::new("/sys/firmware/efi").exists();
let mut ps = PartitionStep::default();
ps.on_enter(state);
// Seçili diski bul ve plan oluştur
if let Some(disk) = state.available_disks.iter().find(|d| d.name == af.install.disk).cloned() {
use crate::installer::{PartitionPlan, PartitionTableType};
let total_mb = disk.size_bytes / 1024 / 1024;
let efi_mb = if is_uefi { 512 } else { 0 };
let swap_mb = recommended_swap_mb();
let root_mb = total_mb.saturating_sub(efi_mb + swap_mb + 512);
state.partition_plan = Some(PartitionPlan {
disk: disk.name.clone(),
table_type: if is_uefi { PartitionTableType::Gpt } else { PartitionTableType::Mbr },
efi_mb,
swap_mb,
root_mb,
});
}
}
fn recommended_swap_mb() -> u64 {
use sysinfo::System;
let mut sys = System::new();
sys.refresh_memory();
let ram_mb = sys.total_memory() / 1024 / 1024;
let swap = if ram_mb <= 2048 { ram_mb } else if ram_mb <= 8192 { ram_mb / 2 } else { 4096 };
swap.clamp(512, 8192)
}
+324
View File
@@ -0,0 +1,324 @@
use eframe::egui;
use rust_i18n::t;
/// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur.
///
/// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1`
/// - NVMe: `/dev/nvme0n1` + 1 → `/dev/nvme0n1p1`
/// - MMC: `/dev/mmcblk0` + 1 → `/dev/mmcblk0p1`
pub fn part_path(disk: &str, num: u32) -> String {
let needs_p = disk.contains("nvme") || disk.contains("mmcblk") || disk.contains("loop");
if needs_p {
format!("{}p{}", disk, num)
} else {
format!("{}{}", disk, num)
}
}
/// 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;
}
// ─────────────────────────────────────────────
// VERİ YAPILARI
// ─────────────────────────────────────────────
#[derive(Clone, Debug)]
pub struct DiskInfo {
pub name: String, // ör. "/dev/sda"
pub model: String,
pub size_gb: u64,
pub size_bytes: u64,
}
/// Otomatik bölümleme planı.
/// Disk seçimi yapılınca hesaplanır; Özet ekranında gösterilir.
#[derive(Clone, Debug, Default)]
pub struct PartitionPlan {
pub disk: String,
pub table_type: PartitionTableType, // GPT veya MBR
pub efi_mb: u64, // 0 → BIOS, >0 → UEFI
pub swap_mb: u64,
pub root_mb: u64, // kalan alan
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum PartitionTableType {
#[default]
Gpt,
Mbr,
}
impl std::fmt::Display for PartitionTableType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PartitionTableType::Gpt => write!(f, "GPT"),
PartitionTableType::Mbr => write!(f, "MBR"),
}
}
}
/// Manuel bölümleme için desteklenen dosya sistemleri.
#[derive(Clone, Debug, PartialEq)]
pub enum FsType {
Ext4,
Btrfs,
Xfs,
Fat32, // EFI bölümü için
Swap,
}
impl FsType {
pub fn label(&self) -> &'static str {
match self {
FsType::Ext4 => "ext4",
FsType::Btrfs => "btrfs",
FsType::Xfs => "xfs",
FsType::Fat32 => "FAT32 (EFI)",
FsType::Swap => "swap",
}
}
}
impl std::fmt::Display for FsType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label())
}
}
/// Kullanıcının manuel olarak tanımladığı tek bir bölüm.
#[derive(Clone, Debug)]
pub struct CustomPartition {
/// Bölüm aygit adı; oluşturulacak (ör. "/dev/sda1")
pub device: String,
/// Boyut MB cinsinden; 0 = "kalan alan"
pub size_mb: u64,
/// Dosya sistemi
pub fstype: FsType,
/// Bağlama noktası; swap için "swap"
pub mountpoint: String,
}
/// Kurulum boyunca toplanan tüm veriler.
pub struct GlobalState {
pub language: String,
pub timezone: String,
pub selected_disk: Option<String>,
pub erase_disk: bool,
pub partition_plan: Option<PartitionPlan>,
/// Manuel bölümleme modu için kullanıcının tanımladığı bölümler.
pub custom_partitions: Vec<CustomPartition>,
pub keyboard_layout: String,
pub keyboard_variant: String,
pub username: String,
pub password: String,
pub password_confirm: String,
pub hostname: String,
pub current_step: usize,
pub available_disks: Vec<DiskInfo>,
pub install_progress: f32,
pub install_log: Vec<String>,
pub demo_mode: bool,
pub is_dark: bool,
}
impl Default for GlobalState {
fn default() -> Self {
Self {
language: "tr".to_string(),
timezone: "Europe/Istanbul".to_string(),
selected_disk: None,
erase_disk: true,
partition_plan: None,
custom_partitions: Vec::new(),
keyboard_layout: "tr".to_string(),
keyboard_variant: String::new(),
username: String::new(),
password: String::new(),
password_confirm: String::new(),
hostname: String::new(),
current_step: 0,
available_disks: Vec::new(),
install_progress: 0.0,
install_log: Vec::new(),
demo_mode: false,
is_dark: true,
}
}
}
// ─────────────────────────────────────────────
// WELCOME STEP (sistem gereksinimleri + dil seçimi)
// ─────────────────────────────────────────────
use crate::autoinstall::checker::{CheckResult, CheckStatus, run_all, all_passed};
pub struct WelcomeStep {
checks: Vec<CheckResult>,
checked: bool,
last_language: String,
}
impl Default for WelcomeStep {
fn default() -> Self {
Self { checks: Vec::new(), checked: false, last_language: String::new() }
}
}
impl InstallerStep for WelcomeStep {
fn name(&self) -> String { t!("welcome").to_string() }
fn on_enter(&mut self, state: &mut GlobalState) {
if !self.checked || self.last_language != state.language {
rust_i18n::set_locale(&state.language);
self.checks = run_all();
self.checked = true;
self.last_language = state.language.clone();
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
use crate::ui::theme;
if self.last_language != state.language {
rust_i18n::set_locale(&state.language);
self.checks = run_all();
self.last_language = state.language.clone();
}
theme::section_heading(ui, &t!("welcome_title"));
ui.label(
egui::RichText::new(t!("welcome_description"))
.color(theme::c_text_dim())
.size(13.0),
);
ui.add_space(12.0);
// Dil seçimi (Açılır Liste / ComboBox)
ui.horizontal(|ui| {
ui.label(t!("select_language"));
let current_lang_label = if state.language == "tr" { "🇹🇷 Türkçe" } else { "🇺🇸 English" };
egui::ComboBox::from_id_source("language_select")
.selected_text(current_lang_label)
.show_ui(ui, |ui| {
ui.selectable_value(&mut state.language, "tr".to_string(), "🇹🇷 Türkçe");
ui.selectable_value(&mut state.language, "en".to_string(), "🇺🇸 English");
});
});
ui.add_space(14.0);
ui.separator();
ui.add_space(10.0);
ui.label(
egui::RichText::new(t!("system_checks_title"))
.size(15.0)
.strong()
.color(theme::c_text()),
);
ui.add_space(6.0);
// Kontrol tablosu
egui::Grid::new("checks_grid")
.num_columns(3)
.spacing([10.0, 6.0])
.striped(true)
.show(ui, |ui| {
for check in &self.checks {
// Durum simgesi + renk
let (icon, color) = match check.status {
CheckStatus::Pass => ("", theme::c_success()),
CheckStatus::Warn => ("⚠️", theme::c_warning()),
CheckStatus::Fail => ("", theme::c_error()),
};
ui.colored_label(color, icon);
ui.strong(&check.name);
ui.label(
egui::RichText::new(&check.message)
.size(12.0)
.color(theme::c_text_dim()),
);
ui.end_row();
}
});
// Hata varsa uyarı
ui.add_space(10.0);
let has_fail = self.checks.iter().any(|c| c.status == CheckStatus::Fail);
if has_fail {
theme::error_box(ui, &t!("check_fail_warning"));
} else if self.checks.iter().any(|c| c.status == CheckStatus::Warn) {
theme::warning_box(ui, &t!("check_warn_notice"));
} else if !self.checks.is_empty() {
theme::success_box(ui, &t!("check_all_pass"));
}
// Yeniden tara butonu
ui.add_space(8.0);
if ui.add(theme::secondary_button(&t!("recheck"))).clicked() {
self.checked = false;
self.on_enter(state);
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
// Demo modunda her zaman ileri gitmeye izin ver
if state.demo_mode {
return true;
}
// Fail varsa ilerleyemez; kontroller bitmemişse de bekle
!self.checks.is_empty() && all_passed(&self.checks)
}
}
// ─────────────────────────────────────────────
// LOCATION STEP
// ─────────────────────────────────────────────
pub struct LocationStep {
search_query: String,
timezones: Vec<String>,
}
impl Default for LocationStep {
fn default() -> Self {
let tz_list = vec![
"Africa/Cairo", "America/Argentina/Buenos_Aires", "America/New_York",
"America/Sao_Paulo", "Asia/Baku", "Asia/Dubai", "Asia/Istanbul",
"Asia/Tokyo", "Australia/Sydney", "Europe/Berlin", "Europe/Istanbul",
"Europe/London", "Europe/Moscow", "Europe/Paris", "Europe/Rome",
"Pacific/Auckland",
].iter().map(|s| s.to_string()).collect();
Self { search_query: String::new(), timezones: tz_list }
}
}
impl InstallerStep for LocationStep {
fn name(&self) -> String { t!("location").to_string() }
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("location_title"));
ui.label(t!("location_description"));
ui.add_space(10.0);
ui.label(t!("search_timezone"));
ui.text_edit_singleline(&mut self.search_query);
ui.add_space(5.0);
egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
for tz in &self.timezones {
if tz.to_lowercase().contains(&self.search_query.to_lowercase()) {
ui.selectable_value(&mut state.timezone, tz.clone(), tz);
}
}
});
}
fn is_complete(&self, state: &GlobalState) -> bool { !state.timezone.is_empty() }
}
+1313
View File
File diff suppressed because it is too large Load Diff
+405
View File
@@ -0,0 +1,405 @@
mod installer;
mod steps;
mod jobs;
mod ui;
mod autoinstall;
use installer::{GlobalState, InstallerStep, WelcomeStep, LocationStep};
use steps::{KeyboardStep, UsersStep, SummaryStep, ExecutionStep, FinishStep, PartitionStep};
use eframe::egui;
use rust_i18n::t;
rust_i18n::i18n!("locales");
fn main() -> eframe::Result<()> {
// ── CLI: otomatik kurulum modu ───────────────────────
let args: Vec<String> = std::env::args().collect();
let demo_mode = args.contains(&"--demo".to_string());
if let Some(pos) = args.iter().position(|a| a == "--auto-install") {
if let Some(path) = args.get(pos + 1) {
return run_auto_install(path, demo_mode);
} else {
eprintln!("Hata: --auto-install <dosya.toml> şeklinde kullanın.");
std::process::exit(1);
}
}
run_gui(demo_mode)
}
/// Grafik arayüzü başlatır (normal mod).
fn run_gui(demo_mode: bool) -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1100.0, 760.0])
.with_min_inner_size([900.0, 650.0])
.with_title(t!("installer").to_string()),
..Default::default()
};
eframe::run_native(
&t!("installer").to_string(),
options,
Box::new(move |cc| {
// Görsel yükleyicileri etkinleştir (PNG desteği)
egui_extras::install_image_loaders(&cc.egui_ctx);
ui::theme::apply(&cc.egui_ctx, true);
let mut app = YaliApp::default();
app.state.demo_mode = demo_mode;
Box::new(app)
}),
)
}
/// Answer file ile tam otomatik, UI-siz kurulum.
fn run_auto_install(path: &str, demo_mode: bool) -> eframe::Result<()> {
use autoinstall::{load, apply_to_state};
use std::path::Path;
println!("=== Yali-RS Otomatik Kurulum ===");
println!("{}: {}", t!("answer_file"), path);
// Dosyayı yükle ve doğrula
let af = match load(Path::new(path)) {
Ok(af) => af,
Err(e) => {
eprintln!("{}: {}", t!("error"), e);
std::process::exit(1);
}
};
println!("{}: {}", t!("disk"), af.install.disk);
println!("{}: {}", t!("timezone"), af.install.timezone);
println!("{}: {}", t!("username"), af.user.username);
println!("{}: {}", t!("hostname"), af.user.hostname);
// Sistem kontrollerini çalıştır
println!("\n--- {} ---", t!("system_checks"));
let checks = autoinstall::checker::run_all();
for c in &checks {
let sym = match c.status {
autoinstall::checker::CheckStatus::Pass => "",
autoinstall::checker::CheckStatus::Warn => "",
autoinstall::checker::CheckStatus::Fail => "",
};
println!("[{}] {}: {}", sym, c.name, c.message);
}
if !autoinstall::checker::all_passed(&checks) {
eprintln!("{}: {}", t!("error"), "Critical check error. Installation stopped.");
std::process::exit(2);
}
// State'i hazırla
let mut state = GlobalState::default();
state.demo_mode = demo_mode;
apply_to_state(&af, &mut state);
if state.partition_plan.is_none() {
eprintln!("{}: {}", t!("error"), format!("{}: {}", t!("partition_plan_could_not_be_created"), af.install.disk));
std::process::exit(3);
}
println!("\n--- {} ---", t!("installation_started"));
// JobQueue'yu oluştur ve tokio'da çalıştır
let locale = af.install.locale.clone();
let timezone = af.install.timezone.clone();
let username = af.user.username.clone();
let password = af.user.password.clone();
let hostname = af.user.hostname.clone();
let kb_layout = af.install.keyboard.clone();
let kb_variant = af.install.keyboard_variant.clone();
let source = af.install.source.clone();
let mount = af.install.mount.clone();
let plan = state.partition_plan.unwrap();
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
rt.block_on(async {
use std::sync::mpsc;
use jobs::{InstallMessage, UiSender};
// CLI modunda egui Context yok; basit bir mpsc kanal kullanıyoruz.
// UiSender'a dummy bir context gerekeceğinden terminal çıktısı için
// doğrudan kanalı dinliyoruz.
let (tx, rx) = mpsc::channel::<InstallMessage>();
// Arka plan iş parçacığı — gerçek job kuyruğunu çalıştırır.
// egui::Context::default() CLI modunda request_repaint()'i no-op yapar;
// mesajlar mpsc kanalı üzerinden terminale aktarılır.
let cli_ctx = eframe::egui::Context::default();
let ui_sender = UiSender::new(tx, cli_ctx);
let plan_c = plan.clone();
let locale_c = locale.clone();
let timezone_c = timezone.clone();
let username_c = username.clone();
let password_c = password.clone();
let hostname_c = hostname.clone();
let kb_layout_c = kb_layout.clone();
let kb_variant_c= kb_variant.clone();
let source_c = source.clone();
let mount_c = mount.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
let mut queue = jobs::build_full_job_queue(
plan_c,
&mount_c,
&source_c,
&locale_c,
&timezone_c,
&username_c,
&password_c,
&hostname_c,
&kb_layout_c,
&kb_variant_c,
demo_mode,
);
rt.block_on(queue.run_all(ui_sender));
});
// Kanalı oku ve terminale yaz
loop {
match rx.recv() {
Ok(InstallMessage::Log(line)) => println!(" {}", line),
Ok(InstallMessage::Progress(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::Done) => {
println!("\n\n{}: {}", t!("installation_done"), t!("installation_done_description"));
break;
}
Ok(InstallMessage::Error(e)) => {
eprintln!("\n{}: {}", t!("error"), e);
std::process::exit(4);
}
Err(_) => break,
}
}
});
// eframe::Result döndürmek için Ok
Ok(())
}
struct YaliApp {
state: GlobalState,
steps: Vec<Box<dyn InstallerStep>>,
last_step: usize,
}
impl Default for YaliApp {
fn default() -> Self {
let mut app = Self {
state: GlobalState::default(),
steps: vec![
Box::new(WelcomeStep::default()),
Box::new(LocationStep::default()),
Box::new(KeyboardStep::default()),
Box::new(PartitionStep::default()),
Box::new(UsersStep::default()),
Box::new(SummaryStep),
Box::new(ExecutionStep::default()),
Box::new(FinishStep),
],
last_step: usize::MAX,
};
app.steps[0].on_enter(&mut app.state);
app.last_step = 0;
app
}
}
impl eframe::App for YaliApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
rust_i18n::set_locale(&self.state.language);
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;
// Adım değişmişse on_enter çağır
if cur != self.last_step {
if let Some(step) = self.steps.get_mut(cur) {
step.on_enter(&mut self.state);
}
self.last_step = cur;
}
// ── Sol panel: adım listesi ──────────────────────────
egui::SidePanel::left("sidebar")
.exact_width(195.0)
.frame(egui::Frame::none().fill(ui::theme::c_sidebar()))
.show(ctx, |ui| {
ui.add_space(20.0);
// PisiLinux logo
ui.vertical_centered(|ui| {
let logo = if self.state.is_dark {
egui::include_image!("../assets/pisi-logo-dark.png")
} else {
egui::include_image!("../assets/pisi-logo-light.png")
};
ui.add(
egui::Image::new(logo)
.max_width(120.0)
.rounding(egui::Rounding::same(8.0)),
);
ui.add_space(8.0);
ui.label(
egui::RichText::new(t!("os_name"))
.size(11.0)
.color(ui::theme::c_text_dim()),
);
// get desktop environment
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
ui.label(
egui::RichText::new(&format!("{} Desktop", desktop))
.size(11.0)
.color(ui::theme::c_text_dim()),
);
});
ui.add_space(16.0);
ui.separator();
ui.add_space(12.0);
// Adım listesi
for (i, step) in self.steps.iter().enumerate() {
let label = format!("{}. {}", i + 1, step.name());
egui::Frame::none()
.fill(if i == cur {
egui::Color32::from_rgba_unmultiplied(0x21, 0x96, 0xF3, 30)
} else {
egui::Color32::TRANSPARENT
})
.rounding(egui::Rounding::same(6.0))
.inner_margin(egui::Margin::symmetric(10.0, 5.0))
.show(ui, |ui| {
ui.set_width(175.0);
if i == cur {
ui.horizontal(|ui| {
ui.colored_label(ui::theme::c_accent(), "");
ui.colored_label(ui::theme::c_text(), label);
});
} else if i < cur {
ui.horizontal(|ui| {
ui.colored_label(ui::theme::c_success(), "");
ui.colored_label(ui::theme::c_text_dim(), label);
});
} else {
ui.colored_label(
egui::Color32::from_rgb(80, 80, 100),
format!(" {}", label),
);
}
});
ui.add_space(2.0);
}
// Alt kısım: tema değiştirme + sürüm bilgisi
ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
ui.add_space(12.0);
ui.colored_label(
egui::Color32::from_rgb(60, 60, 90),
"Yali-RS v0.2.0",
);
ui.add_space(6.0);
// Tema değiştirme butonu
let theme_icon = if self.state.is_dark { "" } else { "🌙" };
let theme_label = if self.state.is_dark { "Light" } else { "Dark" };
if ui.add(
egui::Button::new(
egui::RichText::new(format!("{} {}", theme_icon, theme_label))
.size(12.0)
)
.rounding(egui::Rounding::same(6.0))
.min_size(egui::Vec2::new(100.0, 28.0))
).clicked() {
self.state.is_dark = !self.state.is_dark;
ui::theme::set_theme_mode(self.state.is_dark);
ui::theme::apply(ctx, self.state.is_dark);
}
});
});
// ── Alt panel: navigasyon ────────────────────────────
egui::TopBottomPanel::bottom("footer")
.frame(egui::Frame::none()
.fill(ui::theme::c_bg_panel())
.stroke(egui::Stroke::new(1.0, ui::theme::c_border())))
.show(ctx, |ui| {
ui.add_space(10.0);
ui.horizontal(|ui| {
ui.add_space(8.0);
// İptal — kurulum ve bitiş ekranında gizle
if !is_exec && !is_finish {
if ui.add(ui::theme::secondary_button(&t!("cancel"))).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(8.0);
let is_valid = self.steps
.get(cur)
.map(|s| s.is_complete(&self.state))
.unwrap_or(false);
if !is_finish {
// İleri / Tamamlandı butonu
if !is_exec || is_valid {
let next_label = if is_exec {
t!("finish_next")
} else {
t!("next")
};
let btn = if is_valid {
ui::theme::primary_button(&next_label)
} else {
// Devre dışı görünüm için secondary kullan
ui::theme::secondary_button(&next_label)
};
if ui.add_enabled(is_valid, btn).clicked() {
self.state.current_step += 1;
}
}
// Geri — kurulum ekranında gizle
if cur > 0 && !is_exec {
ui.add_space(8.0);
if ui.add(ui::theme::secondary_button(&t!("back"))).clicked() {
self.state.current_step -= 1;
}
}
}
});
});
ui.add_space(10.0);
});
// ── Merkez panel ─────────────────────────────────────
egui::CentralPanel::default()
.frame(egui::Frame::none()
.fill(ui::theme::c_bg_dark())
.inner_margin(egui::Margin::same(20.0)))
.show(ctx, |ui| {
egui::ScrollArea::vertical()
.id_source("main_scroll")
.show(ui, |ui| {
if let Some(step) = self.steps.get_mut(cur) {
step.show(ui, &mut self.state);
}
});
});
}
}
+305
View File
@@ -0,0 +1,305 @@
//! Kurulumun gerçek olarak yürütüldüğü ekran.
//!
//! Sol yarı : Slayt gösterisi (Slideshow)
//! Sağ yarı : İlerleme çubuğu + renkli log terminali
//!
//! Hata olursa tüm panel ErrorScreen ile kaplanır.
use std::sync::mpsc;
use std::thread;
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::jobs::{self, InstallMessage, UiSender};
use crate::ui::{ErrorScreen, Slideshow};
use crate::ui::theme;
pub struct ExecutionStep {
rx: Option<mpsc::Receiver<InstallMessage>>,
tx: Option<mpsc::Sender<InstallMessage>>,
finished: bool,
error_screen: Option<ErrorScreen>,
slideshow: Option<Slideshow>,
}
impl Default for ExecutionStep {
fn default() -> Self {
Self {
rx: None,
tx: None,
finished: false,
error_screen: None,
slideshow: None,
}
}
}
impl InstallerStep for ExecutionStep {
fn name(&self) -> String { t!("execution").to_string() }
fn on_enter(&mut self, state: &mut GlobalState) {
let (tx, rx) = mpsc::channel::<InstallMessage>();
self.rx = Some(rx);
self.tx = Some(tx);
self.finished = false;
self.error_screen = None;
let desktop = std::env::var("XDG_CURRENT_DESKTOP")
.unwrap_or_else(|_| "default".to_string())
.to_lowercase();
self.slideshow = Some(Slideshow::new(&desktop));
state.install_progress = 0.0;
state.install_log.clear();
state.install_log.push(t!("install_starting").to_string());
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
// ── İş parçacığını ilk show()'da başlat ───────────────
if let Some(tx) = self.tx.take() {
let timezone = state.timezone.clone();
let language = state.language.clone();
let plan = state.partition_plan.clone();
let username = state.username.clone();
let password = state.password.clone();
let hostname = state.hostname.clone();
let kb_layout = state.keyboard_layout.clone();
let kb_variant = state.keyboard_variant.clone();
let demo_mode = state.demo_mode;
let ctx = ui.ctx().clone();
let erase_disk = state.erase_disk;
let custom_partitions = state.custom_partitions.clone();
let selected_disk = state.selected_disk.clone();
let ui_sender = UiSender::new(tx, ctx);
thread::spawn(move || {
let locale = if language == "tr" {
"tr_TR.UTF-8".to_string()
} else {
"en_US.UTF-8".to_string()
};
let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
let src = jobs::detect_source_dir();
if erase_disk {
if let Some(p) = plan {
let mut queue = jobs::build_full_job_queue(
p, "/mnt", &src,
&locale, &timezone,
&username, &password, &hostname,
&kb_layout, &kb_variant,
demo_mode,
);
rt.block_on(queue.run_all(ui_sender));
} else {
ui_sender.send(InstallMessage::Error(
t!("error_partition_plan_not_found").to_string()
));
}
} else {
if let Some(disk) = selected_disk {
let mut queue = jobs::build_custom_job_queue(
disk, custom_partitions, "/mnt", &src,
&locale, &timezone,
&username, &password, &hostname,
&kb_layout, &kb_variant,
demo_mode,
);
rt.block_on(queue.run_all(ui_sender));
} else {
ui_sender.send(InstallMessage::Error(
t!("error_target_disk_not_selected").to_string()
));
}
}
});
}
// ── Kanaldaki mesajları tüket ──────────────────────────
if let Some(ref rx) = self.rx {
loop {
match rx.try_recv() {
Ok(InstallMessage::Log(line)) => {
state.install_log.push(line);
}
Ok(InstallMessage::Progress(p)) => {
state.install_progress = p;
}
Ok(InstallMessage::Done) => {
state.install_progress = 1.0;
self.finished = true;
}
Ok(InstallMessage::Error(e)) => {
self.error_screen = Some(ErrorScreen::new(
t!("install_failed"),
e,
state.install_log.clone(),
));
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => {
if !self.finished && self.error_screen.is_none() {
self.error_screen = Some(ErrorScreen::new(
t!("install_failed"),
t!("error_thread_unexpectedly_terminated").to_string(),
state.install_log.clone(),
));
}
break;
}
}
}
}
// ── Hata ekranı ────────────────────────────────────────
if let Some(ref mut err) = self.error_screen {
let mut retry = false;
let mut quit = false;
err.show(ui, || retry = true, || quit = true);
if retry { self.on_enter(state); }
if quit { ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close); }
return;
}
// ── Normal kurulum görünümü ────────────────────────────
// Üst: Slayt gösterisi | Alt: ilerleme + log
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);
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(); }
});
}
});
});
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);
// İlerleme çubuğu
let running = !self.finished
&& self.error_screen.is_none()
&& 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);
// 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()),
);
});
ui.add_space(4.0);
ui.add(
egui::ProgressBar::new(state.install_progress)
.animate(running)
.desired_height(10.0),
);
});
// 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);
}
});
});
});
});
});
// Kurulum devam ediyorsa sürekli yeniden çiz
if !self.finished && self.error_screen.is_none() {
ui.ctx().request_repaint();
}
}
fn is_complete(&self, _state: &GlobalState) -> bool {
self.finished && self.error_screen.is_none()
}
}
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)
}
}
+60
View File
@@ -0,0 +1,60 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
use crate::ui::theme;
pub struct FinishStep;
impl InstallerStep for FinishStep {
fn name(&self) -> String { t!("finish").to_string() }
fn show(&mut self, ui: &mut egui::Ui, _state: &mut GlobalState) {
ui.add_space(32.0);
ui.vertical_centered(|ui| {
// Büyük onay simgesi
ui.label(
egui::RichText::new("")
.size(72.0)
.color(theme::c_success()),
);
ui.add_space(16.0);
ui.label(
egui::RichText::new(t!("finish_title"))
.size(24.0)
.strong()
.color(theme::c_text()),
);
ui.add_space(8.0);
ui.label(
egui::RichText::new(t!("finish_description"))
.size(14.0)
.color(theme::c_text_dim()),
);
ui.add_space(32.0);
// Yeniden Başlat — ana eylem butonu
if ui.add(
theme::primary_button(&t!("finish_restart"))
.min_size(egui::vec2(200.0, 44.0)),
).clicked() {
let _ = std::process::Command::new("systemctl")
.arg("reboot")
.spawn();
}
ui.add_space(12.0);
// Canlı masaüstüne dön — ikincil eylem
if ui.add(
theme::secondary_button(&t!("finish_live"))
.min_size(egui::vec2(200.0, 36.0)),
).clicked() {
ui.ctx().send_viewport_cmd(egui::ViewportCommand::Close);
}
});
}
fn is_complete(&self, _state: &GlobalState) -> bool { true }
}
+103
View File
@@ -0,0 +1,103 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
/// Klavye düzeni ve varyantı seçimi.
/// Gerçek uygulamada mevcut düzenler `localectl list-keymaps`
/// veya `/usr/share/X11/xkb/rules/evdev.lst` dosyasından okunabilir.
pub struct KeyboardStep {
test_input: String,
layouts: Vec<(&'static str, &'static str)>, // (kod, gösterim adı)
}
impl Default for KeyboardStep {
fn default() -> Self {
Self {
test_input: String::new(),
layouts: vec![
("tr", "Türkçe (Q)"),
("tr:f", "Türkçe (F)"),
("us", "İngilizce (US)"),
("de", "Almanca"),
("fr", "Fransızca"),
("es", "İspanyolca"),
("ru", "Rusça"),
("ar", "Arapça"),
],
}
}
}
impl InstallerStep for KeyboardStep {
fn name(&self) -> String {
t!("keyboard").to_string()
}
fn on_enter(&mut self, state: &mut GlobalState) {
// Varsayılan düzen sistemi diline göre seçilir
if state.keyboard_layout.is_empty() {
state.keyboard_layout = if state.language == "tr" {
"tr".to_string()
} else {
"us".to_string()
};
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("keyboard_title"));
ui.label(t!("keyboard_description"));
ui.add_space(12.0);
ui.columns(2, |cols| {
// Sol: Düzen listesi
cols[0].label(t!("keyboard_layout_label"));
egui::ScrollArea::vertical()
.id_source("kbd_layouts")
.max_height(260.0)
.show(&mut cols[0], |ui| {
// &(code, display) pattern ile code: &str, display: &str olur
for &(code, display) in &self.layouts {
// tr:f kodunu layout+variant olarak ayır
let (layout, variant) = if let Some(pos) = code.find(':') {
(&code[..pos], &code[pos + 1..])
} else {
(code, "")
};
let is_selected = state.keyboard_layout == layout
&& state.keyboard_variant == variant;
if ui.selectable_label(is_selected, display).clicked() {
state.keyboard_layout = layout.to_string();
state.keyboard_variant = variant.to_string();
}
}
});
// Sağ: Test alanı
cols[1].label(t!("keyboard_test_label"));
cols[1].add_space(4.0);
cols[1].add(
egui::TextEdit::multiline(&mut self.test_input)
.desired_rows(5)
.hint_text(t!("keyboard_test_hint")),
);
cols[1].add_space(8.0);
cols[1].label(format!("{}: {}", t!("keyboard_selected"), {
let variant_part = if state.keyboard_variant.is_empty() {
String::new()
} else {
format!(" ({})", state.keyboard_variant)
};
format!("{}{}", state.keyboard_layout, variant_part)
}));
});
}
fn is_complete(&self, state: &GlobalState) -> bool {
!state.keyboard_layout.is_empty()
}
}
+13
View File
@@ -0,0 +1,13 @@
pub mod keyboard;
pub mod users;
pub mod summary;
pub mod execution;
pub mod finish;
pub mod partition;
pub use keyboard::KeyboardStep;
pub use users::UsersStep;
pub use summary::SummaryStep;
pub use execution::ExecutionStep;
pub use finish::FinishStep;
pub use partition::PartitionStep;
+585
View File
@@ -0,0 +1,585 @@
//! Disk bölümleme adımı — Otomatik ve Manuel mod destekli.
use eframe::egui;
use rust_i18n::t;
// sysinfo::Disks mount point başına bir kayıt döndürür (partition'lar),
// ham disk listesi için lsblk kullanıyoruz.
/// `/sys/block` altındaki gerçek blok aygıtlarını (partition değil) listeler.
/// lsblk'dan JSON çıktısı ayrıştırılır; başarısız olursa /sys/block fallback.
fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
// lsblk -d: sadece diskler (partition'lar hariç)
// -o NAME,SIZE,MODEL,TYPE -b: byte cinsinden boyut
let out = std::process::Command::new("lsblk")
.args(["-d", "-o", "NAME,SIZE,MODEL", "-b", "--noheadings"])
.output();
let mut disks = Vec::new();
if let Ok(o) = out {
let text = String::from_utf8_lossy(&o.stdout);
for line in text.lines() {
let cols: Vec<&str> = line.splitn(3, char::is_whitespace)
.filter(|s| !s.is_empty())
.collect();
if cols.len() < 2 { continue; }
let name_raw = cols[0]; // örn. "sda" veya "nvme0n1"
// loop, ram, sr aygıtlarını atla
if name_raw.starts_with("loop")
|| name_raw.starts_with("ram")
|| name_raw.starts_with("sr")
{ continue; }
let size_bytes: u64 = cols[1].trim().parse().unwrap_or(0);
let model = if cols.len() >= 3 { cols[2].trim() } else { "" };
disks.push(crate::installer::DiskInfo {
name: format!("/dev/{}", name_raw),
model: model.to_string(),
size_gb: size_bytes / 1_073_741_824,
size_bytes,
});
}
}
// lsblk başarısız olduysa /sys/block fallback
if disks.is_empty() {
if let Ok(rd) = std::fs::read_dir("/sys/block") {
for entry in rd.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("loop") || name.starts_with("ram") { continue; }
let size_path = format!("/sys/block/{}/size", name);
let size_sectors: u64 = std::fs::read_to_string(&size_path)
.unwrap_or_default().trim().parse().unwrap_or(0);
let size_bytes = size_sectors * 512;
let model_path = format!("/sys/block/{}/device/model", name);
let model = std::fs::read_to_string(&model_path)
.unwrap_or_default().trim().to_string();
disks.push(crate::installer::DiskInfo {
name: format!("/dev/{}", name),
model,
size_gb: size_bytes / 1_073_741_824,
size_bytes,
});
}
}
}
disks
}
use crate::installer::{
CustomPartition, DiskInfo, FsType, GlobalState, InstallerStep,
PartitionPlan, PartitionTableType,
};
const MIN_DISK_GB: u64 = 10;
const EFI_MB: u64 = 512;
const SWAP_MB_MIN: u64 = 512;
const SWAP_MB_MAX: u64 = 8 * 1024;
// ─────────────────────────────────────────────
// Manuel bölümleme için yerel durum
// ─────────────────────────────────────────────
#[derive(Default)]
struct ManualState {
/// "Bölüm Ekle" diyaloğu açık mı?
adding: bool,
/// Diyalog: seçili dosya sistemi
add_fstype: usize, // FsType listesindeki indeks
/// Diyalog: boyut alanı (MB, metin girişi)
add_size_str: String,
/// Diyalog: bağlama noktası
add_mountpoint: String,
/// Silinmek istenen bölüm indeksi (onay isteği)
delete_idx: Option<usize>,
/// Genel hata mesajı
error: Option<String>,
}
const FS_OPTIONS: &[(&str, FsType)] = &[
("ext4", FsType::Ext4),
("btrfs", FsType::Btrfs),
("xfs", FsType::Xfs),
("FAT32 (EFI)", FsType::Fat32),
("swap", FsType::Swap),
];
// ─────────────────────────────────────────────
// PartitionStep
// ─────────────────────────────────────────────
pub struct PartitionStep {
scanned: bool,
is_uefi: bool,
manual: ManualState,
}
impl Default for PartitionStep {
fn default() -> Self {
Self {
scanned: false,
is_uefi: std::path::Path::new("/sys/firmware/efi").exists(),
manual: ManualState::default(),
}
}
}
/// RAM miktarına göre önerilen swap alanı (MB).
fn recommended_swap_mb() -> u64 {
use sysinfo::System;
let mut sys = System::new();
sys.refresh_memory();
let ram_mb = sys.total_memory() / 1024 / 1024;
let swap = if ram_mb <= 2048 {
ram_mb
} else if ram_mb <= 8192 {
ram_mb / 2
} else {
4096
};
swap.clamp(SWAP_MB_MIN, SWAP_MB_MAX)
}
/// Seçili disk için otomatik bölümleme planı hesapla.
fn make_plan(disk: &DiskInfo, is_uefi: bool) -> Option<PartitionPlan> {
if disk.size_gb < MIN_DISK_GB {
return None;
}
let total_mb = disk.size_bytes / 1024 / 1024;
let efi_mb = if is_uefi { EFI_MB } else { 0 };
let swap_mb = recommended_swap_mb();
let overhead = efi_mb + swap_mb + 512;
let root_mb = total_mb.saturating_sub(overhead);
if root_mb < 6144 { return None; }
Some(PartitionPlan {
disk: disk.name.clone(),
table_type: if is_uefi { PartitionTableType::Gpt } else { PartitionTableType::Mbr },
efi_mb,
swap_mb,
root_mb,
})
}
/// Manuel listedeki bölümlerin geçerliliğini kontrol eder.
fn validate_manual(parts: &[CustomPartition], is_uefi: bool) -> Option<String> {
let has_root = parts.iter().any(|p| p.mountpoint == "/");
let has_efi = parts.iter().any(|p| p.mountpoint == "/boot/efi" || p.fstype == FsType::Fat32);
if !has_root {
return Some(t!("mp_no_root").to_string());
}
if is_uefi && !has_efi {
return Some(t!("mp_no_efi").to_string());
}
None
}
impl InstallerStep for PartitionStep {
fn name(&self) -> String { t!("partition").to_string() }
fn on_enter(&mut self, state: &mut GlobalState) {
if !self.scanned {
state.available_disks = list_block_devices();
state.available_disks.retain(|d| d.size_gb >= 4);
self.scanned = true;
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("partition_title"));
ui.label(t!("partition_description"));
ui.add_space(10.0);
if state.available_disks.is_empty() {
ui.colored_label(egui::Color32::RED, t!("no_disks_found"));
if ui.button(t!("rescan_disks")).clicked() {
self.scanned = false;
self.on_enter(state);
}
return;
}
// ── Mod seçimi ─────────────────────────────────────────
ui.horizontal(|ui| {
ui.selectable_value(&mut state.erase_disk, true, t!("erase_disk_label"));
ui.selectable_value(&mut state.erase_disk, false, t!("manual_partition_label"));
});
ui.add_space(10.0);
if state.erase_disk {
self.show_auto_mode(ui, state);
} else {
self.show_manual_mode(ui, state);
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
if state.erase_disk {
state.partition_plan.is_some()
} else {
!state.custom_partitions.is_empty()
&& validate_manual(&state.custom_partitions, self.is_uefi).is_none()
}
}
}
// ─────────────────────────────────────────────
// Otomatik mod
// ─────────────────────────────────────────────
impl PartitionStep {
fn show_auto_mode(&self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.group(|ui| {
ui.label(t!("select_disk_label"));
ui.add_space(4.0);
let disks = state.available_disks.clone();
for disk in &disks {
let too_small = disk.size_gb < MIN_DISK_GB;
let label = format!("{}{} GB ({})", disk.name, disk.size_gb, disk.model);
ui.add_enabled_ui(!too_small, |ui| {
let selected = state.selected_disk.as_deref() == Some(&disk.name);
if ui.radio(selected, &label).clicked() {
state.selected_disk = Some(disk.name.clone());
state.partition_plan = make_plan(disk, self.is_uefi);
}
});
if too_small {
ui.small(t!("disk_min_required", err = t!("disk_too_small"), n = MIN_DISK_GB.to_string()));
}
}
});
if let Some(ref plan) = state.partition_plan {
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
ui.strong(t!("partition_plan_title"));
ui.add_space(6.0);
partition_bar(ui, plan);
ui.add_space(8.0);
egui::Grid::new("plan_grid")
.num_columns(3)
.spacing([20.0, 6.0])
.striped(true)
.show(ui, |ui| {
ui.strong(t!("plan_col_part"));
ui.strong(t!("plan_col_fs"));
ui.strong(t!("plan_col_size"));
ui.end_row();
if plan.efi_mb > 0 {
ui.label(format!("{}1 (EFI)", plan.disk));
ui.label("FAT32");
ui.label(format!("{} MB", plan.efi_mb));
ui.end_row();
}
let swap_part = if plan.efi_mb > 0 { 2 } else { 1 };
ui.label(format!("{}{} (swap)", plan.disk, swap_part));
ui.label("swap");
ui.label(format!("{} MB", plan.swap_mb));
ui.end_row();
let root_part = swap_part + 1;
ui.label(format!("{}{}{}", plan.disk, root_part, t!("plan_root_suffix")));
ui.label("ext4");
ui.label(format!("{:.1} GB", plan.root_mb as f64 / 1024.0));
ui.end_row();
});
ui.add_space(8.0);
ui.label(format!("{}: {}", t!("partition_table_type"), plan.table_type));
ui.add_space(10.0);
egui::Frame::none()
.fill(egui::Color32::from_rgba_unmultiplied(200, 60, 40, 25))
.rounding(5.0)
.inner_margin(egui::Margin::same(8.0))
.show(ui, |ui| {
ui.colored_label(
egui::Color32::from_rgb(210, 80, 60),
format!("{}: {}", t!("erase_warning"), plan.disk),
);
});
}
}
}
// ─────────────────────────────────────────────
// Manuel mod
// ─────────────────────────────────────────────
impl PartitionStep {
fn show_manual_mode(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
let accent = crate::ui::theme::c_accent();
// ── Disk seçimi (hedef disk) ────────────────────────────
ui.group(|ui| {
ui.label(t!("select_disk_label"));
ui.add_space(4.0);
let disks = state.available_disks.clone();
for disk in &disks {
let selected = state.selected_disk.as_deref() == Some(&disk.name);
let label = format!("{}{} GB ({})", disk.name, disk.size_gb, disk.model);
if ui.radio(selected, &label).clicked() {
state.selected_disk = Some(disk.name.clone());
// Disk değişince listeyi sıfırla
state.custom_partitions.clear();
self.manual.error = None;
}
}
});
ui.add_space(8.0);
let disk = match &state.selected_disk {
Some(d) => d.clone(),
None => {
ui.label(t!("mp_select_disk_first"));
return;
}
};
// ── Mevcut bölüm listesi tablosu ───────────────────────
ui.strong(t!("mp_plan_title"));
ui.add_space(4.0);
let mut delete_idx: Option<usize> = None;
egui::Grid::new("manual_parts_grid")
.num_columns(5)
.spacing([12.0, 6.0])
.striped(true)
.show(ui, |ui| {
// Başlık
ui.strong(t!("mp_device"));
ui.strong(t!("mp_size"));
ui.strong(t!("mp_fstype"));
ui.strong(t!("mp_mountpoint"));
ui.strong(t!("mp_action"));
ui.end_row();
for (i, part) in state.custom_partitions.iter().enumerate() {
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)
} else {
format!("{} MB", part.size_mb)
};
ui.label(&part.device);
ui.label(&size_str);
ui.label(part.fstype.label());
ui.label(&part.mountpoint);
if ui.small_button("🗑").on_hover_text(t!("mp_delete")).clicked() {
delete_idx = Some(i);
}
ui.end_row();
}
});
// Silme onayı
if let Some(idx) = delete_idx {
self.manual.delete_idx = Some(idx);
}
if let Some(idx) = self.manual.delete_idx {
let dev = state.custom_partitions.get(idx)
.map(|p| p.device.clone())
.unwrap_or_default();
egui::Window::new(t!("mp_delete_confirm_title"))
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ui.ctx(), |ui| {
ui.label(t!("mp_confirm_delete", dev = dev));
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui.button(t!("mp_delete_confirm_yes")).clicked() {
if idx < state.custom_partitions.len() {
state.custom_partitions.remove(idx);
}
self.manual.delete_idx = None;
}
if ui.button(t!("cancel")).clicked() {
self.manual.delete_idx = None;
}
});
});
}
ui.add_space(8.0);
// ── "Bölüm Ekle" butonu ────────────────────────────────
let btn = egui::Button::new(format!(" {}", t!("mp_add")))
.fill(accent.gamma_multiply(0.15));
if ui.add(btn).clicked() {
self.manual.adding = true;
let next_num = state.custom_partitions.len() + 1;
self.manual.add_size_str.clear();
self.manual.add_mountpoint.clear();
self.manual.add_fstype = 0;
// Akıllı varsayılan bağlama noktası
self.manual.add_mountpoint = match next_num {
1 if self.is_uefi => "/boot/efi".to_string(),
1 => "/".to_string(),
2 if self.is_uefi => "swap".to_string(),
2 => "swap".to_string(),
3 => "/".to_string(),
_ => String::new(),
};
}
// ── "Bölüm Ekle" diyaloğu ──────────────────────────────
if self.manual.adding {
egui::Window::new(t!("mp_add"))
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ui.ctx(), |ui| {
egui::Grid::new("add_part_grid")
.num_columns(2)
.spacing([12.0, 8.0])
.show(ui, |ui| {
// Dosya sistemi
ui.label(t!("mp_fstype"));
egui::ComboBox::from_id_source("fs_combo")
.selected_text(FS_OPTIONS[self.manual.add_fstype].0)
.show_ui(ui, |ui| {
for (i, (label, _)) in FS_OPTIONS.iter().enumerate() {
ui.selectable_value(
&mut self.manual.add_fstype, i, *label
);
}
});
ui.end_row();
// Boyut
ui.label(t!("mp_size_hint"));
ui.text_edit_singleline(&mut self.manual.add_size_str);
ui.end_row();
// Bağlama noktası
ui.label(t!("mp_mountpoint"));
let mp_field = egui::TextEdit::singleline(&mut self.manual.add_mountpoint)
.hint_text(t!("mp_add_dialog_hint"));
ui.add(mp_field);
ui.end_row();
});
ui.add_space(8.0);
ui.horizontal(|ui| {
let ok_btn = egui::Button::new(t!("mp_add_btn")).fill(accent.gamma_multiply(0.2));
if ui.add(ok_btn).clicked() {
let size_mb: u64 = self.manual.add_size_str
.trim().parse().unwrap_or(0);
let mp = self.manual.add_mountpoint.trim().to_string();
let fstype = FS_OPTIONS[self.manual.add_fstype].1.clone();
let part_num = state.custom_partitions.len() + 1;
let device = crate::installer::part_path(&disk, part_num as u32);
if mp.is_empty() {
self.manual.error = Some(t!("mp_error_empty_mountpoint").to_string());
} else {
state.custom_partitions.push(CustomPartition {
device,
size_mb,
fstype,
mountpoint: mp,
});
self.manual.error = None;
self.manual.adding = false;
}
}
if ui.button(t!("cancel")).clicked() {
self.manual.adding = false;
}
});
});
}
// ── Doğrulama mesajları ─────────────────────────────────
ui.add_space(6.0);
if let Some(ref err) = self.manual.error {
ui.colored_label(egui::Color32::from_rgb(217, 0, 91), err);
}
if !state.custom_partitions.is_empty() {
if let Some(warn) = validate_manual(&state.custom_partitions, self.is_uefi) {
ui.colored_label(egui::Color32::from_rgb(220, 160, 40), warn);
} else {
ui.colored_label(
egui::Color32::from_rgb(60, 180, 90),
t!("mp_plan_valid"),
);
}
}
}
}
// ─────────────────────────────────────────────
// Yardımcı: renkli bölüm şeridi
// ─────────────────────────────────────────────
fn partition_bar(ui: &mut egui::Ui, plan: &PartitionPlan) {
let total = (plan.efi_mb + plan.swap_mb + plan.root_mb) as f32;
let bar_height = 28.0;
let (rect, _) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), bar_height),
egui::Sense::hover(),
);
let painter = ui.painter();
struct Seg<'a> { frac: f32, color: egui::Color32, label: &'a str }
let mut segments: Vec<Seg> = Vec::new();
if plan.efi_mb > 0 {
segments.push(Seg {
frac: plan.efi_mb as f32 / total,
color: egui::Color32::from_rgb(80, 160, 220),
label: "EFI",
});
}
segments.push(Seg {
frac: plan.swap_mb as f32 / total,
color: egui::Color32::from_rgb(220, 160, 40),
label: "swap",
});
segments.push(Seg {
frac: plan.root_mb as f32 / total,
color: egui::Color32::from_rgb(80, 190, 100),
label: "/",
});
let mut x = rect.left();
let rounding = egui::Rounding::same(4.0);
for (i, seg) in segments.iter().enumerate() {
let w = seg.frac * rect.width();
let seg_rect = egui::Rect::from_min_size(
egui::pos2(x, rect.top()),
egui::vec2(w - 1.0, bar_height),
);
let seg_rounding = if i == 0 {
egui::Rounding { nw: rounding.nw, sw: rounding.sw, ne: 0.0, se: 0.0 }
} else if i == segments.len() - 1 {
egui::Rounding { nw: 0.0, sw: 0.0, ne: rounding.ne, se: rounding.se }
} else {
egui::Rounding::ZERO
};
painter.rect_filled(seg_rect, seg_rounding, seg.color);
painter.text(
seg_rect.center(),
egui::Align2::CENTER_CENTER,
seg.label,
egui::FontId::proportional(12.0),
egui::Color32::WHITE,
);
x += w;
}
}
+111
View File
@@ -0,0 +1,111 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
/// Kurulumdan önce kullanıcının tüm seçimlerini gösteren özet ekranı.
/// Calamares'in Summary modülüne karşılık gelir.
pub struct SummaryStep;
impl InstallerStep for SummaryStep {
fn name(&self) -> String {
t!("summary").to_string()
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("summary_title"));
ui.label(t!("summary_description"));
ui.add_space(12.0);
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.set_width(ui.available_width());
egui::Grid::new("summary_grid")
.num_columns(2)
.spacing([20.0, 8.0])
.striped(true)
.show(ui, |ui| {
summary_row(ui, t!("summary_language"), &state.language);
summary_row(ui, t!("summary_timezone"), &state.timezone);
let kb = if state.keyboard_variant.is_empty() {
state.keyboard_layout.clone()
} else {
format!("{} ({})", state.keyboard_layout, state.keyboard_variant)
};
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"))
}
}
None => t!("summary_not_selected").to_string(),
};
summary_row(ui, t!("summary_disk"), &disk_str);
// Bölüm planı detayları
if state.erase_disk {
if let Some(ref plan) = state.partition_plan {
let plan_str = if plan.efi_mb > 0 {
format!("EFI {}MB | swap {}MB | / {:.1}GB ({})",
plan.efi_mb, plan.swap_mb,
plan.root_mb as f64 / 1024.0,
plan.table_type)
} else {
format!("swap {}MB | / {:.1}GB ({})",
plan.swap_mb,
plan.root_mb as f64 / 1024.0,
plan.table_type)
};
summary_row(ui, t!("summary_partition_plan"), &plan_str);
}
} else if !state.custom_partitions.is_empty() {
let mut plan_lines = 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)
} else {
format!("{} MB", part.size_mb)
};
plan_lines.push(format!("{}{} ({}, {})", part.device, part.mountpoint, part.fstype, size_str));
}
summary_row(ui, t!("summary_custom_partitions"), &plan_lines.join("\n"));
}
summary_row(ui, t!("summary_username"), &state.username);
summary_row(ui, t!("summary_hostname"), &state.hostname);
});
});
ui.add_space(16.0);
// Uyarı kutusu
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"),
);
});
}
fn is_complete(&self, _state: &GlobalState) -> bool {
// Özet her zaman geçerlidir; önceki adımlar bunu garantiler.
true
}
}
fn summary_row(ui: &mut egui::Ui, label: impl Into<String>, value: &str) {
ui.strong(label.into());
ui.label(value);
ui.end_row();
}
+186
View File
@@ -0,0 +1,186 @@
use eframe::egui;
use rust_i18n::t;
use crate::installer::{GlobalState, InstallerStep};
/// Kullanıcı adı, şifre ve hostname ayarları adımı.
/// Calamares'in Users modülüne karşılık gelir.
pub struct UsersStep {
show_password: bool,
show_password_confirm: bool,
}
impl Default for UsersStep {
fn default() -> Self {
Self {
show_password: false,
show_password_confirm: false,
}
}
}
/// Kullanıcı adının geçerliliğini kontrol eder.
/// Linux kuralları: küçük harf veya alt çizgi ile başlamalı,
/// en fazla 32 karakter, yalnızca [a-z0-9_-] içermeli.
fn is_valid_username(s: &str) -> bool {
if s.is_empty() || s.len() > 32 {
return false;
}
let mut chars = s.chars();
// İlk karakter küçük harf veya alt çizgi olmalı
match chars.next() {
Some(c) if c.is_ascii_lowercase() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
}
/// Hostname kuralları: 1-63 karakter, yalnızca [a-zA-Z0-9-], tire ile başlayamaz/bitemez.
fn is_valid_hostname(s: &str) -> bool {
if s.is_empty() || s.len() > 63 {
return false;
}
if s.starts_with('-') || s.ends_with('-') {
return false;
}
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
fn password_strength(p: &str) -> (String, egui::Color32) {
let len = p.len();
let has_upper = p.chars().any(|c| c.is_uppercase());
let has_digit = p.chars().any(|c| c.is_ascii_digit());
let has_special = p.chars().any(|c| "!@#$%^&*()_+-=[]{}|;':\",./<>?".contains(c));
let score = (len >= 8) as u8
+ (len >= 12) as u8
+ has_upper as u8
+ has_digit as u8
+ has_special as u8;
match score {
0..=1 => (t!("pwd_very_weak").to_string(), egui::Color32::from_rgb(200, 60, 40)),
2 => (t!("pwd_weak").to_string(), egui::Color32::from_rgb(220, 140, 40)),
3 => (t!("pwd_medium").to_string(), egui::Color32::from_rgb(200, 180, 40)),
4 => (t!("pwd_strong").to_string(), egui::Color32::from_rgb(80, 170, 80)),
_ => (t!("pwd_very_strong").to_string(), egui::Color32::from_rgb(40, 140, 60)),
}
}
impl InstallerStep for UsersStep {
fn name(&self) -> String {
t!("users").to_string()
}
fn on_enter(&mut self, state: &mut GlobalState) {
// Hostname boşsa username'den öner
if state.hostname.is_empty() && !state.username.is_empty() {
state.hostname = format!("{}-pc", state.username);
}
}
fn show(&mut self, ui: &mut egui::Ui, state: &mut GlobalState) {
ui.heading(t!("users_title"));
ui.label(t!("users_description"));
ui.add_space(12.0);
egui::Grid::new("users_grid")
.num_columns(2)
.spacing([12.0, 10.0])
.show(ui, |ui| {
// Kullanıcı adı
ui.label(t!("username_label"));
ui.horizontal(|ui| {
ui.text_edit_singleline(&mut state.username);
if !state.username.is_empty() {
if is_valid_username(&state.username) {
ui.colored_label(egui::Color32::from_rgb(80, 170, 80), "");
} else {
ui.colored_label(egui::Color32::from_rgb(200, 60, 40), "");
}
}
});
ui.end_row();
// Şifre
ui.label(t!("password_label"));
ui.horizontal(|ui| {
if self.show_password {
ui.text_edit_singleline(&mut state.password);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password).password(true));
}
if ui.small_button(if self.show_password { "🙈" } else { "👁" }).clicked() {
self.show_password = !self.show_password;
}
});
ui.end_row();
// Şifre gücü göstergesi
ui.label("");
if !state.password.is_empty() {
let (label, color) = password_strength(&state.password);
ui.colored_label(color, label);
}
ui.end_row();
// Şifre doğrulama
ui.label(t!("password_confirm_label"));
ui.horizontal(|ui| {
if self.show_password_confirm {
ui.text_edit_singleline(&mut state.password_confirm);
} else {
ui.add(egui::TextEdit::singleline(&mut state.password_confirm).password(true));
}
if ui.small_button(if self.show_password_confirm { "🙈" } else { "👁" }).clicked() {
self.show_password_confirm = !self.show_password_confirm;
}
if !state.password_confirm.is_empty() {
if state.password == state.password_confirm {
ui.colored_label(egui::Color32::from_rgb(80, 170, 80), "");
} else {
ui.colored_label(egui::Color32::from_rgb(200, 60, 40), t!("passwords_no_match"));
}
}
});
ui.end_row();
// Hostname
ui.label(t!("hostname_label"));
ui.horizontal(|ui| {
// Username değişince hostname otomatik güncellenir (boşsa)
if state.hostname.is_empty() && !state.username.is_empty() {
state.hostname = format!("{}-pc", state.username);
}
ui.text_edit_singleline(&mut state.hostname);
if !state.hostname.is_empty() {
if is_valid_hostname(&state.hostname) {
ui.colored_label(egui::Color32::from_rgb(80, 170, 80), "");
} else {
ui.colored_label(egui::Color32::from_rgb(200, 60, 40), "");
}
}
});
ui.end_row();
});
// Hata mesajları
ui.add_space(8.0);
if !state.username.is_empty() && !is_valid_username(&state.username) {
ui.colored_label(
egui::Color32::from_rgb(200, 60, 40),
t!("username_invalid"),
);
}
if !state.hostname.is_empty() && !is_valid_hostname(&state.hostname) {
ui.colored_label(
egui::Color32::from_rgb(200, 60, 40),
t!("hostname_invalid"),
);
}
}
fn is_complete(&self, state: &GlobalState) -> bool {
is_valid_username(&state.username)
&& is_valid_hostname(&state.hostname)
&& !state.password.is_empty()
&& state.password == state.password_confirm
}
}
+165
View File
@@ -0,0 +1,165 @@
//! Hata yakalama ve kullanıcıya bildirme ekranı.
//!
//! Kurulum sırasında kritik bir hata oluştuğunda
//! bu ekran tüm merkez paneli kaplar.
//! Log detayları açılıp kapanabilir; kullanıcı
//! hata raporunu panoya kopyalayabilir.
use eframe::egui;
use rust_i18n::t;
use crate::ui::theme;
pub struct ErrorScreen {
pub title: String,
pub message: String,
pub log_lines: Vec<String>,
log_expanded: bool,
copied: bool,
copied_timer: u8, // frame sayacı; 0 olunca "Kopyalandı" etiketi kaybolur
}
impl ErrorScreen {
pub fn new(title: impl Into<String>, message: impl Into<String>, log: Vec<String>) -> Self {
Self {
title: title.into(),
message: message.into(),
log_lines: log,
log_expanded: false,
copied: false,
copied_timer: 0,
}
}
/// Ekranı çizer. `on_retry` ve `on_quit` kapanışları buton basımlarında çağrılır.
pub fn show(
&mut self,
ui: &mut egui::Ui,
on_retry: impl FnOnce(),
on_quit: impl FnOnce(),
) {
// "Kopyalandı" zamanlayıcısını azalt
if self.copied && self.copied_timer > 0 {
self.copied_timer -= 1;
if self.copied_timer == 0 {
self.copied = false;
}
ui.ctx().request_repaint();
}
ui.add_space(24.0);
ui.vertical_centered(|ui| {
// ── Büyük hata simgesi ─────────────────────────
ui.label(
egui::RichText::new("")
.size(56.0)
.color(theme::c_error()),
);
ui.add_space(10.0);
// ── Başlık ────────────────────────────────────
ui.label(
egui::RichText::new(&self.title)
.size(22.0)
.strong()
.color(theme::c_text()),
);
ui.add_space(6.0);
// ── Hata mesajı ───────────────────────────────
egui::Frame::none()
.fill(egui::Color32::from_rgba_unmultiplied(0xF4, 0x43, 0x36, 18))
.stroke(egui::Stroke::new(1.0, theme::c_error()))
.rounding(egui::Rounding::same(8.0))
.inner_margin(egui::Margin::same(12.0))
.show(ui, |ui| {
ui.set_width(ui.available_width().min(560.0));
ui.colored_label(theme::c_error(), &self.message);
});
ui.add_space(16.0);
// ── Log bölümü (aç/kapat) ─────────────────────
let log_label = if self.log_expanded {
t!("error_hide_log")
} else {
t!("error_show_log")
};
if ui.button(log_label).clicked() {
self.log_expanded = !self.log_expanded;
}
if self.log_expanded {
ui.add_space(6.0);
egui::ScrollArea::vertical()
.max_height(180.0)
.id_source("error_log")
.show(ui, |ui| {
egui::Frame::none()
.fill(egui::Color32::from_rgb(14, 14, 22))
.rounding(egui::Rounding::same(6.0))
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.set_width(ui.available_width().min(560.0));
for line in &self.log_lines {
let color = log_line_color(line);
ui.colored_label(color, line);
}
});
});
ui.add_space(6.0);
// Panoya kopyala
let copy_label = if self.copied {
t!("error_copied")
} else {
t!("error_copy_log")
};
if ui.small_button(copy_label).clicked() && !self.copied {
let full_log = self.log_lines.join("\n");
ui.ctx().copy_text(full_log);
self.copied = true;
self.copied_timer = 120; // ~2 saniye (60fps varsayımı)
}
}
ui.add_space(20.0);
// ── Eylem butonları ───────────────────────────
ui.horizontal(|ui| {
// Ortalamak için boş genişlik hesapla
let btn_w = 140.0;
let gap = 16.0;
let total = btn_w * 2.0 + gap;
let side = (ui.available_width() - total) / 2.0;
ui.add_space(side.max(0.0));
if ui.add(theme::danger_button(&t!("error_quit"))).clicked() {
on_quit();
}
ui.add_space(gap);
if ui.add(theme::primary_button(&t!("error_retry"))).clicked() {
on_retry();
}
});
});
}
}
fn log_line_color(line: &str) -> egui::Color32 {
if line.starts_with('✗') || line.to_lowercase().contains("hata") || line.to_lowercase().contains("error") {
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(180, 180, 200)
}
}
+6
View File
@@ -0,0 +1,6 @@
pub mod theme;
pub mod slideshow;
pub mod error_screen;
pub use error_screen::ErrorScreen;
pub use slideshow::Slideshow;
+428
View File
@@ -0,0 +1,428 @@
//! Kurulum sırasında gösterilen slayt gösterisi.
//!
//! 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.
use eframe::egui;
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 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) -> Vec<Slide> {
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",
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",
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",
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",
title: t!("lumina_applications_title"),
description: t!("lumina_applications_description"),
},
Slide {
icon: "lumina_theme_title",
title: t!("lumina_theme_title"),
description: t!("lumina_theme_description"),
},
Slide {
icon: "lumina_panel_title",
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",
title: t!("mate_settings_title"),
description: t!("mate_settings_description"),
},
Slide {
icon: "mate_applications_title",
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",
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,
}
impl Slideshow {
pub fn new(desktop: &str) -> Self {
Self {
slides: slides_for_desktop(desktop),
current: 0,
last_advance: Instant::now(),
}
}
/// Her frame'de çağrılır. Otomatik ilerlemeyi ve çizimi yönetir.
pub fn show(&mut self, ui: &mut egui::Ui) {
// Otomatik ilerleme
if self.last_advance.elapsed() >= SLIDE_INTERVAL {
self.current = (self.current + 1) % self.slides.len();
self.last_advance = Instant::now();
ui.ctx().request_repaint_after(SLIDE_INTERVAL);
} else {
// Bir sonraki değişime kadar geri sayım için tekrar çiz
let remaining = SLIDE_INTERVAL
.checked_sub(self.last_advance.elapsed())
.unwrap_or_default();
ui.ctx().request_repaint_after(remaining);
}
let slide = &self.slides[self.current];
let is_dark = ui.visuals().dark_mode;
// Eşleşen slayt simgeleri için görsel belirle
let image_source = match slide.icon {
"budgie" => Some(egui::include_image!("../../assets/budgie.png")),
"cinnamon" => Some(egui::include_image!("../../assets/cinnamon.png")),
"community" => Some(egui::include_image!("../../assets/community.png")),
"file_manager" => Some(egui::include_image!("../../assets/file_manager.png")),
"gnome" => Some(egui::include_image!("../../assets/gnome.png")),
"kde" => Some(egui::include_image!("../../assets/kde.png")),
"lxqt" => Some(egui::include_image!("../../assets/lxqt.png")),
"lumina" => Some(egui::include_image!("../../assets/lumina.png")),
"lxde" => Some(egui::include_image!("../../assets/lxde.png")),
"mate" => Some(egui::include_image!("../../assets/mate.png")),
"pisilogo" => {
if is_dark {
Some(egui::include_image!("../../assets/pisi-logo-dark.png"))
} else {
Some(egui::include_image!("../../assets/pisi-logo-light.png"))
}
}
"pisi" => Some(egui::include_image!("../../assets/pisi-software-center.png")),
"pisi_speed" => Some(egui::include_image!("../../assets/pisi_speed.png")),
"securety" => Some(egui::include_image!("../../assets/securety.png")),
"xfce" => Some(egui::include_image!("../../assets/xfce.png")),
"xfce_settings" => Some(egui::include_image!("../../assets/xfce_settings.png")),
_ => None,
};
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.vertical_centered(|ui| {
// Simge veya Görsel
if let Some(img) = image_source {
ui.add(
egui::Image::new(img)
.max_height(64.0)
.rounding(egui::Rounding::same(8.0)),
);
} else {
ui.label(
egui::RichText::new(slide.icon)
.size(48.0),
);
}
ui.add_space(10.0);
// Başlık
ui.label(
egui::RichText::new(slide.title.clone())
.size(17.0)
.strong()
.color(crate::ui::theme::c_text()),
);
ui.add_space(8.0);
// Açıklama
ui.label(
egui::RichText::new(slide.description.clone())
.size(13.0)
.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();
}
}
+276
View File
@@ -0,0 +1,276 @@
//! PisiLinux kurulum teması.
//!
//! PisiLinux renk paleti:
//! Ana mavi : #1A6FA8 Vurgu mavi: #2196F3
//! Koyu arka: #050b0c18 Panel arka : #252535
//! Kenarlık : #3A3A5C Metin : #E0E0F0
//! Yeşil : #4CAF50 Kırmızı : #F44336
//! Sarı : #FF9800
use eframe::egui::{self, Color32, FontId, Rounding, Stroke, Vec2, Visuals};
use std::sync::atomic::{AtomicU32, Ordering};
// ─── Renk Atomics ──────────────────────────────────────────────
static BG_DARK: AtomicU32 = AtomicU32::new(0x121214);
static BG_PANEL: AtomicU32 = AtomicU32::new(0x1A1A1E);
static BG_WIDGET: AtomicU32 = AtomicU32::new(0x242428);
static BORDER: AtomicU32 = AtomicU32::new(0x323238);
static TEXT: AtomicU32 = AtomicU32::new(0xF3F3F5);
static TEXT_DIM: AtomicU32 = AtomicU32::new(0x9E9EAF);
static SUCCESS: AtomicU32 = AtomicU32::new(0x4CAF50);
static WARNING: AtomicU32 = AtomicU32::new(0xFF9800);
static ERROR: AtomicU32 = AtomicU32::new(0xF44336);
static SIDEBAR: AtomicU32 = AtomicU32::new(0x0B0B0D);
static ACCENT_COLOR: AtomicU32 = AtomicU32::new(0xd9005b); // Pisi Premium Vurgu (#d9005b)
// Helper to convert hex to Color32
fn color_from_hex(rgb: u32) -> Color32 {
Color32::from_rgb((rgb >> 16) as u8, ((rgb >> 8) & 0xFF) as u8, (rgb & 0xFF) as u8)
}
pub fn c_bg_dark() -> Color32 { color_from_hex(BG_DARK.load(Ordering::Relaxed)) }
pub fn c_bg_panel() -> Color32 { color_from_hex(BG_PANEL.load(Ordering::Relaxed)) }
pub fn c_bg_widget() -> Color32 { color_from_hex(BG_WIDGET.load(Ordering::Relaxed)) }
pub fn c_border() -> Color32 { color_from_hex(BORDER.load(Ordering::Relaxed)) }
pub fn c_text() -> Color32 { color_from_hex(TEXT.load(Ordering::Relaxed)) }
pub fn c_text_dim() -> Color32 { color_from_hex(TEXT_DIM.load(Ordering::Relaxed)) }
pub fn c_success() -> Color32 { color_from_hex(SUCCESS.load(Ordering::Relaxed)) }
pub fn c_warning() -> Color32 { color_from_hex(WARNING.load(Ordering::Relaxed)) }
pub fn c_error() -> Color32 { color_from_hex(ERROR.load(Ordering::Relaxed)) }
pub fn c_sidebar() -> Color32 { color_from_hex(SIDEBAR.load(Ordering::Relaxed)) }
pub fn set_theme_mode(is_dark: bool) {
if is_dark {
BG_DARK.store(0x121214, Ordering::Relaxed);
BG_PANEL.store(0x1A1A1E, Ordering::Relaxed);
BG_WIDGET.store(0x242428, Ordering::Relaxed);
BORDER.store(0x323238, Ordering::Relaxed);
TEXT.store(0xF3F3F5, Ordering::Relaxed);
TEXT_DIM.store(0x9E9EAF, Ordering::Relaxed);
SIDEBAR.store(0x0B0B0D, Ordering::Relaxed);
} else {
BG_DARK.store(0xF0F0F5, Ordering::Relaxed);
BG_PANEL.store(0xFFFFFF, Ordering::Relaxed);
BG_WIDGET.store(0xE8E8EE, Ordering::Relaxed);
BORDER.store(0xD0D0DA, Ordering::Relaxed);
TEXT.store(0x20202A, Ordering::Relaxed);
TEXT_DIM.store(0x60606A, Ordering::Relaxed);
SIDEBAR.store(0xE4E4EA, Ordering::Relaxed);
}
}
pub fn c_accent() -> Color32 {
let rgb = ACCENT_COLOR.load(std::sync::atomic::Ordering::Relaxed);
Color32::from_rgb((rgb >> 16) as u8, ((rgb >> 8) & 0xFF) as u8, (rgb & 0xFF) as u8)
}
pub fn c_accent_dim() -> Color32 {
let rgb = ACCENT_COLOR.load(std::sync::atomic::Ordering::Relaxed);
let r = (((rgb >> 16) & 0xFF) as f32 * 0.8) as u8;
let g = (((rgb >> 8) & 0xFF) as f32 * 0.8) as u8;
let b = ((rgb & 0xFF) as f32 * 0.8) as u8;
Color32::from_rgb(r, g, b)
}
/// egui bağlamına PisiLinux temasını uygular.
/// `cc.egui_ctx` üzerinden `setup_custom_fonts` ve `set_visuals` çağrısını birleştirir.
pub fn apply(ctx: &egui::Context, is_dark: bool) {
setup_fonts(ctx);
ctx.set_visuals(build_visuals(is_dark));
ctx.set_style(build_style());
}
fn setup_fonts(ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default();
// egui'nin yerleşik fontlarına ek olarak sistem fontunu dene.
// ISO ortamında genellikle DejaVu veya Noto bulunur.
// Bulunamazsa egui varsayılanı kullanılır.
for path in [
"/usr/share/fonts/exo-1/Exo2-Medium.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf",
] {
if let Ok(data) = std::fs::read(path) {
fonts.font_data.insert(
"system_font".to_owned(),
egui::FontData::from_owned(data),
);
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, "system_font".to_owned());
break;
}
}
ctx.set_fonts(fonts);
}
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();
v.faint_bg_color = c_bg_widget();
v.extreme_bg_color = c_sidebar();
v.code_bg_color = if is_dark { Color32::from_rgb(0x18, 0x18, 0x28) } else { Color32::from_rgb(0xEE, 0xEE, 0xF5) };
// Metin
v.override_text_color = Some(c_text());
// Kenarlıklar
v.widgets.noninteractive.bg_stroke = Stroke::new(1.0, c_border());
v.widgets.inactive.bg_stroke = Stroke::new(1.0, c_border());
v.widgets.hovered.bg_stroke = Stroke::new(1.5, c_accent());
v.widgets.active.bg_stroke = Stroke::new(2.0, c_accent());
// Widget dolguları
v.widgets.noninteractive.bg_fill = c_bg_widget();
v.widgets.inactive.bg_fill = c_bg_widget();
v.widgets.hovered.bg_fill = if is_dark { Color32::from_rgb(0x35, 0x35, 0x55) } else { Color32::from_rgb(0xD5, 0xD5, 0xE5) };
v.widgets.active.bg_fill = c_accent_dim();
// Metin renkleri
v.widgets.noninteractive.fg_stroke = Stroke::new(1.0, c_text_dim());
v.widgets.inactive.fg_stroke = Stroke::new(1.0, c_text());
v.widgets.hovered.fg_stroke = Stroke::new(1.5, c_text());
v.widgets.active.fg_stroke = Stroke::new(2.0, if is_dark { Color32::WHITE } else { Color32::BLACK });
// Yuvarlak köşeler
v.widgets.noninteractive.rounding = Rounding::same(6.0);
v.widgets.inactive.rounding = Rounding::same(6.0);
v.widgets.hovered.rounding = Rounding::same(6.0);
v.widgets.active.rounding = Rounding::same(6.0);
// Seçim rengi
v.selection.bg_fill = c_accent_dim();
v.selection.stroke = Stroke::new(1.0, c_accent());
// Hyperlink
v.hyperlink_color = c_accent();
// Pencere kenarlığı
v.window_stroke = Stroke::new(1.0, c_border());
v.window_rounding = Rounding::same(8.0);
v
}
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 = 8.0;
// Metin boyutları
style.text_styles = [
(egui::TextStyle::Small, FontId::proportional(11.0)),
(egui::TextStyle::Body, FontId::proportional(14.0)),
(egui::TextStyle::Button, FontId::proportional(14.0)),
(egui::TextStyle::Heading, FontId::proportional(20.0)),
(egui::TextStyle::Monospace, FontId::monospace(13.0)),
]
.into();
style
}
// ─── Yardımcı widget'lar ─────────────────────────────────────────
/// Vurgulu "ana eylem" butonu (mavi dolgu).
pub fn primary_button(text: &str) -> egui::Button<'_> {
egui::Button::new(egui::RichText::new(text).color(Color32::WHITE))
.fill(c_accent())
.stroke(Stroke::new(1.0, c_accent()))
.rounding(Rounding::same(6.0))
.min_size(Vec2::new(100.0, 32.0))
}
/// İkincil "iptal/geri" butonu (şeffaf dolgu).
pub fn secondary_button(text: &str) -> egui::Button<'_> {
egui::Button::new(egui::RichText::new(text).color(c_text_dim()))
.fill(Color32::TRANSPARENT)
.stroke(Stroke::new(1.0, c_border()))
.rounding(Rounding::same(6.0))
.min_size(Vec2::new(80.0, 32.0))
}
/// Kırmızı "tehlikeli eylem" butonu (disk silme onayı vb.).
pub fn danger_button(text: &str) -> egui::Button<'_> {
egui::Button::new(egui::RichText::new(text).color(Color32::WHITE))
.fill(Color32::from_rgb(0xC6, 0x28, 0x28))
.stroke(Stroke::new(1.0, c_error()))
.rounding(Rounding::same(6.0))
.min_size(Vec2::new(100.0, 32.0))
}
/// Bölüm başlığı — büyük, açık renkli, alt çizgisiz.
pub fn section_heading(ui: &mut egui::Ui, text: &str) {
ui.label(
egui::RichText::new(text)
.size(18.0)
.color(c_text())
.strong(),
);
ui.add_space(2.0);
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()),
);
ui.add_space(8.0);
}
/// Hata kutusu — kırmızı çerçeveli bildirim.
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()))
.rounding(Rounding::same(6.0))
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(c_error(), "");
ui.colored_label(c_error(), msg);
});
});
}
/// Uyarı kutusu — turuncu çerçeveli bildirim.
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()))
.rounding(Rounding::same(6.0))
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(c_warning(), "");
ui.colored_label(c_warning(), msg);
});
});
}
/// Başarı kutusu — yeşil çerçeveli bildirim.
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()))
.rounding(Rounding::same(6.0))
.inner_margin(egui::Margin::same(10.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.colored_label(c_success(), "");
ui.colored_label(c_success(), msg);
});
});
}