diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index ffb20e2..1e90632 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,15 +2,35 @@
Changelog
#########
-[XX] - YYYY-MM-DD
-=================
+[0.6.0] - 2026-07-07
+====================
Added
-----
+- **Installateur Anaconda 45 porté sur Arch** (payload LiveOS) : page « Récapitulatif avant installation » avec bouton
+ Précédent, choix du bureau intégré (GNOME, KDE, XFCE, Hyprland, i3, CLI) avec compatibilité VR affichée, splash de
+ préparation alimenté en direct par les logs (diagnostic automatique en cas de blocage), garde-fou RAM (< 2 Go),
+ mécanisme de retry réseau pour l'installation du bureau (``fws-desktop-retry``).
+- **Outillage FWS** : ``fws-hello`` (accueil 1er démarrage), ``fws-update`` + indicateur de mises à jour,
+ ``fws-keys`` (raccourcis réels), ``fws-powermenu``, ``fws-about``, ``fws-vr-setup`` (SteamVR/ALVR/Monado),
+ ``fws-proton-ge``, ``fws-setup-hardware`` (drivers GPU/VM/audio/Bluetooth), ``fws-dnd``.
+- **Récupération système** (« point de restauration ») : application GTK ``fws-recovery-gui`` + moteur ``fws-recovery``
+ (GRUB, initramfs, pacman, chroot), accessible depuis le live (entrée de boot), le menu GRUB du système installé
+ (``fws-recovery.target``) et son menu d'applications (élévation polkit).
+- **Swap zram** sur le live et le système installé (``zram-generator``).
+- **firewalld** activé par défaut sur le système installé.
+- Bureaux Hyprland et i3 à parité visuelle (waybar/polybar, wofi/rofi, Catppuccin Mocha), thème SDDM « fws »,
+ bannière GDM.
+
Changed
-------
+- **Branding FWS complet** : l'installateur s'annonce « FWS 0.6 » (variables ``ANACONDA_PRODUCT*``), splash FWS au menu
+ de boot BIOS (l'image Arch d'archiso est remplacée), ``/etc/issue`` sans version de noyau, filtres d'affichage
+ Fedora/Arch sur le splash et le journal de récupération.
+- Toutes les entrées de boot plafonnent les workers udev bloqués (``udev.event_timeout=45``) ; le menu BIOS démarre
+ explicitement l'entrée FWS (``DEFAULT fws``).
- Use xz compression for the baseline profile's bootstrap tarball so that it has more differences when compared to the
releng profile.
- Match network interfaces by their type, instead of name, in systemd-networkd configuration.
@@ -21,12 +41,21 @@ Deprecated
Fixed
-----
+- Watchdog du splash qui affichait son diagnostic PAR-DESSUS l'installateur (il se désarme désormais dès que la GUI
+ d'anaconda est à l'écran, et mesure l'inactivité en itérations — insensible aux sauts d'horloge des VM).
+- Gel de l'installateur sur machines à faible RAM (1 Go) : zram + avertissement préalable.
+- Profils ``baseline``/``releng`` synchronisés (la fusion baseline-écrase-releng avait fait perdre ``firewalld``,
+ le cap udev et les entrées de boot FWS d'ISO précédentes).
+- DNS du système installé (NetworkManager + systemd-resolved), hook ``lvm2`` dans l'initramfs, GRUB de secours
+ ``--removable`` en UEFI.
- Use ``systemd-run`` to invoke ``curl`` with a transient service that depends on ``network-online.target`` since no
services are currently pulling in ``network_online.target``.
Removed
-------
+- ``configs/baseline/syslinux/syslinux-linux.cfg`` (label ``arch`` stock, orphelin et sans cap udev).
+
[87] - 2025-10-29
=================
diff --git a/LICENSING.md b/LICENSING.md
index 1098c13..742d82d 100644
--- a/LICENSING.md
+++ b/LICENSING.md
@@ -85,8 +85,12 @@ projet (« inbound = outbound », `GPL-3.0-or-later`), avec attestation d'origin
plus « Arch Linux ») ; `motd` ne renvoie plus vers `archlinux.org`
- [ ] **Dépôt rendu publiquement accessible** (sinon l'offre écrite n'est pas
opposable) — action hors-code
-- [ ] Branding Red Hat/Fedora retiré de l'UI Anaconda ; installateur marqué
- « modifié » — à faire pendant la migration (config `product.d`)
+- [x] Branding Red Hat/Fedora retiré de l'UI Anaconda : nom de produit **FWS**
+ via les variables `ANACONDA_PRODUCTNAME`/`ANACONDA_PRODUCTVERSION`
+ (exportées par le `.xinitrc` du live, lues depuis l'os-release FWS —
+ l'en-tête affiche « FWS 0.6 INSTALLATION ») ; visuel **Arch** du menu de
+ boot BIOS remplacé par un splash FWS ; le caractère « version modifiée
+ d'Anaconda » reste documenté ici et dans TRADEMARKS.md
- [ ] Décision tranchée sur `python-iso639` (AGPL-3.0-only) : garder ou remplacer
- [ ] Revue par un conseil FOSS (recommandé avant diffusion commerciale)
@@ -96,4 +100,4 @@ projet (« inbound = outbound », `GPL-3.0-or-later`), avec attestation d'origin
> dépôt** (construits depuis l'AUR) : à corriger à la source AUR, pas ici.
> **Ceci n'est pas un avis juridique.** Base de conformité de bonne foi au
-> 2026-06-17.
+> 2026-07-07.
diff --git a/README.md b/README.md
index 17396b9..9bb1eb5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# FWS
-**FWS** est une distribution Linux live/installable basée sur **Arch Linux**, livrée sous forme d'ISO bootable BIOS/UEFI. L'installateur graphique est **en cours de migration de Calamares vers Anaconda** (Calamares retiré au commit 05ae9c3, intégration d'Anaconda en cours — voir le chantier dédié).
+**FWS** est une distribution Linux live/installable basée sur **Arch Linux**, pensée pour **remplacer Windows** : bureau prêt à l'emploi, jeux (Steam/Proton-GE), réalité virtuelle (SteamVR), compatibilité applicative (winboat/Wine) et outillage de maintenance intégré. L'installateur graphique est **Anaconda** (celui de Fedora/RHEL), **porté sur Arch** : payload LiveOS (copie du live sur le disque), spokes FWS (choix du bureau, récapitulatif avant installation), finalisation pacman/GRUB/mkinitcpio en `%post`.
Le projet est conçu pour être **construit depuis Windows via WSL** (les scripts détectent Git Bash et basculent automatiquement dans WSL), mais fonctionne aussi directement depuis une **Arch Linux native** ou depuis **toute distribution non-Arch (Fedora, Ubuntu…) via une bascule automatique dans un conteneur Arch podman**.
@@ -9,14 +9,40 @@ Le projet est conçu pour être **construit depuis Windows via WSL** (les script
## Aperçu
- **Base** : Arch Linux (rolling release)
-- **Installateur** : en migration Calamares → Anaconda (Calamares retiré, Anaconda en préparation ; payload `liveimg` pour installer FWS et non Fedora)
-- **Affichage live** : Xorg + Openbox (session graphique conservée pour l'installateur)
-- **Bureaux installables** : XFCE, KDE Plasma, GNOME, Hyprland (choix à l'installation)
-- **Boot** : BIOS (syslinux) + UEFI (systemd-boot)
+- **Installateur** : **Anaconda 45 porté sur Arch** — payload LiveOS (installe FWS, pas Fedora), page « Récapitulatif avant installation » avec bouton Précédent, choix du bureau intégré, **brandé FWS** (en-tête « FWS 0.6 INSTALLATION », via les variables `ANACONDA_PRODUCT*` exportées par le `.xinitrc`)
+- **Affichage live** : Xorg + Openbox + splash de préparation qui montre l'activité réelle d'anaconda (dernier log actif + chrono ; si rien ne bouge avant l'apparition de l'installateur, il le dit puis affiche un diagnostic complet — le watchdog se désarme dès que la GUI est à l'écran)
+- **Mémoire** : **4 Go recommandés** pour installer (2 Go minimum — swap **zram** intégré au live ET au système installé ; sous 2 Go le live avertit, à 1 Go sans zram l'installateur graphique gèle)
+- **Bureaux installables** : GNOME, KDE Plasma, XFCE, **Hyprland** (Wayland — non compatible VR), **i3** (X11 — compatible VR/SteamVR), ou ligne de commande seule
+- **Boot** : UEFI (systemd-boot) + BIOS (syslinux, splash **FWS**, `DEFAULT fws`) — entrées *normale*, *safe graphics (nomodeset)*, *Réparer FWS (recovery)* et *speech*
+- **Récupération** : accessible depuis le live (entrée de boot), depuis le **menu GRUB du système installé** (« Réparer FWS ») et depuis son **menu d'applications** (« Récupération FWS », élévation polkit)
- **Compression** : squashfs (xz)
---
+## Ce qu'il y a dans FWS
+
+### À l'installation
+- **Choix du bureau** dans Anaconda, avec compatibilité VR affichée (i3 = SteamVR ✔ ; Hyprland = ✘)
+- **Page récapitulative** avant toute écriture disque : config choisie (langue, clavier, fuseau, comptes, partitionnement, bureau, nom de machine) + matériel détecté (GPU, disques, virtualisation, réseau)
+- **Automatique en fin d'install** : drivers GPU (NVIDIA/AMD/Intel), outils invité VM (VMware/VirtualBox/QEMU/Hyper-V), PipeWire + pavucontrol, Bluetooth (BlueZ + blueman), luminosité (brightnessctl), **pare-feu firewalld activé**, Steam + Wine + Monado + MangoHud + gamemode, clavier/locale, hostname, sudo pour l'utilisateur créé, branding FWS
+
+### Sur le système installé (outillage `fws-*`)
+| Commande | Rôle | Accès |
+|---|---|---|
+| `fws-hello` | Bienvenue au 1er démarrage (mise à jour, codecs, Proton-GE, VR, raccourcis) | auto au 1er login |
+| `fws-update` | Mise à jour complète (pacman + cache + orphelins + flatpak) | bouton de la barre |
+| `fws-keys` | Panneau des **raccourcis clavier réels** (lit la config Hyprland/i3) | `Super+K`, bouton ⌨ |
+| `fws-powermenu` | Verrouiller / déconnexion / suspendre / redémarrer / éteindre | `Super+X`, bouton |
+| `fws-about` | « À propos » : le projet FWS + infos système | menu d'applications |
+| `fws-vr-setup` | Drivers/runtimes VR (Vive/Index → SteamVR natif ; Quest → ALVR ; Monado) | `Super+Shift+V` (i3), menu |
+| `fws-proton-ge` | Installe/actualise Proton-GE pour Steam (vérif sha512) | fws-hello, terminal |
+| `fws-recovery` | **Application GTK de réparation** : GRUB, initramfs, pacman, terminal chroot (moteur bash + repli console) | menu « Récupération FWS » de l'OS installé, entrée GRUB « Réparer FWS », entrée de boot du live |
+
+### Hyprland & i3 : parité visuelle
+Même identité Catppuccin Mocha (nuances de bleu) sur les deux : barre flottante arrondie (**waybar** / **polybar**) avec date+heure, indicateur de mises à jour, volume/réseau/CPU/RAM/luminosité/Bluetooth cliquables ; menus assortis (**wofi** / **rofi**) ; mêmes raccourcis (déclarés avec descriptions → `Super+K`) ; **Dolphin** en gestionnaire de fichiers ; thème **SDDM « fws »** maison (pur QtQuick, accent adapté au bureau ; GNOME garde GDM avec bannière FWS).
+
+---
+
## Environnement requis — INSTALLATION ULTRA PRÉCISE
### Côté Windows (machine hôte)
@@ -146,9 +172,9 @@ Détails utiles :
Depuis **Git Bash sur Windows** (à la racine du repo) :
```bash
-# Étape 1 : compilation des paquets AUR (winboat) + deps locales
+# Étape 1 : compilation des paquets AUR (winboat) + PKGBUILD locaux
+# (anaconda porté sur Arch + ses dépendances python)
# → crée local-repo/ avec les .pkg.tar.zst
-# (Calamares retiré ; l'installateur Anaconda est un chantier en cours)
./setup-aur.sh
# Étape 2 : build de l'ISO via mkarchiso
@@ -170,11 +196,11 @@ L'ISO finale est générée dans `./out/FWS-YYYY.MM.DD-x86_64.iso`.
4. **`pacman -Syu`** complet (obligatoire — sinon `makepkg` plante sur les "conflicting files" lors des splits de paquets type `gcc-libs`).
5. **Install des outils de build** : `base-devel git`.
6. **Création d'un utilisateur `fwsbuild`** non-root avec `sudo` NOPASSWD (makepkg refuse de tourner en root).
-7. **Clone + build AUR** : `winboat-bin` via `makepkg -s --noconfirm`, plus les PKGBUILD locaux de `pkgbuilds/` (deps Anaconda). Calamares n'est plus compilé.
+7. **Clone + build AUR** : `winboat-bin` via `makepkg -s --noconfirm`, plus les PKGBUILD locaux de `pkgbuilds/` (anaconda, anaconda-widgets, pykickstart, blivet, python-meh patché…). Le build est **incrémental** : un paquet déjà présent dans `local-repo/` à la bonne version est sauté.
8. **Création du repo local** : `repo-add local-repo/fws-local.db.tar.gz`.
9. **Cleanup** : suppression de l'utilisateur `fwsbuild`.
-**Durée** : quelques minutes (winboat-bin + deps locales ; Calamares et ses ~12 dépendances Qt6 ne sont plus compilés).
+**Durée** : quelques minutes en incrémental ; le premier build complet (anaconda + deps) est plus long.
---
@@ -220,11 +246,12 @@ L'ISO finale est générée dans `./out/FWS-YYYY.MM.DD-x86_64.iso`.
## Séquence de boot de l'ISO
-1. Démarrage de l'ISO FWS (BIOS ou UEFI)
+1. Menu de boot : **installation** (défaut), **safe graphics** (`nomodeset`), **Réparer FWS** (`fws.recovery=1` → app GTK `fws-recovery-gui` via openbox, repli console `fws-recovery`), **speech**. Toutes les entrées plafonnent les workers udev bloqués (`udev.event_timeout=45`)
2. `systemd` → autologin root sur `tty1`
-3. `.zprofile` → `startx`
-4. `.xinitrc` → Openbox (session graphique du live)
-5. **[Chantier C]** lancement de l'installateur Anaconda — pas encore câblé (Calamares retiré au commit 05ae9c3)
+3. `.zprofile` → mode recovery si demandé, sinon `startx`
+4. `.xinitrc` → Openbox + **garde-fou RAM** (avertissement sous 2 Go) + **splash « Préparation de l'installateur »** (label alimenté en direct par les logs anaconda ; à 4 min sans activité → diagnostic auto `/tmp/fws-stall.txt` ; le tout se désarme dès que la GUI d'anaconda apparaît), bind mount `/run/rootfsbase` (source du payload LiveOS), patch du hub Anaconda, **exports `ANACONDA_PRODUCT*`** (branding FWS), puis `dbus-run-session liveinst`
+5. **Anaconda** : accueil → hub (langue, clavier, disque, utilisateur, **bureau FWS**) → « Commencer l'installation » → **page Récapitulatif** (bouton Précédent / Lancer l'installation) → installation → écran de réussite → extinction
+6. En cas d'échec : écran de debug avec tous les logs (liveinst, %post chrooté, anaconda)
---
@@ -233,8 +260,18 @@ L'ISO finale est générée dans `./out/FWS-YYYY.MM.DD-x86_64.iso`.
- **Paquets du live** : `configs/releng/packages.x86_64`
- **Paquets baseline** : `configs/baseline/packages.x86_64`
- **Overlay rootfs** : `configs/releng/airootfs/` ou `configs/baseline/airootfs/`
-- **Kickstart de l'installateur** : `configs/releng/airootfs/usr/share/anaconda/interactive-defaults.ks` (payload liveimg + %post)
-- **Branding ISO** : `configs/releng/profiledef.sh` (`iso_name`, `iso_label`, `iso_publisher`)
+- **Kickstart de l'installateur** : `configs/releng/airootfs/usr/share/anaconda/interactive-defaults.ks` (payload LiveOS via `--liveinst` + tout le `%post` : bureaux, drivers, pare-feu, branding)
+- **Spokes Anaconda FWS** : `configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/` (`fws_desktop.py`, `fws_recap.py`) + glades dans `usr/share/anaconda/ui/spokes/`
+- **Configs bureaux & thèmes** : `configs/releng/airootfs/usr/local/share/fws/` (hyprland, i3, waybar, polybar, wofi, rofi, picom) et `usr/share/sddm/themes/fws/`
+- **Outils fws-\*** : `configs/releng/airootfs/usr/local/bin/`
+- **Récupération sur l'OS installé** : entrée GRUB générée par `configs/releng/airootfs/etc/grub.d/42_fws-recovery` → `fws-recovery.target` + `fws-recovery-boot.service` (`etc/systemd/system/`) ; lanceur `usr/local/share/applications/fws-recovery.desktop` + politique polkit `usr/share/polkit-1/actions/org.fws.recovery.policy`
+- **Swap zram** (live + installé) : `configs/releng/airootfs/etc/systemd/zram-generator.conf` (paquet `zram-generator`)
+- **Branding ISO** : `configs/releng/profiledef.sh` (`iso_name`, `iso_label`, `iso_publisher`) ; **visuel du menu BIOS** : `configs/releng/syslinux/splash.png` (PNG 8-bit) ; **nom/version affichés par l'installateur** : exports `ANACONDA_PRODUCTNAME`/`ANACONDA_PRODUCTVERSION` dans le `.xinitrc` (lus depuis l'os-release du live)
+
+> ⚠ **Règle d'or de la fusion** : au build, `configs/baseline/*` est copié **par-dessus** `configs/releng/*` — tout fichier homonyme de baseline **écrase** celui de releng. Ça concerne notamment `profiledef.sh`, `packages.x86_64`, `pacman.conf`, `efiboot/loader/entries/01-archiso-linux.conf` et `syslinux/syslinux.cfg` : **toute modification d'un de ces fichiers doit être reportée dans les deux profils** (des divergences ont déjà fait perdre silencieusement `firewalld` et le cap `udev.event_timeout` d'une ISO ; un `syslinux-linux.cfg` stock orphelin côté baseline définissait aussi un label `arch` sans le cap — supprimé, et le menu BIOS pointe désormais explicitement sur `DEFAULT fws`). Vérifier avec :
+> ```bash
+> comm -12 <(cd configs/baseline && find . -type f | sort) <(cd configs/releng && find . -type f | sort)
+> ```
---
@@ -247,7 +284,20 @@ L'ISO finale est générée dans `./out/FWS-YYYY.MM.DD-x86_64.iso`.
| `pacman: signature unknown trust` | Keyring périmé → `pacman -Sy --noconfirm archlinux-keyring` puis relancer. |
| `makepkg: conflicting files` | `pacman -Syu` incomplet avant le build. Relancer `setup-aur.sh`. |
| `mkarchiso` plante sur `[fws-local]` | Le repo local n'a pas été généré. Relancer `setup-aur.sh`. |
-| Écran noir après login Hyprland | Tu testes dans VirtualBox → pas d'accélération GPU. Utiliser QEMU/KVM avec `virtio-gpu` ou bare-metal. |
+| Écran bleu 1-2 min au boot du live | Normal en VM : un worker udev traîne (plafonné à 45 s) — le splash « Préparation de l'installateur » l'indique. Attendre. |
+| L'installateur semble figé / fenêtre blanche | Rendu logiciel en VM : la fenêtre Anaconda se peint lentement. Patienter quelques secondes. |
+| Session Hyprland/i3 : fenêtres au contenu noir en VM | Compositing sur GPU virtuel — picom est auto-désactivé en VM (config FWS) ; si tu l'as réactivé, `pkill picom`. |
+| Hyprland crash au lancement en VM | Géré : le wrapper `fws-hyprland` force le rendu logiciel (`kms_swrast`/llvmpipe) sous hyperviseur. Sur vrai GPU, rendu matériel normal. |
+| Steam plante à l'install de SteamVR | En VM : `steam -cef-disable-gpu -cef-disable-gpu-compositing` (et la VR ne peut PAS marcher en VM — GPU/USB inaccessibles). |
+| Icône Bluetooth absente / blueman en erreur | Pas d'adaptateur Bluetooth (cas VM) : le module de barre se cache et l'applet ne se lance pas — comportement voulu. |
+| Le système installé ne boote plus | Menu GRUB → « **Réparer FWS (récupération du système)** » (boote l'initramfs fallback + l'app de réparation). Si GRUB lui-même est mort : booter l'ISO → entrée « **Réparer FWS (recovery)** ». |
+| Réparer depuis le système installé (il boote encore) | Menu d'applications → « **Récupération FWS** » (mot de passe admin demandé une fois — polkit). |
+| Réinstallation par-dessus une install existante | Le kickstart nettoie tout (`zerombr` + `clearpart --all`), mais en cas de comportement étrange au 2ᵉ passage, effacer/recréer le disque virtuel. |
+| Boot BIOS | Fonctionne (résolu) : syslinux est sous `/boot/syslinux` de l'ISO (pas `/syslinux`), El-Torito BIOS + UEFI présents ; install BIOS complète validée en VM. Le menu BIOS démarre l'entrée FWS par défaut (`DEFAULT fws`). |
+| Splash « Préparation de l'installateur » figé | **RAM insuffisante** (reproduit : gel à 1 Go, OK à 2 Go, aucun swap sur le live avant zram). En VM : allouer **4 Go** (le wizard VMware « Other 64-bit » n'en met que 1 !). Le live avertit désormais sous 2 Go, embarque un swap zram, et le splash affiche l'activité réelle d'anaconda (+ diagnostic auto si les logs ne bougent plus). |
+| Dialogue « préparation bloquée : diagnostic » PAR-DESSUS l'installateur | Bug corrigé (ISO ≤ 2026-07-07 01:04) : le watchdog du splash se déclenchait alors qu'anaconda était à l'écran (logs silencieux = normal quand il attend l'utilisateur). Il se désarme désormais dès l'apparition de la GUI ; ferme le dialogue et continue, ou rebuild. |
+| « Possibly missing firmware for module … » pendant une réparation Recovery | Avertissements `mkinitcpio` **normaux** : firmware de matériel exotique (SCSI/fibre channel…) absent de `linux-firmware`. Sans effet, sauf si tu possèdes ce matériel précis. Le journal Recovery l'explique désormais. |
+| Recovery : « tout est grisé » | Corrigé : seul le sélecteur était désactivé (une seule installation détectée) mais toute l'app semblait morte ; il reste actif désormais. Les boutons d'action, eux, ont toujours été fonctionnels. |
| Retours à la ligne CRLF | `build.sh` lance `dos2unix` automatiquement, mais si tu modifies pendant le build, relancer. |
### Logs utiles
@@ -256,11 +306,16 @@ L'ISO finale est générée dans `./out/FWS-YYYY.MM.DD-x86_64.iso`.
# Pendant le build
tail -f /tmp/fws-build/work/iso/arch/pkglist.x86_64.txt
-# Dans l'ISO (autologin tty1)
-journalctl -b
+# Dans le LIVE (Ctrl+Alt+F2, login root) — pendant/après l'installation
+journalctl -b # système
+cat /tmp/fws-liveinst.log # sortie complète de liveinst/anaconda
+cat /tmp/anaconda.log # log principal anaconda
+cat /tmp/fws-post-chroot.log # %post chrooté = grub-install/mkinitcpio
+# En cas d'échec, l'écran de debug du live affiche déjà tout ça (zenity).
-# Hyprland post-install
-cat ~/.local/share/hyprland/hyprland.log
+# Sur le système installé
+cat /var/log/fws-post.log # trace complète du %post d'installation
+journalctl -b # premier démarrage
```
---
diff --git a/TRADEMARKS.md b/TRADEMARKS.md
index c8abf75..babcc53 100644
--- a/TRADEMARKS.md
+++ b/TRADEMARKS.md
@@ -47,12 +47,24 @@ Le système live héritait de l'`os-release` du paquet `filesystem` d'Arch et
donc sans conflit, et est retiré du système final par
`zzzz99-remove-custom-hooks-from-airootfs.hook`.
- **`motd`** : ne référence plus `archlinux.org` ; pointe vers le dépôt FWS.
+- **Menu de boot BIOS (2026-07-07)** : l'image de fond `syslinux/splash.png`
+ héritée d'archiso affichait le **logo Arch Linux plein écran** — remplacée
+ par un visuel FWS original (générique, sans logo tiers).
+- **Bannière de login (`/etc/issue`)** : `\S (\l)` — affiche le `PRETTY_NAME`
+ FWS, sans la version du noyau (dont le suffixe `-archN`).
+- **Textes utilisateur** : `fws-about` dit « distribution Linux » ; le splash
+ de préparation et le journal de l'app Récupération remplacent à l'affichage
+ les mentions « Arch Linux »/« Fedora » issues des logs d'outils tiers (les
+ identifiants techniques réels — `archlinux-keyring`, miroirs — restent
+ intacts : usage nominatif fonctionnel).
> Note technique : `ID=fws` + `ID_LIKE=arch` préserve la détection « famille
> Arch » des outils tiers (et satisfait les checks `^ID(_LIKE)?=.*arch` de
> `build.sh`/`setup-aur.sh`, qui portent de toute façon sur l'hôte de build).
> Si la migration Anaconda révèle un composant testant `ID=arch` en dur, basculer
> transitoirement le hook sur `ID=arch` tout en gardant `NAME`/`PRETTY_NAME` FWS.
+> Le suffixe `-archN` de `uname -r` vient du binaire du noyau Arch : usage
+> nominatif inévitable sans recompiler le kernel.
---
@@ -60,7 +72,7 @@ Le système live héritait de l'`os-release` du paquet `filesystem` d'Arch et
> **Red Hat®** et **Fedora®** sont des marques déposées de **Red Hat, Inc.**
> L'installateur **Anaconda** est développé par Red Hat. FWS distribue une
-> **version MODIFIÉE** d'Anaconda (portée sur Arch, payload `liveimg`), **non
+> **version MODIFIÉE** d'Anaconda (portée sur Arch, payload LiveOS), **non
> officielle**, **non fournie ni approuvée** par Red Hat ou le projet Fedora.
> *« This product includes a modified version of Anaconda. Red Hat and Fedora
> are trademarks of Red Hat, Inc. This is an unofficial build, not provided or
@@ -74,9 +86,19 @@ Engagements FWS :
passer une version modifiée pour officielle) ;
- **recommandation béton :** retirer tout **logo / nom de produit Red Hat ou
Fedora** de l'interface de l'installateur si présent, et y afficher
- « Installateur FWS (basé sur Anaconda) ». La configuration produit d'Anaconda
- se règle via `product.d` / les données de branding — à auditer pendant la
- migration.
+ « Installateur FWS (basé sur Anaconda) ».
+
+### ✅ Remédiation appliquée (2026-07-07 — l'installateur s'annonce « FWS »)
+
+Sans `/.buildstamp`, Anaconda s'annonçait « ANACONDA BLUESKY INSTALLATION » et
+ses logs parlent de Fedora. `pyanaconda/core/product.py` donne priorité aux
+variables d'environnement : le `.xinitrc` du live exporte
+`ANACONDA_PRODUCTNAME`/`ANACONDA_PRODUCTVERSION` (depuis l'os-release FWS) et
+`ANACONDA_ISFINAL=true` → l'en-tête et l'accueil affichent **« FWS 0.6 »**. Le
+splash de préparation filtre en outre les mentions Fedora des lignes de log
+affichées. Les mentions profondes dans les **logs techniques** (fichiers
+`/tmp/*.log`, écrans de debug) sont conservées volontairement : identification
+loyale du composant amont (version modifiée d'Anaconda), utile au diagnostic.
---
diff --git a/VERSION b/VERSION
index 7d85683..a918a2a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.5.4
+0.6.0
diff --git a/configs/baseline/airootfs/root/.xinitrc b/configs/baseline/airootfs/root/.xinitrc
index b7b050e..414a96b 100644
--- a/configs/baseline/airootfs/root/.xinitrc
+++ b/configs/baseline/airootfs/root/.xinitrc
@@ -20,10 +20,146 @@ _OB_PID=$!
# boucle (symptôme « fond bleu qui clignote/reboote »). On NE rend jamais la main
# à startx : on reste sur Openbox pour lire les logs / rebooter proprement.
if command -v liveinst >/dev/null 2>&1; then
+ # Payload LiveOS : avec --liveinst (passé par liveinst), Anaconda IGNORE le
+ # payload du kickstart (anaconda.py teste opts.liveinst AVANT le type du
+ # payload DBus — un « liveimg » dans le ks serait du code mort) et détecte
+ # l'image live sur des chemins Fedora/dracut : /dev/mapper/live-base,
+ # /dev/mapper/live-osimg-min ou /run/rootfsbase — tous ABSENTS d'archiso →
+ # « SourceSetupError: No Live OS image found! » = crash au démarrage
+ # (reproduit hors VM dans un chroot de l'airootfs). On expose donc le
+ # squashfs archiso (monté ro sur /run/archiso/airootfs) là où Anaconda le
+ # cherche : findmnt y résout le loop device → montage → rsync vers la cible.
+ if [ -d /run/archiso/airootfs ] && [ ! -e /run/rootfsbase ]; then
+ mkdir -p /run/rootfsbase
+ mount --bind /run/archiso/airootfs /run/rootfsbase || rmdir /run/rootfsbase
+ fi
+
+ # GARDE-FOU RAM : le live n'a que le zram comme marge — en dessous de
+ # ~2 Go, Anaconda (GUI Python + squashfs) peut geler la machine en plein
+ # « Préparation de l'installateur ». On prévient AVANT plutôt que de
+ # laisser l'utilisateur croire à un bug.
+ _MEMKB=$(awk '/^MemTotal:/{print $2}' /proc/meminfo 2>/dev/null)
+ if [ -n "$_MEMKB" ] && [ "$_MEMKB" -lt 2000000 ] && command -v zenity >/dev/null 2>&1; then
+ zenity --warning --width=460 --title="FWS — mémoire insuffisante ?" \
+ --text="Cette machine n'a que $(( _MEMKB / 1024 )) Mo de RAM.\n\nL'installateur graphique peut geler en dessous de 2 Go.\nEn VM : augmente la mémoire (4 Go recommandés) puis redémarre.\n\nOK pour tenter quand même." 2>/dev/null
+ fi
+
+ # ÉCRAN D'ATTENTE immédiat : le démarrage d'anaconda peut prendre 2 à
+ # 4 minutes (observé en VM VMware : un worker udev se bloque au boot et
+ # n'est tué par udevd qu'après 45 s — udev.event_timeout). Sans feedback,
+ # l'utilisateur croit à une panne alors que TOUT va bien. Le splash montre
+ # EN DIRECT ce que fait anaconda : un nourrisseur écrit sur stdin de
+ # zenity --progress (« # texte » = mise à jour du libellé) le log anaconda
+ # le plus récemment modifié + son dernier message + un chrono. Si les logs
+ # ne bougent plus (vrai blocage), le libellé le DIT (au lieu d'une barre
+ # qui pulse pour rien) et, au-delà de 4 min d'inactivité, on collecte
+ # journal+dmesg dans /tmp/fws-stall.txt et on l'affiche : l'écran devient
+ # lui-même le rapport de bug. Le nourrisseur meurt par SIGPIPE quand le
+ # splash est tué à la sortie de liveinst.
+ _SPLASH_PID=""
+ if command -v zenity >/dev/null 2>&1; then
+ (
+ _el=0; _quiet=0; _prevstamp=""
+ while :; do
+ sleep 3
+ # GUI d'anaconda à l'écran (marqueur ui.gui) → le splash n'a
+ # plus de raison d'être et le watchdog DOIT se désarmer : des
+ # logs silencieux pendant que l'utilisateur remplit les écrans
+ # sont NORMAUX (bug vécu : le rapport « préparation bloquée »
+ # s'affichait PAR-DESSUS l'installateur au bout de 4 min).
+ if grep -q 'ui\.gui' /tmp/anaconda.log 2>/dev/null; then
+ pkill -f 'zenity --progress' 2>/dev/null
+ exit 0
+ fi
+ # Compteurs en itérations (pas d'heure murale : l'horloge du
+ # guest SAUTE en VM — resynchro RTC — et gonflerait les âges,
+ # vu en vrai : « 242 s d'inactivité » 90 s après le boot).
+ _el=$(( _el + 3 ))
+ _last=$(ls -t /tmp/storage.log /tmp/anaconda.log /tmp/program.log /tmp/dbus.log 2>/dev/null | head -n1)
+ if [ -z "$_last" ]; then
+ printf '# Démarrage d'"'"'Anaconda… (%d min %02d s)\n' $(( _el / 60 )) $(( _el % 60 )) || exit 0
+ continue
+ fi
+ _stamp=$(stat -c %Y "$_last" 2>/dev/null)
+ if [ "$_stamp" = "$_prevstamp" ]; then
+ _quiet=$(( _quiet + 3 ))
+ else
+ _quiet=0; _prevstamp="$_stamp"
+ fi
+ # Rebranding d'affichage : anaconda (installateur Fedora) parle
+ # de Fedora/Arch dans ses logs — on ne montre pas ça à l'écran.
+ _msg=$(tail -n1 "$_last" 2>/dev/null | tr -d '\r' \
+ | sed -E 's/Fedora( Linux)?/FWS/g; s/Arch Linux/FWS/g; s/\/FWS/g' \
+ | cut -c1-90)
+ if [ "$_quiet" -ge 240 ] && [ ! -f /tmp/fws-stall.txt ]; then
+ {
+ echo "=== FWS : préparation bloquée (aucune activité log depuis ${_quiet}s) ==="
+ echo "--- dernier log actif : $_last"; tail -n 25 "$_last" 2>/dev/null
+ echo "--- udev (workers bloqués ?)"; udevadm settle --timeout=0 2>&1
+ echo "--- journal (fin)"; journalctl -b --no-pager 2>/dev/null | tail -n 60
+ echo "--- dmesg (fin)"; dmesg 2>/dev/null | tail -n 40
+ } > /tmp/fws-stall.txt 2>&1
+ zenity --text-info --title="FWS — préparation bloquée : diagnostic" \
+ --filename=/tmp/fws-stall.txt --width=1000 --height=650 2>/dev/null &
+ fi
+ if [ "$_quiet" -ge 60 ]; then
+ printf '# ⚠ Aucune activité depuis %d s (%s) — dernier message : %s\n' "$_quiet" "$(basename "$_last")" "$_msg" || exit 0
+ else
+ printf '# Préparation (%d min %02d s) — %s : %s\n' $(( _el / 60 )) $(( _el % 60 )) "$(basename "$_last")" "$_msg" || exit 0
+ fi
+ done
+ ) | zenity --progress --pulsate --no-cancel \
+ --title="FWS" \
+ --text="Préparation de l'installateur FWS…\n\nAnalyse du matériel et des disques en cours.\nEncore une à deux minutes, c'est normal." \
+ --width=560 2>/dev/null &
+ _SPLASH_PID=$!
+ fi
+
+ # Hub Résumé SANS le libellé « We won't touch your disks… » : notre glade
+ # modifié NE PEUT PAS être livré directement à son chemin final —
+ # mkarchiso pose l'overlay airootfs AVANT pacstrap, et pacman refuse
+ # alors d'installer anaconda par-dessus (« exists in filesystem »). On le
+ # livre donc sous /usr/local/share/fws et on ÉCRASE ici, au boot du live
+ # (l'overlay du live est inscriptible), le fichier du paquet.
+ if [ -f /usr/local/share/fws/anaconda-summary-hub.glade ]; then
+ cp -f /usr/local/share/fws/anaconda-summary-hub.glade \
+ /usr/share/anaconda/ui/hubs/summary.glade 2>/dev/null || true
+ fi
+
+ # BRANDING : sans /.buildstamp, anaconda s'appelle « anaconda bluesky »
+ # (défauts de pyanaconda/core/product.py) et ses logs parlent de Fedora.
+ # Les variables d'environnement ANACONDA_* ont priorité → l'en-tête
+ # devient « FWS 1.0 INSTALLATION » et le log « anaconda … for FWS started ».
+ # (Les mentions Fedora PROFONDES dans les logs — GeoIP, profils — restent :
+ # ce sont des chaînes internes de l'installateur, sans effet sur l'install.)
+ export ANACONDA_PRODUCTNAME="$(. /etc/os-release 2>/dev/null; echo "${NAME:-FWS}")"
+ export ANACONDA_PRODUCTVERSION="$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID:-0.6}")"
+ export ANACONDA_ISFINAL=true
+
LIVEINST_LOG=/tmp/fws-liveinst.log
- liveinst >"$LIVEINST_LOG" 2>&1
+ # Bus D-Bus de SESSION : au démarrage (--liveinst), inhibit_screensaver()
+ # de pyanaconda se connecte au bus de session. S'il n'y en a AUCUN (notre
+ # startx root minimal n'en garantit pas), Gio lève une GLib.GError que
+ # pyanaconda n'attrape PAS (il n'attrape que DBusError) → crash immédiat
+ # (2e crash reproduit hors VM). dbus-run-session fournit un bus de session
+ # pour toute la durée de l'installateur ; avec un bus présent, l'absence du
+ # service ScreenSaver est attrapée (simple warning dans anaconda.log).
+ if command -v dbus-run-session >/dev/null 2>&1 && [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
+ dbus-run-session -- liveinst >"$LIVEINST_LOG" 2>&1
+ else
+ liveinst >"$LIVEINST_LOG" 2>&1
+ fi
rc=$?
+ # liveinst terminé (install finie ou échec) : le splash n'a plus de raison
+ # d'être — kill de zenity ; le nourrisseur meurt de SIGPIPE à son
+ # prochain printf. Idem pour un éventuel dialogue de diagnostic de stall.
+ if [ -n "$_SPLASH_PID" ]; then
+ pkill -P "$_SPLASH_PID" 2>/dev/null
+ kill "$_SPLASH_PID" 2>/dev/null
+ fi
+ pkill -f "fws-stall.txt" 2>/dev/null
+
# ATTENTION : liveinst NE propage PAS le code de sortie d'anaconda — il
# enchaîne son propre nettoyage et sort souvent 0 même quand anaconda a
# planté. On ne se fie donc PAS à $rc : en phase debug on regroupe TOUS les
@@ -66,6 +202,9 @@ if command -v liveinst >/dev/null 2>&1; then
echo
echo "########## %post --nochroot (/tmp/fws-post-nochroot.log) ##########"
cat /tmp/fws-post-nochroot.log 2>/dev/null || echo "(absent)"
+ echo
+ echo "########## Récapitulatif (page GUI fws_recap, avant install) ##########"
+ cat /tmp/fws-hw-summary.txt 2>/dev/null || echo "(absent — page récap pas atteinte)"
for f in /tmp/anaconda.log /tmp/syslog /tmp/program.log \
/tmp/storage.log /tmp/dbus.log /tmp/packaging.log /tmp/X.log; do
echo
diff --git a/configs/baseline/airootfs/root/.zprofile b/configs/baseline/airootfs/root/.zprofile
index 11af8b8..ce7430f 100644
--- a/configs/baseline/airootfs/root/.zprofile
+++ b/configs/baseline/airootfs/root/.zprofile
@@ -1,5 +1,18 @@
-# Démarrage automatique de Xorg uniquement dans le live FWS.
+# Démarrage automatique dans le live FWS uniquement.
# Sur le système installé, /run/archiso n'existe pas, donc rien ne se lance.
+
+# Mode RÉPARATION (entrée de boot « Réparer FWS », paramètre fws.recovery=1) :
+# on démarre X + l'application GTK (fws-recovery-session lance openbox puis
+# fws-recovery-gui, qui pilote le moteur fws-recovery). Si X échoue à
+# démarrer, startx rend la main → repli sur l'assistant console fws-recovery.
+# (Sur le SYSTÈME INSTALLÉ, l'équivalent est fws-recovery-boot.service via
+# l'entrée GRUB « Réparer FWS » — pas ce fichier.)
+if [[ -d /run/archiso && "$(tty)" == "/dev/tty1" ]] \
+ && grep -qw 'fws.recovery=1' /proc/cmdline 2>/dev/null; then
+ startx /usr/local/bin/fws-recovery-session -- vt1
+ exec fws-recovery
+fi
+
if [[ -d /run/archiso && "$(tty)" == "/dev/tty1" && -z "$DISPLAY" ]]; then
exec startx -- vt1
fi
diff --git a/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_desktop.py b/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_desktop.py
new file mode 100644
index 0000000..607e3cc
--- /dev/null
+++ b/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_desktop.py
@@ -0,0 +1,106 @@
+# FWS — Spoke « Desktop Environment » : nouvel onglet de choix du bureau.
+#
+# POURQUOI ICI (airootfs, pas dans le paquet anaconda) : Anaconda découvre les
+# spokes au RUNTIME via collect()/collect_spokes (os.listdir du dossier des
+# spokes puis import de chaque .py). Déposer ce fichier dans le dossier des
+# spokes via l'overlay airootfs suffit donc à le faire apparaître — SANS rebuild
+# du paquet anaconda (juste ./build.sh). Le glade associé est dans
+# /usr/share/anaconda/ui/spokes/fws_desktop.glade (aussi via l'airootfs).
+#
+# ⚠ Le chemin /usr/lib/python3.14/... est couplé à la version de Python d'Arch
+# (3.14 au moment du build d'anaconda). À mettre à jour si Python change de
+# version majeure (en même temps que le rebuild d'anaconda de toute façon).
+#
+# FONCTION : afficher 5 choix (CLI/GNOME/KDE/Hyprland/XFCE) et écrire la clé
+# choisie dans /tmp/fws-desktop. Le %post du kickstart lit ce fichier, fait
+# « pacman -S » du bureau + d'un navigateur (Firefox), et active le display
+# manager. « cli » (défaut) = aucun bureau, rien à installer.
+
+from pyanaconda.core.i18n import CN_, _
+from pyanaconda.ui.categories.software import SoftwareCategory
+from pyanaconda.ui.gui.spokes import NormalSpoke
+
+__all__ = ["FWSDesktopSpoke"]
+
+# clé interne (écrite dans /tmp/fws-desktop) → libellé affiché dans le statut.
+_CHOICES = [
+ ("cli", _("Command line only (no desktop)")),
+ ("gnome", "GNOME"),
+ ("kde", "KDE Plasma"),
+ ("hyprland", "Hyprland — Wayland (non compatible VR)"),
+ ("i3", "i3 — X11 (compatible VR : SteamVR)"),
+ ("xfce", "XFCE"),
+]
+_DESKTOP_FILE = "/tmp/fws-desktop"
+
+
+class FWSDesktopSpoke(NormalSpoke):
+ """Choix de l'environnement de bureau FWS (écrit /tmp/fws-desktop)."""
+
+ builderObjects = ["fwsDesktopWindow"]
+ mainWidgetName = "fwsDesktopWindow"
+ uiFile = "spokes/fws_desktop.glade"
+ category = SoftwareCategory
+ icon = "preferences-desktop-display-symbolic"
+ title = CN_("GUI|Spoke", "_Desktop Environment")
+
+ @staticmethod
+ def get_screen_id():
+ """Identifiant unique de cet écran."""
+ return "fws-desktop-selection"
+
+ def __init__(self, *args):
+ NormalSpoke.__init__(self, *args)
+ self._choice = "cli"
+ self._radios = {}
+
+ def initialize(self):
+ NormalSpoke.initialize(self)
+ for key, _label in _CHOICES:
+ self._radios[key] = self.builder.get_object("radio_" + key)
+ # Reprend un éventuel choix déjà écrit (revisite du spoke).
+ try:
+ with open(_DESKTOP_FILE) as fh:
+ val = fh.read().strip()
+ if val in self._radios:
+ self._choice = val
+ except OSError:
+ pass
+
+ def refresh(self):
+ # Coche le bouton correspondant au choix courant.
+ rb = self._radios.get(self._choice) or self._radios.get("cli")
+ if rb is not None:
+ rb.set_active(True)
+
+ def apply(self):
+ # Relit le bouton actif et persiste le choix pour le %post du kickstart.
+ for key, rb in self._radios.items():
+ if rb is not None and rb.get_active():
+ self._choice = key
+ break
+ try:
+ with open(_DESKTOP_FILE, "w") as fh:
+ fh.write(self._choice)
+ except OSError:
+ pass
+
+ @property
+ def status(self):
+ for key, label in _CHOICES:
+ if key == self._choice:
+ return label
+ return _("Command line only (no desktop)")
+
+ @property
+ def completed(self):
+ # « cli » est un défaut valide → le spoke n'est jamais bloquant.
+ return True
+
+ @property
+ def mandatory(self):
+ return False
+
+ @property
+ def ready(self):
+ return True
diff --git a/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_recap.py b/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_recap.py
new file mode 100644
index 0000000..b2ceb8a
--- /dev/null
+++ b/configs/baseline/airootfs/usr/lib/python3.14/site-packages/pyanaconda/ui/gui/spokes/fws_recap.py
@@ -0,0 +1,348 @@
+# FWS — Page « Récapitulatif » : écran À PART ENTIÈRE avant l'installation.
+#
+# POURQUOI ICI (airootfs, pas dans le paquet anaconda) : même mécanisme que la
+# spoke « Desktop Environment » (fws_desktop.py) — Anaconda découvre les spokes
+# au RUNTIME (collect) dans ce dossier → l'overlay airootfs suffit, AUCUN
+# rebuild du paquet anaconda. Le glade associé est dans
+# /usr/share/anaconda/ui/spokes/fws_recap.glade (aussi via l'airootfs).
+#
+# PLACEMENT DANS LE FLUX : standalone spoke « postForHub = SummaryHub » avec
+# priority = -10. L'ordre des écrans est pre(SummaryHub) → SummaryHub →
+# post(SummaryHub) trié par priority croissante (UserInterface.
+# _orderActionClasses). ProgressSpoke — l'écran qui DÉMARRE l'installation
+# dans son refresh() — est aussi post(SummaryHub), priority 0 par défaut →
+# cette page s'intercale ENTRE les deux : l'utilisateur clique « Commencer
+# l'installation » sur le hub, voit CE récapitulatif (le disque n'a encore
+# subi AUCUNE écriture), et l'installation ne démarre qu'à son clic
+# « Lancer l'installation ». Le bouton « Précédent » ramène au hub pour
+# modifier les choix (cf. _on_back_clicked : la file d'actions du GUI est
+# consommée à chaque écran, on y réinsère le hub).
+#
+# CONTENU : configuration choisie par l'utilisateur (via les modules D-Bus
+# d'Anaconda : langue, clavier, fuseau, comptes, partitionnement + les
+# fichiers /tmp des spokes FWS : bureau, hostname) ET matériel détecté
+# (virtualisation, GPU, disques, réseau) — mêmes sources que ce que le
+# %post du kickstart et fws-setup-hardware appliqueront.
+#
+# ⚠ « completed » doit rester False : _instantiateAction SAUTE les standalone
+# spokes complétées (mécanisme prévu pour les installs kickstart).
+# ⚠ Chemin couplé à la version Python d'Arch (3.14) — comme fws_desktop.py —
+# et le bouton « Précédent » s'appuie sur les internes du GUI d'anaconda
+# 45.8 (_actions/_currentAction) : à re-vérifier à chaque bump d'anaconda.
+
+import inspect
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk
+
+from pyanaconda.anaconda_loggers import get_module_logger
+from pyanaconda.core import util
+from pyanaconda.modules.common.constants.objects import DISK_SELECTION
+from pyanaconda.modules.common.constants.services import (
+ LOCALIZATION,
+ STORAGE,
+ TIMEZONE,
+ USERS,
+)
+from pyanaconda.modules.common.structures.user import UserData
+from pyanaconda.ui.gui import GraphicalUserInterface
+from pyanaconda.ui.gui.hubs.summary import SummaryHub
+from pyanaconda.ui.gui.spokes import StandaloneSpoke
+
+log = get_module_logger(__name__)
+
+__all__ = ["FWSRecapSpoke"]
+
+# Doit rester aligné sur le %post du kickstart (étape « Bureau choisi ») et
+# sur les libellés de la spoke fws_desktop.
+_DESKTOP_LABELS = {
+ "cli": "ligne de commande uniquement (aucun bureau)",
+ "gnome": "GNOME (+ Firefox, GDM)",
+ "kde": "KDE Plasma (+ Firefox, SDDM)",
+ "hyprland": "Hyprland — Wayland, non compatible VR (+ Firefox, SDDM)",
+ "i3": "i3 — X11, compatible VR/SteamVR (+ Firefox, SDDM)",
+ "xfce": "XFCE (+ Firefox, SDDM)",
+}
+
+_PARTITIONING_LABELS = {
+ "AUTOMATIC": "automatique (recommandé)",
+ "INTERACTIVE": "personnalisé (assistant)",
+ "CUSTOM": "personnalisé",
+ "BLIVET": "avancé (blivet-gui)",
+ "MANUAL": "points de montage manuels",
+}
+
+
+def _safe(getter, default):
+ """Valeur d'un getter D-Bus/système, ou défaut : le récap ne doit JAMAIS
+ faire tomber l'installateur, quel que soit le module indisponible (d'où
+ le except large : dasbus lève des types variés selon la panne)."""
+ try:
+ return getter()
+ except Exception: # pylint: disable=broad-except
+ return default
+
+
+def _capture(*argv):
+ """stdout d'une commande, chaîne vide si absente ou en échec."""
+ try:
+ return util.execWithCapture(argv[0], list(argv[1:])) or ""
+ except (OSError, RuntimeError):
+ return ""
+
+
+def _read_flag(path):
+ try:
+ with open(path) as fh:
+ return fh.read().strip()
+ except OSError:
+ return ""
+
+
+def _config_lines():
+ """Section « configuration choisie » : interroge les modules D-Bus
+ d'Anaconda (remplis par les spokes) + les fichiers /tmp des spokes FWS."""
+ lines = []
+
+ lang = _safe(lambda: LOCALIZATION.get_proxy().Language, "")
+ if lang:
+ lines.append(f" • Langue : {lang}")
+
+ layouts = _safe(lambda: list(LOCALIZATION.get_proxy().XLayouts), [])
+ if layouts:
+ lines.append(f" • Clavier (X11) : {', '.join(layouts)}")
+
+ tz = _safe(lambda: TIMEZONE.get_proxy().Timezone, "")
+ if tz:
+ ntp = _safe(lambda: TIMEZONE.get_proxy().NTPEnabled, None)
+ suffix = {True: " — synchronisation NTP activée",
+ False: " — synchronisation NTP désactivée"}.get(ntp, "")
+ lines.append(f" • Fuseau horaire : {tz}{suffix}")
+
+ users = _safe(
+ lambda: UserData.from_structure_list(USERS.get_proxy().Users), [])
+ if users:
+ names = ", ".join(u.name for u in users if u.name)
+ lines.append(f" • Utilisateur(s) : {names} (sudo via wheel)")
+ else:
+ lines.append(" • Utilisateur(s) : AUCUN — pensez-y avant de lancer !")
+
+ root_locked = _safe(lambda: USERS.get_proxy().IsRootAccountLocked, None)
+ root_pw = _safe(lambda: USERS.get_proxy().IsRootPasswordSet, None)
+ if root_locked:
+ root_txt = "verrouillé (recommandé)"
+ elif root_pw:
+ root_txt = "actif, mot de passe défini"
+ elif root_pw is not None:
+ # Même règle que l'étape « Sécurité » du %post : un root resté sans
+ # mot de passe sera verrouillé en fin d'installation.
+ root_txt = "sans mot de passe → sera VERROUILLÉ en fin d'install"
+ else:
+ root_txt = ""
+ if root_txt:
+ lines.append(f" • Compte root : {root_txt}")
+
+ # Hostname : écrit par le spoke Utilisateur (patch user.py du paquet
+ # anaconda). Même nettoyage / même défaut que le %post --nochroot.
+ hn = "".join(c for c in _read_flag("/tmp/fws-hostname")
+ if c.isalnum() or c in "._-") or "fws"
+ lines.append(f" • Nom de machine : {hn}")
+
+ # Bureau : écrit par fws_desktop.apply() (sortie de la spoke). Absent =
+ # spoke jamais visitée = « cli », exactement le défaut du %post.
+ desk = _read_flag("/tmp/fws-desktop") or "cli"
+ lines.append(f" • Bureau : {_DESKTOP_LABELS.get(desk, desk)}")
+
+ method = _safe(
+ lambda: STORAGE.get_proxy(
+ STORAGE.get_proxy().AppliedPartitioning).PartitioningMethod, "")
+ if method:
+ lines.append(" • Partitionnement : "
+ f"{_PARTITIONING_LABELS.get(method, method)}")
+
+ return lines, desk, method
+
+
+def _build_summary():
+ config_lines, desk, method = _config_lines()
+ config_text = "\n".join(config_lines) \
+ or " (configuration non disponible)"
+
+ # Disques ciblés par l'installation (spoke « Destination »).
+ selected = set(_safe(
+ lambda: list(STORAGE.get_proxy(DISK_SELECTION).SelectedDisks), []))
+ disks = []
+ for line in _capture("lsblk", "-dno", "NAME,SIZE,MODEL,TYPE").splitlines():
+ fields = line.split()
+ if fields and fields[-1] == "disk":
+ mark = " ← CIBLE de l'installation" if fields[0] in selected else ""
+ disks.append(" • " + " ".join(fields[:-1]) + mark)
+ disk_text = "\n".join(disks) or " (aucun disque détecté)"
+
+ if method == "AUTOMATIC":
+ disk_warn = (
+ " ⚠ Rien n'a encore été écrit. Le partitionnement automatique\n"
+ " EFFACERA ENTIÈREMENT le(s) disque(s) cible(s) au lancement.")
+ else:
+ disk_warn = (
+ " ⚠ Rien n'a encore été écrit. Au lancement, ce sont vos choix\n"
+ " de partitionnement faits dans l'installateur qui s'appliqueront.")
+
+ virt = (_capture("systemd-detect-virt").strip().splitlines() or ["none"])[0]
+ if virt in ("", "none"):
+ virt = "aucune (machine physique)"
+
+ gpus = []
+ for line in _capture("lspci").splitlines():
+ low = line.lower()
+ if ("vga" in low or "3d" in low or "display" in low) and ":" in line:
+ gpus.append(" • " + line.split(":", 2)[-1].strip())
+ gpu_text = "\n".join(gpus) or " (aucune carte graphique détectée)"
+
+ try:
+ net_ok = util.execWithRedirect("ip", ["route", "get", "1.1.1.1"]) == 0
+ except (OSError, RuntimeError):
+ net_ok = False
+ if net_ok:
+ net = "disponible ✔"
+ else:
+ net = ("INDISPONIBLE ✖ — bureau, drivers et stack FWS ne\n"
+ " s'installeront PAS (rattrapable au 1er boot)")
+
+ extras = ("Steam, Wine, VR (Monado/OpenXR), winboat-bin"
+ if desk != "cli" else "rien de plus (choix CLI)")
+
+ return f"""\
+══════════════════════════════════════════════════════════
+ FWS — RÉCAPITULATIF AVANT INSTALLATION
+══════════════════════════════════════════════════════════
+
+CONFIGURATION CHOISIE
+{config_text}
+
+DISQUES
+{disk_warn}
+{disk_text}
+
+MATÉRIEL DÉTECTÉ
+ • Virtualisation : {virt}
+ • Réseau : {net}
+ Cartes graphiques (drivers installés automatiquement en fin d'install) :
+{gpu_text}
+
+DÉROULEMENT DE L'INSTALLATION
+ 1. Partitionnement, puis copie du système live FWS (payload LiveOS)
+ 2. Noyau + initramfs régénérés (mkinitcpio), GRUB installé
+ 3. Bureau choisi + navigateur installés (réseau requis)
+ 4. Matériel : drivers GPU, outils VM, son, Bluetooth, Podman
+ + si bureau choisi : {extras}
+
+« Précédent » pour revenir modifier vos choix —
+« Lancer l'installation » pour démarrer réellement l'installation.
+"""
+
+
+class FWSRecapSpoke(StandaloneSpoke):
+ """Récapitulatif des choix, affiché entre le hub et l'installation."""
+
+ builderObjects = ["fwsRecapWindow"]
+ mainWidgetName = "fwsRecapWindow"
+ uiFile = "spokes/fws_recap.glade"
+
+ postForHub = SummaryHub
+ priority = -10
+
+ @staticmethod
+ def get_screen_id():
+ """Identifiant unique de cet écran."""
+ return "fws-recap"
+
+ @property
+ def completed(self):
+ # False, sinon _instantiateAction saute l'écran (cf. en-tête).
+ return False
+
+ def apply(self):
+ # Rien à persister : cette page ne fait que montrer l'état.
+ pass
+
+ def initialize(self):
+ StandaloneSpoke.initialize(self)
+ # Le bouton « continuer » de CETTE page est le vrai déclencheur :
+ # l'écran suivant (ProgressSpoke) démarre la file d'installation.
+ self.window.get_continue_button().set_label("_Lancer l'installation")
+
+ # Capture pour « Précédent » : initialize() est appelé DEPUIS
+ # GraphicalUserInterface._on_continue_clicked (clic « Commencer
+ # l'installation » du hub), AVANT la bascule de _currentAction → à
+ # cet instant ui._currentAction est ENCORE l'instance du hub Résumé,
+ # et le « self » de l'appelant est l'UI. Aucune API publique ne les
+ # expose ; on remonte la pile d'appels (déterministe — pas de scan
+ # mémoire, pas de variable globale).
+ self._gui = None
+ self._hub = None
+ frame = inspect.currentframe()
+ try:
+ frame = frame.f_back
+ while frame is not None:
+ candidate = frame.f_locals.get("self")
+ if isinstance(candidate, GraphicalUserInterface):
+ self._gui = candidate
+ current = getattr(candidate, "_currentAction", None)
+ if isinstance(current, SummaryHub):
+ self._hub = current
+ break
+ frame = frame.f_back
+ finally:
+ del frame
+
+ # Bouton « Précédent », placé avec les boutons de navigation
+ # (à côté de « Quitter » — leur parent est la GtkButtonBox du bas).
+ quit_button = self.window.get_quit_button()
+ button_box = quit_button.get_parent()
+ self._back_button = Gtk.Button.new_with_mnemonic("_Précédent")
+ self._back_button.connect("clicked", self._on_back_clicked)
+ button_box.add(self._back_button)
+ button_box.reorder_child(self._back_button, 0)
+ if self._gui is None or self._hub is None:
+ # Références introuvables (flux inattendu) : bouton grisé plutôt
+ # qu'une navigation cassée — l'install reste possible.
+ log.warning("FWS recap: hub/UI introuvables, « Précédent » désactivé.")
+ self._back_button.set_sensitive(False)
+ self._back_button.show()
+
+ def _on_back_clicked(self, _button):
+ ui = self._gui
+ hub = self._hub
+ if ui is None or hub is None:
+ return
+ log.info("FWS recap: retour au hub Résumé pour modification.")
+ # La file d'actions du GUI est CONSOMMÉE (pop(0)) à chaque écran :
+ # ici _actions = [FWSRecapSpoke, ProgressSpoke] et le hub en est déjà
+ # sorti. On le réinsère en tête pour que le prochain « Commencer
+ # l'installation » ré-enchaîne hub → NOUVELLE page récap (résumé
+ # recalculé avec les choix modifiés) → installation.
+ if not ui._actions or ui._actions[0] is not SummaryHub:
+ ui._actions.insert(0, SummaryHub)
+ # _on_continue_clicked a désactivé le « continuer » du hub en le
+ # quittant : on le réarme, sinon « Commencer l'installation » est mort.
+ hub.window.set_may_continue(True)
+ # Même séquence que la transition officielle (_on_continue_clicked).
+ self.exited.emit(self)
+ hub.refresh()
+ ui.mainWindow.setCurrentAction(hub)
+ hub.entered.emit(hub)
+ ui._currentAction = hub
+
+ def refresh(self):
+ StandaloneSpoke.refresh(self)
+ text = _build_summary()
+ self.builder.get_object("recapView").get_buffer().set_text(text)
+ # Copie pour le harnais de debug du .xinitrc (fws-debug.txt).
+ try:
+ with open("/tmp/fws-hw-summary.txt", "w") as fh:
+ fh.write(text)
+ except OSError:
+ pass
diff --git a/configs/baseline/airootfs/usr/local/share/fws/anaconda-summary-hub.glade b/configs/baseline/airootfs/usr/local/share/fws/anaconda-summary-hub.glade
new file mode 100644
index 0000000..db733f0
--- /dev/null
+++ b/configs/baseline/airootfs/usr/local/share/fws/anaconda-summary-hub.glade
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
diff --git a/configs/baseline/airootfs/usr/share/anaconda/ui/spokes/fws_desktop.glade b/configs/baseline/airootfs/usr/share/anaconda/ui/spokes/fws_desktop.glade
new file mode 100644
index 0000000..6d5fafb
--- /dev/null
+++ b/configs/baseline/airootfs/usr/share/anaconda/ui/spokes/fws_desktop.glade
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+ False
+ True
+ True
+ DESKTOP ENVIRONMENT
+
+
+
+ False
+ vertical
+ 6
+
+
+ False
+
+
+ False
+ 6
+ 6
+ 6
+
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ False
+ 12
+ 0
+ 0
+ 0
+ 48
+ 24
+ 24
+
+
+ False
+ vertical
+ 8
+
+
+ True
+ False
+ 0
+ 12
+ True
+ 60
+ Choose the desktop environment to install. A web browser (Firefox) is installed automatically with any desktop.
+
+
+ False
+ True
+ 0
+
+
+
+
+ Command line only (no desktop)
+ True
+ True
+ False
+ True
+ True
+
+
+ False
+ True
+ 1
+
+
+
+
+ GNOME
+ True
+ True
+ False
+ True
+ radio_cli
+
+
+ False
+ True
+ 2
+
+
+
+
+ KDE Plasma
+ True
+ True
+ False
+ True
+ radio_cli
+
+
+ False
+ True
+ 3
+
+
+
+
+ Hyprland — Wayland (NOT VR compatible)
+ True
+ True
+ False
+ True
+ radio_cli
+
+
+ False
+ True
+ 4
+
+
+
+
+ i3 — X11 (VR compatible: SteamVR, HTC Vive, Valve Index…)
+ True
+ True
+ False
+ True
+ radio_cli
+
+
+ False
+ True
+ 5
+
+
+
+
+ XFCE
+ True
+ True
+ False
+ True
+ radio_cli
+
+
+ False
+ True
+ 6
+
+
+
+
+
+
+
+
+
+
+ DESKTOP ENVIRONMENT
+
+
+
+
diff --git a/configs/baseline/airootfs/usr/share/anaconda/ui/spokes/fws_recap.glade b/configs/baseline/airootfs/usr/share/anaconda/ui/spokes/fws_recap.glade
new file mode 100644
index 0000000..2b8885a
--- /dev/null
+++ b/configs/baseline/airootfs/usr/share/anaconda/ui/spokes/fws_recap.glade
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+ False
+ RÉCAPITULATIF AVANT INSTALLATION
+
+
+ False
+ vertical
+
+
+ False
+
+
+ False
+
+
+
+
+
+
+
+
+
+
+ False
+ False
+ 0
+
+
+
+
+ False
+ 0
+ 12
+ 6
+
+
+ False
+ vertical
+ 6
+
+
+ True
+ True
+ in
+
+
+ True
+ True
+ False
+ False
+ True
+ 12
+ 12
+ 12
+ 12
+ word-char
+
+
+
+
+ True
+ True
+ 0
+
+
+
+
+
+
+ True
+ True
+ 1
+
+
+
+
+
+
diff --git a/configs/baseline/efiboot/loader/entries/01-archiso-linux.conf b/configs/baseline/efiboot/loader/entries/01-archiso-linux.conf
index 8c7964f..19928c5 100644
--- a/configs/baseline/efiboot/loader/entries/01-archiso-linux.conf
+++ b/configs/baseline/efiboot/loader/entries/01-archiso-linux.conf
@@ -1,4 +1,5 @@
-title Arch Linux (%ARCH%, UEFI)
-linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux
-initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
-options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID%
+title FWS install medium (%ARCH%, UEFI)
+sort-key 01
+linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux
+initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
+options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% udev.event_timeout=45
diff --git a/configs/baseline/packages.x86_64 b/configs/baseline/packages.x86_64
index 86cd99c..b9e025b 100644
--- a/configs/baseline/packages.x86_64
+++ b/configs/baseline/packages.x86_64
@@ -62,6 +62,7 @@ zsh
grml-zsh-config
bash
zenity
+zram-generator
nano
vim
less
@@ -79,6 +80,7 @@ pacman-contrib
dmidecode
usbutils
pciutils
+firewalld
fastfetch
htop
@@ -103,6 +105,8 @@ open-vm-tools
# --- FWS (fws-local) ---
winboat-bin
+# Gestionnaire D&D 5e (dés, personnages, grimoire, sessions, combat).
+fws-dnd
# --- Installateur Anaconda (fws-local) ---
# 'anaconda' tire automatiquement anaconda-widgets + toutes ses dépendances
diff --git a/configs/baseline/pacman.conf b/configs/baseline/pacman.conf
index c9118e9..e7e6497 100644
--- a/configs/baseline/pacman.conf
+++ b/configs/baseline/pacman.conf
@@ -1,8 +1,3 @@
-[fws-local]
-SigLevel = Optional TrustAll
-Server = file:///mnt/c/Users/Murasaki/Documents/FWS/fws/local-repo
-
-
#
# /etc/pacman.conf
#
diff --git a/configs/baseline/profiledef.sh b/configs/baseline/profiledef.sh
index f0ba624..1ab0b95 100644
--- a/configs/baseline/profiledef.sh
+++ b/configs/baseline/profiledef.sh
@@ -26,4 +26,19 @@ file_permissions=(
["/usr/local/bin/choose-mirror"]="0:0:755"
["/usr/local/bin/Installation_guide"]="0:0:755"
["/usr/local/bin/livecd-sound"]="0:0:755"
+ ["/usr/bin/load_policy"]="0:0:755"
+ ["/usr/bin/setstatus"]="0:0:755"
+ ["/usr/local/bin/fws-setup-hardware"]="0:0:755"
+ ["/usr/local/bin/fws-keys"]="0:0:755"
+ ["/usr/local/bin/fws-vr-setup"]="0:0:755"
+ ["/usr/local/bin/fws-hello"]="0:0:755"
+ ["/usr/local/bin/fws-update"]="0:0:755"
+ ["/usr/local/bin/fws-updates-check"]="0:0:755"
+ ["/usr/local/bin/fws-proton-ge"]="0:0:755"
+ ["/usr/local/bin/fws-recovery"]="0:0:755"
+ ["/usr/local/bin/fws-recovery-session"]="0:0:755"
+ ["/usr/local/bin/fws-recovery-gui"]="0:0:755"
+ ["/usr/local/bin/fws-powermenu"]="0:0:755"
+ ["/usr/local/bin/fws-about"]="0:0:755"
+ ["/etc/grub.d/42_fws-recovery"]="0:0:755"
)
diff --git a/configs/baseline/syslinux/syslinux-linux.cfg b/configs/baseline/syslinux/syslinux-linux.cfg
deleted file mode 100644
index 10bb394..0000000
--- a/configs/baseline/syslinux/syslinux-linux.cfg
+++ /dev/null
@@ -1,5 +0,0 @@
-LABEL arch
-MENU LABEL Arch Linux (%ARCH%, BIOS)
-LINUX /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux
-INITRD /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
-APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID%
diff --git a/configs/baseline/syslinux/syslinux.cfg b/configs/baseline/syslinux/syslinux.cfg
index 561ac7c..cbda72f 100644
--- a/configs/baseline/syslinux/syslinux.cfg
+++ b/configs/baseline/syslinux/syslinux.cfg
@@ -1,9 +1,11 @@
-SERIAL 0 115200
-UI menu.c32
-MENU TITLE Arch Linux
-MENU CLEAR
+DEFAULT select
-DEFAULT arch
-TIMEOUT 30
+LABEL select
+COM32 whichsys.c32
+APPEND -pxe- pxe -sys- sys -iso- sys
-INCLUDE syslinux-linux.cfg
+LABEL pxe
+CONFIG archiso_pxe.cfg
+
+LABEL sys
+CONFIG archiso_sys.cfg
diff --git a/configs/releng/airootfs/etc/grub.d/42_fws-recovery b/configs/releng/airootfs/etc/grub.d/42_fws-recovery
new file mode 100755
index 0000000..d0b300c
--- /dev/null
+++ b/configs/releng/airootfs/etc/grub.d/42_fws-recovery
@@ -0,0 +1,40 @@
+#!/bin/sh
+# ============================================================================
+# FWS — entrée « Réparer FWS » dans le menu GRUB du SYSTÈME INSTALLÉ
+# (l'équivalent de l'environnement de récupération de Windows).
+#
+# Ce script est exécuté par grub-mkconfig (à l'installation via le %post du
+# kickstart, puis à chaque fws-recovery « Réinstaller GRUB »). Il démarre le
+# MÊME système sur fws-recovery.target (+ marqueur fws.recovery=1) :
+# fws-recovery-boot.service prend alors tty1 et lance l'application de
+# réparation (GUI via startx, repli console).
+#
+# Sur le live, ce fichier est inerte (grub-mkconfig n'y tourne jamais).
+# ============================================================================
+set -e
+. /usr/share/grub/grub-mkconfig_lib
+
+kernel=/boot/vmlinuz-linux
+# L'initramfs FALLBACK (sans autodetect) : c'est l'entrée de secours, elle
+# doit démarrer même si le matériel a changé depuis le dernier mkinitcpio.
+initrd=/boot/initramfs-linux-fallback.img
+[ -f "$initrd" ] || initrd=/boot/initramfs-linux.img
+[ -f "$kernel" ] && [ -f "$initrd" ] || exit 0
+
+# Même logique racine que 10_linux : UUID sauf refus explicite.
+if [ -n "${GRUB_DEVICE_UUID:-}" ] && [ "x${GRUB_DISABLE_LINUX_UUID:-}" != xtrue ]; then
+ rootarg="root=UUID=$GRUB_DEVICE_UUID"
+else
+ rootarg="root=$GRUB_DEVICE"
+fi
+
+rel_kernel="$(make_system_path_relative_to_its_root "$kernel")"
+rel_initrd="$(make_system_path_relative_to_its_root "$initrd")"
+
+cat <{GLib.markup_escape_text(key)}")
+ k.get_style_context().add_class("dim-label")
+ v = Gtk.Label(label=val, xalign=0, selectable=True)
+ v.set_line_wrap(True)
+ grid.attach(k, 0, i, 1, 1)
+ grid.attach(v, 1, i, 1, 1)
+ return grid
+
+
+def _project_tab(osr):
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10, margin=20)
+ title = Gtk.Label()
+ name = osr.get("PRETTY_NAME") or osr.get("NAME") or "FWS Linux"
+ title.set_markup(f"{name}")
+ box.pack_start(title, False, False, 0)
+
+ tag = Gtk.Label(label="Une distribution Linux pensée pour remplacer "
+ "Windows : bureau prêt à l'emploi, jeux, VR et "
+ "compatibilité applicative.")
+ tag.set_line_wrap(True)
+ tag.set_max_width_chars(52)
+ box.pack_start(tag, False, False, 0)
+
+ feats = Gtk.Label()
+ feats.set_markup(
+ "Inclus : installateur graphique · plusieurs bureaux "
+ "(GNOME, KDE, Hyprland, i3, XFCE) · Steam & Proton-GE · "
+ "réalité virtuelle (SteamVR) · winboat (applications Windows) · "
+ "pare-feu · réparation système intégrée.")
+ feats.set_line_wrap(True)
+ feats.set_max_width_chars(52)
+ feats.set_xalign(0)
+ box.pack_start(feats, False, False, 6)
+
+ home = osr.get("HOME_URL") or "https://github.com/You-re-like-Windows-a-bitch/fws"
+ link = Gtk.LinkButton.new_with_label(home, "Site du projet")
+ box.pack_start(link, False, False, 0)
+ return box
+
+
+def _system_tab(osr):
+ rows = [
+ ("Système", osr.get("PRETTY_NAME", "FWS Linux")),
+ ("Build", osr.get("BUILD_ID", "rolling")),
+ ("Noyau", _cmd("uname", "-r") or "inconnu"),
+ ("Bureau", _desktop()),
+ ("Affichage", _session_type()),
+ ("Nom de machine", _cmd("hostname") or os.uname().nodename),
+ ("Processeur", _cpu()),
+ ("Mémoire", _mem()),
+ ("Carte(s) graphique(s)", "\n".join(_gpus())),
+ ("Disque(s)", "\n".join(_disks())),
+ ]
+ scroll = Gtk.ScrolledWindow()
+ scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
+ scroll.add(_grid(rows))
+ return scroll
+
+
+class AboutWindow(Gtk.Window):
+ def __init__(self):
+ super().__init__(title="À propos de FWS")
+ self.set_default_size(520, 460)
+ self.set_position(Gtk.WindowPosition.CENTER)
+ self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
+
+ osr = _osrelease()
+ notebook = Gtk.Notebook()
+ notebook.append_page(_project_tab(osr), Gtk.Label(label="Le projet FWS"))
+ notebook.append_page(_system_tab(osr), Gtk.Label(label="Système"))
+ self.add(notebook)
+ self.connect("destroy", Gtk.main_quit)
+
+
+def main():
+ if not Gtk.init_check()[0]:
+ return 1
+ win = AboutWindow()
+ win.show_all()
+ Gtk.main()
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/configs/releng/airootfs/usr/local/bin/fws-hello b/configs/releng/airootfs/usr/local/bin/fws-hello
new file mode 100755
index 0000000..0891730
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-hello
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+# FWS — fenêtre de bienvenue au premier démarrage (pendant post-install de la
+# page récap de l'installateur). Boutons : mise à jour, codecs, VR, raccourcis,
+# Proton-GE. GTK3 : présent sur TOUS les bureaux FWS (Firefox le tire) ;
+# python-gobject est installé avec le bureau par le kickstart.
+#
+# Lancement : /etc/xdg/autostart (KDE/GNOME/XFCE) et exec des configs
+# Hyprland/i3, toujours avec --autostart : on se retire sans bruit si
+# l'utilisateur a coché « ne plus afficher » (~/.config/fws/hello-done).
+
+import os
+import shutil
+import subprocess
+import sys
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+gi.require_version("Gdk", "3.0") # sinon gi charge Gdk 4.0 (conflit avec Gtk3)
+from gi.repository import Gdk, GLib, Gtk # noqa: E402
+
+# WM_CLASS déterministe : les règles i3/Hyprland « fenêtre flottante »
+# ciblent cette classe.
+GLib.set_prgname("fws-hello")
+
+_FLAG = os.path.expanduser("~/.config/fws/hello-done")
+
+_CODECS = ("gst-plugins-base gst-plugins-good gst-plugins-bad "
+ "gst-plugins-ugly gst-libav ffmpeg mpv")
+
+# (libellé, description, commande, en_terminal)
+_ACTIONS = [
+ ("Mettre à jour le système",
+ "pacman -Syu + nettoyage du cache",
+ "fws-update", True),
+ ("Installer les codecs multimédia",
+ "lecture vidéo/audio (GStreamer, ffmpeg, mpv)",
+ f"sudo pacman -S --needed {_CODECS}", True),
+ ("Proton-GE pour Steam",
+ "meilleure compatibilité des jeux Windows",
+ "fws-proton-ge", True),
+ ("Configurer la VR",
+ "drivers casque : HTC Vive, Index, Quest…",
+ "fws-vr-setup", True),
+]
+
+_TERMINALS = ["kitty", "konsole", "gnome-terminal", "xfce4-terminal", "xterm"]
+
+
+def _terminal():
+ for term in _TERMINALS:
+ if shutil.which(term):
+ return term
+ return None
+
+
+def _run(command, in_terminal):
+ if in_terminal:
+ term = _terminal()
+ if term is None:
+ return
+ if term == "gnome-terminal":
+ argv = [term, "--", "sh", "-c", command]
+ else:
+ argv = [term, "-e", "sh", "-c", command]
+ else:
+ argv = ["sh", "-c", command]
+ subprocess.Popen(argv) # noqa: S603 — commandes internes FWS
+
+
+class HelloWindow(Gtk.Window):
+ def __init__(self):
+ super().__init__(title="Bienvenue sur FWS")
+ self.set_default_size(460, -1)
+ self.set_position(Gtk.WindowPosition.CENTER)
+ self.set_border_width(18)
+ # DIALOGUE : les tuileurs (i3/Hyprland) laissent les dialogues en
+ # flottant → la fenêtre garde sa taille naturelle au lieu d'être
+ # étirée sur tout l'espace de travail (layout illisible sinon).
+ self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
+ self.set_resizable(False)
+
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
+ self.add(box)
+
+ title = Gtk.Label()
+ title.set_markup("Bienvenue sur FWS !")
+ box.pack_start(title, False, False, 0)
+
+ subtitle = Gtk.Label(
+ label="Ton système est prêt. Quelques réglages utiles pour bien démarrer :")
+ subtitle.set_line_wrap(True)
+ subtitle.set_xalign(0)
+ box.pack_start(subtitle, False, False, 0)
+
+ actions = list(_ACTIONS)
+ # Raccourcis clavier : uniquement sur les sessions qui ont fws-keys
+ # (Hyprland/i3) — les autres bureaux ont leurs propres réglages.
+ session = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
+ if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE") or "i3" in session:
+ actions.append(("Voir les raccourcis clavier",
+ "aussi disponible à tout moment : Super+K",
+ "fws-keys", False))
+
+ for label, desc, command, in_term in actions:
+ button = Gtk.Button()
+ inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
+ lab = Gtk.Label()
+ lab.set_markup(f"{label}")
+ lab.set_xalign(0)
+ sub = Gtk.Label(label=desc)
+ sub.set_xalign(0)
+ sub.get_style_context().add_class("dim-label")
+ inner.pack_start(lab, False, False, 0)
+ inner.pack_start(sub, False, False, 0)
+ button.add(inner)
+ button.connect("clicked",
+ lambda _b, c=command, t=in_term: _run(c, t))
+ box.pack_start(button, False, False, 0)
+
+ self._again = Gtk.CheckButton(label="Afficher cette fenêtre au prochain démarrage")
+ self._again.set_active(False)
+ box.pack_start(self._again, False, False, 6)
+
+ close = Gtk.Button(label="Fermer")
+ close.connect("clicked", lambda _b: self.close())
+ box.pack_start(close, False, False, 0)
+
+ self.connect("destroy", self._on_destroy)
+
+ def _on_destroy(self, _win):
+ if not self._again.get_active():
+ os.makedirs(os.path.dirname(_FLAG), exist_ok=True)
+ with open(_FLAG, "w") as fh:
+ fh.write("ok\n")
+ else:
+ try:
+ os.unlink(_FLAG)
+ except OSError:
+ pass
+ Gtk.main_quit()
+
+
+def main():
+ if "--autostart" in sys.argv and os.path.exists(_FLAG):
+ return 0
+ # Pas d'affichage joignable (lancement hors session graphique) : on se
+ # retire sans traceback plutôt que de crasher.
+ if not Gtk.init_check()[0]:
+ return 1
+ win = HelloWindow()
+ win.show_all()
+ Gtk.main()
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/configs/releng/airootfs/usr/local/bin/fws-keys b/configs/releng/airootfs/usr/local/bin/fws-keys
new file mode 100755
index 0000000..0e3dc5e
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-keys
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+# FWS — affiche les raccourcis clavier RÉELS de la session (bouton ⌨ de la
+# barre ou Super+K). Universel :
+# • Hyprland : « hyprctl binds -j » = les binds effectivement chargés
+# (descriptions des bindd) — affichage wofi (Wayland).
+# • i3 : parse ~/.config/i3/config (bindsym, variables résolues)
+# — affichage rofi (X11).
+# Aucune dépendance hors python3 + wofi/rofi (déjà installés avec le bureau).
+
+import json
+import os
+import re
+import subprocess
+import sys
+
+_WOFI_STYLE = "/usr/local/share/fws/wofi-keys.css"
+
+# Bits de modmask Hyprland (masques X11) dans l'ordre d'affichage voulu.
+_MODS = [(64, "SUPER"), (4, "CTRL"), (8, "ALT"), (1, "SHIFT")]
+
+_KEY_LABELS = {
+ "mouse:272": "clic gauche",
+ "mouse:273": "clic droit",
+ "mouse:274": "clic molette",
+ "mouse_down": "molette bas",
+ "mouse_up": "molette haut",
+ "button4": "molette haut",
+ "button5": "molette bas",
+ "return": "Entrée",
+ "space": "Espace",
+ "escape": "Échap",
+ "tab": "Tab",
+ "left": "←",
+ "right": "→",
+ "up": "↑",
+ "down": "↓",
+ "print": "Impr écran",
+}
+
+
+def _label(key):
+ return _KEY_LABELS.get(key.lower(), key)
+
+
+# --- Hyprland : binds réels via hyprctl ------------------------------------
+def hyprland_lines():
+ raw = subprocess.check_output(["hyprctl", "binds", "-j"], text=True)
+ lines, seen = [], set()
+ for bind in json.loads(raw):
+ parts = [name for bit, name in _MODS if bind.get("modmask", 0) & bit]
+ key = bind.get("key") or (f"code {bind['keycode']}" if bind.get("keycode") else "")
+ parts.append(_label(key))
+ combo = " + ".join(p for p in parts if p)
+ if not combo or combo in seen:
+ continue
+ seen.add(combo)
+ desc = (bind.get("description") or "").strip() \
+ or f"{bind.get('dispatcher', '?')} {(bind.get('arg') or '').strip()}".strip()
+ lines.append(f"{combo:<24} {desc}")
+ return lines
+
+
+# --- i3 : parse de la config (variables résolues) ----------------------------
+def i3_lines():
+ path = os.path.expanduser("~/.config/i3/config")
+ variables = {}
+ lines, seen = [], set()
+ with open(path, encoding="utf-8", errors="replace") as fh:
+ content = fh.readlines()
+ for line in content:
+ m = re.match(r"\s*set\s+(\$\S+)\s+(.+)$", line)
+ if m:
+ variables[m.group(1)] = m.group(2).strip()
+ def resolve(text):
+ # $var les plus longs d'abord ($filemanager avant $file…).
+ for var in sorted(variables, key=len, reverse=True):
+ text = text.replace(var, variables[var])
+ return text
+ for line in content:
+ m = re.match(r"\s*bindsym\s+(?:--\S+\s+)*(\S+)\s+(.+)$", line)
+ if not m:
+ continue
+ combo_raw, action = m.groups()
+ combo_raw = resolve(combo_raw).replace("Mod4", "Super").replace("Mod1", "Alt")
+ combo = " + ".join(_label(k) for k in combo_raw.split("+"))
+ if combo in seen:
+ continue
+ seen.add(combo)
+ action = resolve(action)
+ action = re.sub(r"^exec\s+(--no-startup-id\s+)?", "", action).strip().strip('"')
+ lines.append(f"{combo:<24} {action}")
+ return lines
+
+
+def show(lines):
+ if not lines:
+ lines = ["(aucun raccourci trouvé)"]
+ text = "\n".join(lines)
+ if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
+ cmd = ["wofi", "--dmenu", "--prompt", "Raccourcis clavier — taper pour filtrer",
+ "--width", "820", "--height", "600", "--insensitive",
+ "--cache-file", "/dev/null"]
+ if os.path.exists(_WOFI_STYLE):
+ cmd += ["--style", _WOFI_STYLE]
+ else:
+ # X11/i3 : rofi en dmenu, style assorti (mono pour l'alignement).
+ cmd = ["rofi", "-dmenu", "-i", "-p", "Raccourcis clavier",
+ "-no-custom",
+ "-theme-str", 'window { width: 880px; } listview { lines: 18; }',
+ "-theme-str", '* { font: "Noto Sans Mono 11"; }']
+ subprocess.run(cmd, input=text, text=True, check=False)
+
+
+def main():
+ try:
+ if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
+ lines = hyprland_lines()
+ else:
+ lines = i3_lines()
+ except Exception as exc: # noqa: BLE001 — best effort, jamais de crash muet
+ subprocess.run(["notify-send", "Raccourcis FWS", f"Erreur : {exc}"],
+ check=False)
+ return 1
+ show(lines)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/configs/releng/airootfs/usr/local/bin/fws-powermenu b/configs/releng/airootfs/usr/local/bin/fws-powermenu
new file mode 100755
index 0000000..4002796
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-powermenu
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+# FWS — menu d'alimentation (verrouiller / déconnexion / suspendre /
+# redémarrer / éteindre). Bouton de la barre ou Super+X. Universel :
+# • Hyprland (Wayland) → wofi, verrouillage hyprlock, sortie hyprctl.
+# • i3 (X11) → rofi, verrouillage i3lock, sortie i3-msg exit.
+# Aucune dépendance hors python3 + wofi/rofi (déjà là avec le bureau).
+
+import os
+import subprocess
+import sys
+
+_WOFI_STYLE = "/usr/local/share/fws/wofi-style.css"
+_HYPR = bool(os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"))
+
+# (libellé affiché, commande shell). L'icône Nerd Font aide au repérage.
+if _HYPR:
+ _lock = "hyprlock"
+ _logout = "hyprctl dispatch exit"
+else:
+ _lock = "i3lock -c 1e1e2e"
+ _logout = "i3-msg exit"
+
+_ITEMS = [
+ (" Verrouiller", _lock),
+ (" Déconnexion", _logout),
+ (" Suspendre", "systemctl suspend"),
+ (" Redémarrer", "systemctl reboot"),
+ (" Éteindre", "systemctl poweroff"),
+]
+
+
+def _menu(entries):
+ text = "\n".join(entries)
+ if _HYPR:
+ cmd = ["wofi", "--dmenu", "--prompt", "Alimentation",
+ "--width", "300", "--height", "260", "--cache-file", "/dev/null"]
+ if os.path.exists(_WOFI_STYLE):
+ cmd += ["--style", _WOFI_STYLE]
+ else:
+ cmd = ["rofi", "-dmenu", "-i", "-p", "Alimentation", "-no-custom",
+ "-theme-str", "window { width: 300px; } listview { lines: 5; }"]
+ out = subprocess.run(cmd, input=text, text=True,
+ capture_output=True, check=False)
+ return out.stdout.strip()
+
+
+def main():
+ choice = _menu([label for label, _ in _ITEMS])
+ if not choice:
+ return 0
+ for label, command in _ITEMS:
+ if label == choice:
+ subprocess.Popen(["sh", "-c", command]) # noqa: S603,S607
+ break
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/configs/releng/airootfs/usr/local/bin/fws-proton-ge b/configs/releng/airootfs/usr/local/bin/fws-proton-ge
new file mode 100755
index 0000000..8aa035a
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-proton-ge
@@ -0,0 +1,46 @@
+#!/bin/bash
+# FWS — installe/actualise Proton-GE (GloriousEggroll) pour Steam :
+# meilleure compatibilité des jeux Windows que le Proton officiel.
+# S'exécute en UTILISATEUR (installe dans ~/.steam/root/compatibilitytools.d).
+set -u
+say() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
+fail() { printf '\033[1;31m ✗ %s\033[0m\n' "$*"; read -rp "Entrée pour fermer. " _; exit 1; }
+
+[ "$(id -u)" -eq 0 ] && fail "à lancer en utilisateur normal (pas root) : l'outil s'installe dans ton Steam."
+
+API="https://api.github.com/repos/GloriousEggroll/proton-ge-custom/releases/latest"
+DEST="$HOME/.steam/root/compatibilitytools.d"
+
+say "Recherche de la dernière version de Proton-GE…"
+JSON="$(curl -fsSL "$API")" || fail "GitHub inaccessible (réseau ?)"
+URL="$(printf '%s' "$JSON" | grep -o '"browser_download_url": *"[^"]*\.tar\.gz"' | cut -d'"' -f4 | head -1)"
+SUM_URL="$(printf '%s' "$JSON" | grep -o '"browser_download_url": *"[^"]*\.sha512sum"' | cut -d'"' -f4 | head -1)"
+[ -n "$URL" ] || fail "archive introuvable dans la release GitHub"
+NAME="$(basename "$URL" .tar.gz)"
+
+if [ -d "$DEST/$NAME" ]; then
+ say "$NAME est déjà installé ✔"
+ read -rp "Entrée pour fermer. " _; exit 0
+fi
+
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+say "Téléchargement de $NAME (~450 Mo)…"
+curl -fL --progress-bar -o "$TMP/$NAME.tar.gz" "$URL" || fail "téléchargement échoué"
+
+if [ -n "$SUM_URL" ]; then
+ say "Vérification de l'intégrité (sha512)…"
+ curl -fsSL -o "$TMP/sum" "$SUM_URL" \
+ && (cd "$TMP" && sed "s|[^ ]*\.tar\.gz|$NAME.tar.gz|" sum | sha512sum -c -) \
+ || fail "somme de contrôle invalide — archive corrompue ?"
+fi
+
+say "Installation dans $DEST…"
+mkdir -p "$DEST"
+tar -xzf "$TMP/$NAME.tar.gz" -C "$DEST" || fail "extraction échouée"
+
+say "$NAME installé ✔"
+echo " Dans Steam : Paramètres → Compatibilité → activer, puis choisir"
+echo " « $NAME » (ou par jeu : Propriétés → Compatibilité)."
+echo " Redémarre Steam pour qu'il apparaisse."
+read -rp "Entrée pour fermer. " _
diff --git a/configs/releng/airootfs/usr/local/bin/fws-recovery b/configs/releng/airootfs/usr/local/bin/fws-recovery
new file mode 100755
index 0000000..cecbb62
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-recovery
@@ -0,0 +1,220 @@
+#!/bin/bash
+# ============================================================================
+# FWS — moteur de réparation d'une installation existante depuis le live
+# (« point de restauration à la Windows »).
+#
+# L'interface GRAPHIQUE est fws-recovery-gui (GTK3 python — le live embarque
+# python-gobject/gtk3 via Anaconda) ; elle pilote CE script en sous-commandes.
+# Sans argument : assistant CONSOLE (repli sans X, ou usage à la main).
+#
+# Sous-commandes (pour la GUI / scripts) :
+# scan → une ligne « périphérique|nom » par install FWS
+# repair → monte, répare, démonte ; sortie en continu
+# action ∈ grub|initramfs|pacman|pacman-update|all
+# shell → terminal DANS le système (xterm si DISPLAY)
+#
+# arch-chroot (arch-install-scripts, présent sur le live) gère les binds.
+#
+# Fonctionne depuis le LIVE (autre disque) ET depuis le SYSTÈME INSTALLÉ
+# lui-même (menu d'applications / entrée GRUB « Réparer FWS ») : si la cible
+# est la racine en cours d'exécution, on opère directement, sans mount/chroot.
+# ============================================================================
+set -u
+MNT=/mnt/fws-recovery
+ROOTDEV=""
+DO_UPDATE=""
+SELF="" # cible = système courant
+ROOTSRC="$(findmnt -no SOURCE / 2>/dev/null)" # sur le live : overlay → jamais un lsblk
+
+say() { printf '\n==> %s\n' "$*"; }
+warn() { printf ' ⚠ %s\n' "$*"; }
+
+[ "$(id -u)" -eq 0 ] || { echo "root requis"; exit 1; }
+
+cleanup() { [ -n "$MNT" ] && [ "$MNT" != "/" ] && umount -R "$MNT" 2>/dev/null; }
+
+# Exécute DANS la cible : chroot si elle est montée, directement si c'est nous.
+in_target() {
+ if [ -n "$SELF" ]; then "$@"; else arch-chroot "$MNT" "$@"; fi
+}
+
+# ── Moteur ───────────────────────────────────────────────────────────────────
+
+scan_installs() {
+ vgscan --mknodes >/dev/null 2>&1
+ vgchange -ay >/dev/null 2>&1
+ udevadm settle --timeout=10 >/dev/null 2>&1
+ # Le système en cours d'exécution d'abord (app lancée depuis l'OS installé).
+ if [ ! -d /run/archiso ] && grep -qiE '^(ID|NAME)=.*fws' /etc/os-release 2>/dev/null; then
+ printf '%s|%s (ce système)\n' "$ROOTSRC" \
+ "$(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-FWS}")"
+ fi
+ mkdir -p "$MNT"
+ local dev name
+ while read -r dev; do
+ [ -b "$dev" ] || continue
+ [ "$dev" = "$ROOTSRC" ] && continue # déjà listé ci-dessus
+ mount -o ro "$dev" "$MNT" 2>/dev/null || continue
+ if [ -f "$MNT/etc/os-release" ] && [ -d "$MNT/boot" ] \
+ && grep -qiE '^(ID|NAME)=.*fws' "$MNT/etc/os-release" 2>/dev/null; then
+ name="$(. "$MNT/etc/os-release" 2>/dev/null; echo "${PRETTY_NAME:-FWS}")"
+ printf '%s|%s\n' "$dev" "$name"
+ fi
+ umount "$MNT" 2>/dev/null
+ done < <(lsblk -rpno NAME,TYPE | awk '$2=="part"||$2=="lvm"{print $1}')
+}
+
+# Monte la cible + /boot + ESP d'après son fstab.
+# Cas particulier : la cible EST la racine en cours d'exécution (app lancée
+# depuis l'OS installé, ou entrée GRUB « Réparer FWS ») → tout est déjà là,
+# on opère en place (MNT="" : les chemins "$MNT/…" pointent sur la vraie racine).
+mount_target() {
+ ROOTDEV="$1"
+ if [ "$ROOTDEV" = "$ROOTSRC" ] && [ ! -d /run/archiso ]; then
+ SELF=1; MNT=""
+ echo " cible = système en cours d'exécution (pas de montage)"
+ return 0
+ fi
+ mkdir -p "$MNT"
+ mount "$ROOTDEV" "$MNT" || return 1
+ local mp src
+ for mp in /boot /boot/efi; do
+ src="$(awk -v mp="$mp" '$2==mp && $1!~/^#/{print $1; exit}' "$MNT/etc/fstab" 2>/dev/null)"
+ [ -n "$src" ] || continue
+ mount "$(findfs "$src" 2>/dev/null || echo "$src")" "$MNT$mp" 2>/dev/null \
+ && echo " $mp monté" || warn "$mp non monté (on continue)"
+ done
+ return 0
+}
+
+fix_grub() {
+ say "Réinstallation de GRUB…"
+ if [ -d /sys/firmware/efi ]; then
+ in_target grub-install --target=x86_64-efi \
+ --efi-directory=/boot/efi --bootloader-id=FWS --recheck || true
+ in_target grub-install --target=x86_64-efi \
+ --efi-directory=/boot/efi --bootloader-id=FWS --removable --recheck
+ else
+ local disk
+ disk="$(lsblk -spno NAME,TYPE "$ROOTDEV" 2>/dev/null \
+ | awk '$2=="disk"{print $1; exit}' | grep -oE '/dev/[a-z0-9]+')"
+ [ -n "$disk" ] || { warn "disque BIOS introuvable"; return 1; }
+ in_target grub-install --target=i386-pc --recheck "$disk"
+ fi
+ in_target grub-mkconfig -o /boot/grub/grub.cfg
+}
+
+fix_initramfs() {
+ say "Régénération de l'initramfs (mkinitcpio -P)…"
+ echo " NB : les avertissements « Possibly missing firmware for module … » sont"
+ echo " NORMAUX sur Arch : firmware de matériel exotique (cartes SCSI/fibre…)"
+ echo " non fourni par linux-firmware. Sans effet, sauf si tu AS ce matériel."
+ in_target mkinitcpio -P
+}
+
+fix_pacman() {
+ say "Réparation de pacman…"
+ rm -f "$MNT/var/lib/pacman/db.lck" && echo " verrou db.lck retiré (s'il existait)"
+ in_target pacman-key --init
+ in_target pacman-key --populate archlinux
+ if [ -n "$DO_UPDATE" ]; then
+ say "Mise à jour complète (pacman -Syu)…"
+ [ -z "$SELF" ] && cp -f /etc/resolv.conf "$MNT/etc/resolv.conf" 2>/dev/null
+ in_target pacman -Syu --noconfirm
+ fi
+}
+
+fix_all() { fix_initramfs && fix_grub; }
+
+# ── Sous-commandes (pilotées par fws-recovery-gui) ───────────────────────────
+case "${1:-}" in
+ scan)
+ scan_installs
+ exit 0 ;;
+ repair)
+ action="${2:?action manquante}"; dev="${3:?périphérique manquant}"
+ trap cleanup EXIT
+ say "Montage de $dev…"
+ mount_target "$dev" || { warn "montage impossible"; exit 1; }
+ rc=0
+ case "$action" in
+ grub) fix_grub || rc=$? ;;
+ initramfs) fix_initramfs || rc=$? ;;
+ pacman) fix_pacman || rc=$? ;;
+ pacman-update) DO_UPDATE=1; fix_pacman || rc=$? ;;
+ all) fix_all || rc=$? ;;
+ *) warn "action inconnue : $action"; rc=2 ;;
+ esac
+ cleanup; trap - EXIT
+ [ "$rc" -eq 0 ] && say "Terminé ✔" || say "Échec (code $rc)"
+ exit "$rc" ;;
+ shell)
+ dev="${2:?périphérique manquant}"
+ trap cleanup EXIT
+ mount_target "$dev" || { warn "montage impossible"; exit 1; }
+ if [ -z "$SELF" ] && [ -n "${DISPLAY:-}" ] && command -v xterm >/dev/null 2>&1; then
+ xterm -title "FWS — terminal dans le système (chroot)" \
+ -e arch-chroot "$MNT" /bin/bash
+ else
+ in_target /bin/bash
+ fi
+ cleanup; trap - EXIT
+ exit 0 ;;
+ ""|tui) : ;; # assistant console ci-dessous
+ *) echo "usage : fws-recovery [scan|repair |shell ]"; exit 2 ;;
+esac
+
+# ── Assistant CONSOLE (repli sans X / usage manuel) ──────────────────────────
+trap cleanup EXIT
+say "Recherche des installations FWS…"
+mapfile -t CANDIDATES < <(scan_installs)
+
+if [ "${#CANDIDATES[@]}" -eq 0 ]; then
+ warn "Aucune installation FWS trouvée. Disques visibles :"
+ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS
+ read -rp "Entrée pour ouvrir un shell (exit pour redémarrer). " _
+ bash; exit 0
+fi
+
+echo; i=1
+for c in "${CANDIDATES[@]}"; do echo " $i) ${c%%|*} — ${c##*|}"; i=$((i+1)); done
+read -rp "Installation à réparer [1] : " pick; pick="${pick:-1}"
+dev="${CANDIDATES[$((pick-1))]%%|*}"
+[ -n "$dev" ] || exit 1
+say "Montage de $dev…"
+mount_target "$dev" || { warn "montage impossible"; exit 1; }
+
+while :; do
+ cat <<'MENU'
+
+ ╔════════════════════════════════════════════════════════╗
+ ║ FWS — Réparation du système ║
+ ╠════════════════════════════════════════════════════════╣
+ ║ 1) Réinstaller GRUB (le système ne démarre plus) ║
+ ║ 2) Régénérer l'initramfs (mkinitcpio -P) ║
+ ║ 3) Réparer pacman (verrou, clés, mise à jour) ║
+ ║ 4) Tout réparer (initramfs + GRUB) ║
+ ║ 5) Ouvrir un shell DANS le système (chroot) ║
+ ║ q) Quitter et redémarrer ║
+ ╚════════════════════════════════════════════════════════╝
+MENU
+ read -rp " Choix : " choice
+ case "$choice" in
+ 1) fix_grub ;;
+ 2) fix_initramfs ;;
+ 3) DO_UPDATE=""
+ read -rp " Mise à jour complète aussi (réseau) ? [o/N] " _u
+ case "$_u" in o|O|oui) DO_UPDATE=1 ;; esac
+ fix_pacman ;;
+ 4) fix_all ;;
+ 5) say "Shell chroot — « exit » pour revenir au menu."
+ in_target /bin/bash ;;
+ q|Q) break ;;
+ *) warn "choix inconnu" ;;
+ esac
+done
+
+cleanup; trap - EXIT
+say "Réparation terminée."
+read -rp "Redémarrer maintenant ? [O/n] " _r
+case "$_r" in n|N) bash ;; *) systemctl reboot ;; esac
diff --git a/configs/releng/airootfs/usr/local/bin/fws-recovery-gui b/configs/releng/airootfs/usr/local/bin/fws-recovery-gui
new file mode 100755
index 0000000..cd46998
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-recovery-gui
@@ -0,0 +1,269 @@
+#!/usr/bin/env python3
+# FWS — application GRAPHIQUE de réparation du système (mode « Réparer FWS »
+# du live). GTK3 : le live embarque python-gobject/gtk3 via Anaconda.
+#
+# Pilote le MOTEUR fws-recovery en sous-commandes :
+# fws-recovery scan → détecte les installations FWS
+# fws-recovery repair → répare (sortie streamée ici)
+# fws-recovery shell → terminal chroot (xterm)
+#
+# Deux contextes de lancement :
+# • mode « Réparer FWS » (live OU entrée GRUB de l'OS installé) : session
+# root via fws-recovery-session → le moteur est appelé directement ;
+# • menu d'applications de l'OS installé : lancée en UTILISATEUR → chaque
+# appel du moteur passe par pkexec (politique org.fws.recovery,
+# auth_admin_keep : un seul mot de passe pour toute la session).
+
+import os
+import shutil
+import subprocess
+import sys
+
+import gi
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import GLib, Gtk, Pango # noqa: E402
+
+GLib.set_prgname("fws-recovery-gui")
+
+_ENGINE = "/usr/local/bin/fws-recovery"
+
+
+def _engine_cmd(args):
+ """Commande moteur, élevée via pkexec si on n'est pas root."""
+ cmd = [_ENGINE] + args
+ if os.geteuid() != 0:
+ cmd = ["pkexec"] + cmd
+ return cmd
+
+_ACTIONS = [
+ ("grub", "Réinstaller GRUB",
+ "le système ne démarre plus / « Operating System not found »"),
+ ("initramfs", "Régénérer l'initramfs",
+ "kernel panic au démarrage, module manquant (mkinitcpio -P)"),
+ ("pacman", "Réparer pacman",
+ "verrou bloqué, clés de signature cassées"),
+ ("pacman-update", "Réparer pacman + mise à jour complète",
+ "idem, puis pacman -Syu (réseau requis)"),
+ ("all", "Tout réparer",
+ "initramfs puis GRUB — le réflexe « ça ne boote plus »"),
+]
+
+
+class RecoveryWindow(Gtk.Window):
+ def __init__(self):
+ super().__init__(title="FWS — Réparation du système")
+ self.set_icon_name("fws-recovery")
+ self.set_default_size(720, 560)
+ self.set_position(Gtk.WindowPosition.CENTER)
+ self.set_border_width(18)
+ self.connect("destroy", Gtk.main_quit)
+
+ self._proc = None
+ self._installs = [] # [(dev, nom), …]
+ self._action_buttons = []
+
+ root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
+ self.add(root)
+
+ title = Gtk.Label()
+ title.set_markup("Réparation du système FWS")
+ root.pack_start(title, False, False, 0)
+
+ self._status = Gtk.Label(label="Recherche des installations FWS sur les disques…")
+ self._status.set_xalign(0)
+ root.pack_start(self._status, False, False, 0)
+
+ # Sélecteur d'installation (rempli après le scan).
+ picker_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
+ picker_row.pack_start(Gtk.Label(label="Système à réparer :"), False, False, 0)
+ self._picker = Gtk.ComboBoxText()
+ picker_row.pack_start(self._picker, True, True, 0)
+ root.pack_start(picker_row, False, False, 0)
+
+ # Boutons d'action (style fws-hello : titre + description).
+ for key, label, desc in _ACTIONS:
+ button = Gtk.Button()
+ inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
+ lab = Gtk.Label()
+ lab.set_markup(f"{label}")
+ lab.set_xalign(0)
+ sub = Gtk.Label(label=desc)
+ sub.set_xalign(0)
+ sub.get_style_context().add_class("dim-label")
+ inner.pack_start(lab, False, False, 0)
+ inner.pack_start(sub, False, False, 0)
+ button.add(inner)
+ button.connect("clicked", self._on_action, key)
+ root.pack_start(button, False, False, 0)
+ self._action_buttons.append(button)
+
+ shell_btn = Gtk.Button(label="Ouvrir un terminal dans le système (chroot)")
+ shell_btn.connect("clicked", self._on_shell)
+ root.pack_start(shell_btn, False, False, 0)
+ self._action_buttons.append(shell_btn)
+
+ # Journal (sortie du moteur, en continu).
+ frame = Gtk.Frame(label="Journal")
+ scroll = Gtk.ScrolledWindow()
+ scroll.set_min_content_height(150)
+ self._log = Gtk.TextView()
+ self._log.set_editable(False)
+ self._log.set_cursor_visible(False)
+ self._log.set_monospace(True)
+ self._log.modify_font(Pango.FontDescription("Monospace 9"))
+ scroll.add(self._log)
+ frame.add(scroll)
+ root.pack_start(frame, True, True, 0)
+
+ # Pied : redémarrer / éteindre.
+ footer = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
+ reboot = Gtk.Button(label="Redémarrer")
+ reboot.connect("clicked", lambda _b: self._power("reboot"))
+ poweroff = Gtk.Button(label="Éteindre")
+ poweroff.connect("clicked", lambda _b: self._power("poweroff"))
+ footer.pack_end(reboot, False, False, 0)
+ footer.pack_end(poweroff, False, False, 0)
+ root.pack_start(footer, False, False, 0)
+
+ self._set_busy(True)
+ GLib.idle_add(self._start_scan)
+
+ # ── Utilitaires ─────────────────────────────────────────────────────────
+ def _append(self, text):
+ # Rebranding d'affichage : les outils sous-jacents parlent
+ # d'« Arch Linux » — pas le journal FWS. (Les identifiants techniques
+ # comme archlinux-keyring restent tels quels : ce sont de vrais noms
+ # de fichiers/paquets, les masquer fausserait le diagnostic.)
+ text = text.replace("Arch Linux", "FWS").replace("Fedora", "FWS")
+ buf = self._log.get_buffer()
+ buf.insert(buf.get_end_iter(), text)
+ mark = buf.create_mark(None, buf.get_end_iter(), False)
+ self._log.scroll_mark_onscreen(mark)
+ buf.delete_mark(mark)
+
+ def _set_busy(self, busy):
+ for b in self._action_buttons:
+ b.set_sensitive(not busy and bool(self._installs))
+ # Sensible même avec UNE seule install : un combo grisé donne
+ # l'impression que TOUTE l'app est désactivée (retour utilisateur).
+ self._picker.set_sensitive(not busy and bool(self._installs))
+
+ def _selected_dev(self):
+ idx = self._picker.get_active()
+ if 0 <= idx < len(self._installs):
+ return self._installs[idx][0]
+ return None
+
+ def _power(self, what):
+ subprocess.run(["systemctl", what], check=False)
+
+ # ── Scan des installations ──────────────────────────────────────────────
+ def _start_scan(self):
+ self._run_engine(["scan"], self._scan_done, quiet=True)
+ return False
+
+ def _scan_done(self, rc, output):
+ self._installs = []
+ for line in output.splitlines():
+ if "|" in line:
+ dev, _, name = line.partition("|")
+ self._installs.append((dev.strip(), name.strip()))
+ self._picker.append_text(f"{name.strip()} — {dev.strip()}")
+ if self._installs:
+ self._picker.set_active(0)
+ n = len(self._installs)
+ self._status.set_markup(
+ f"{n} installation{'s' if n > 1 else ''} FWS détectée{'s' if n > 1 else ''}."
+ " Choisis une réparation :")
+ elif rc in (126, 127):
+ # pkexec : authentification annulée ou refusée.
+ self._status.set_markup(
+ "Authentification annulée. Ferme puis relance"
+ " l'application pour réessayer.")
+ else:
+ self._status.set_markup(
+ "Aucune installation FWS détectée. Vérifie les disques"
+ " (terminal ci-dessous) ou redémarre.")
+ self._append("Aucune installation FWS trouvée.\n\n"
+ + subprocess.run(["lsblk", "-o", "NAME,SIZE,TYPE,FSTYPE"],
+ capture_output=True, text=True,
+ check=False).stdout)
+ self._set_busy(False)
+ # Le terminal reste utile même sans installation détectée (diagnostic).
+ self._action_buttons[-1].set_sensitive(True)
+
+ # ── Exécution du moteur avec sortie en continu ──────────────────────────
+ def _run_engine(self, args, done_cb, quiet=False):
+ self._set_busy(True)
+ self._proc = subprocess.Popen(
+ _engine_cmd(args), stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, text=True, bufsize=1)
+ chunks = []
+
+ def on_io(source, _cond):
+ line = source.readline()
+ if line:
+ chunks.append(line)
+ if not quiet:
+ self._append(line)
+ return True
+ rc = self._proc.wait()
+ done_cb(rc, "".join(chunks))
+ return False
+
+ GLib.io_add_watch(self._proc.stdout,
+ GLib.IO_IN | GLib.IO_HUP, on_io)
+
+ def _on_action(self, _button, key):
+ dev = self._selected_dev()
+ if not dev:
+ return
+ label = next(lab for k, lab, _d in _ACTIONS if k == key)
+ self._status.set_markup(f"{label} sur {dev} — en cours…")
+ self._append(f"\n───── {label} ({dev}) ─────\n")
+ self._run_engine(["repair", key, dev], self._action_done)
+
+ def _action_done(self, rc, _output):
+ if rc == 0:
+ self._status.set_markup("Terminé ✔ — autre réparation, ou redémarre.")
+ elif rc in (126, 127):
+ self._status.set_markup(
+ "Authentification annulée — rien n'a été modifié.")
+ else:
+ self._status.set_markup(
+ f"Échec (code {rc}) — détail dans le journal ci-dessous.")
+ self._set_busy(False)
+
+ def _on_shell(self, _button):
+ dev = self._selected_dev()
+ if dev and os.geteuid() == 0:
+ subprocess.Popen([_ENGINE, "shell", dev])
+ elif dev:
+ # pkexec purge DISPLAY → le moteur ouvre le chroot DANS cet xterm
+ # (lancé en utilisateur, donc visible sur X11 comme Wayland).
+ subprocess.Popen(["xterm", "-title",
+ "FWS — terminal dans le système (chroot)",
+ "-e", "pkexec", _ENGINE, "shell", dev])
+ elif shutil.which("xterm"):
+ subprocess.Popen(["xterm", "-title", "FWS — terminal"])
+
+
+def main():
+ if not Gtk.init_check()[0]:
+ # Pas d'affichage : le .zprofile retombera sur l'assistant console.
+ return 1
+ if os.geteuid() != 0 and not shutil.which("pkexec"):
+ dlg = Gtk.MessageDialog(message_type=Gtk.MessageType.ERROR,
+ buttons=Gtk.ButtonsType.CLOSE,
+ text="pkexec introuvable : lance fws-recovery-gui en root.")
+ dlg.run()
+ return 1
+ win = RecoveryWindow()
+ win.show_all()
+ Gtk.main()
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/configs/releng/airootfs/usr/local/bin/fws-recovery-session b/configs/releng/airootfs/usr/local/bin/fws-recovery-session
new file mode 100755
index 0000000..56ec69d
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-recovery-session
@@ -0,0 +1,8 @@
+#!/bin/sh
+# FWS — session X du mode réparation. Lancé par startx (cf. /root/.zprofile,
+# entrée de boot « Réparer FWS »). Openbox minimal + l'application GTK
+# fws-recovery-gui. Si la GUI meurt, openbox reste pour un xterm de secours.
+xsetroot -solid "#1a1a2e" 2>/dev/null
+openbox &
+fws-recovery-gui
+exec openbox
diff --git a/configs/releng/airootfs/usr/local/bin/fws-setup-hardware b/configs/releng/airootfs/usr/local/bin/fws-setup-hardware
new file mode 100755
index 0000000..526f631
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-setup-hardware
@@ -0,0 +1,84 @@
+#!/bin/bash
+# ============================================================================
+# FWS — détection matérielle + drivers + stack « remplacement de Windows ».
+#
+# Appelé par le %post du kickstart : fws-setup-hardware
+# ( = cli|gnome|kde|xfce|hyprland). Réseau requis ; CHAQUE étape est
+# NON bloquante (pas de set -e) : un paquet manquant ou pas de réseau ne casse
+# pas l'installation.
+#
+# Installe :
+# - les drivers GPU détectés via lspci (Intel / AMD / NVIDIA) + Mesa/Vulkan,
+# - les outils invité selon l'hyperviseur (VMware/VirtualBox/QEMU/Hyper-V),
+# - le son/micro/casque (PipeWire), le Bluetooth (BlueZ), les conteneurs
+# (Podman),
+# - et — si un bureau est choisi — la stack « comme Windows » : Steam, Wine,
+# VR (Monado/OpenXR → SteamVR pour HTC Vive / Valve Index ; Oculus n'a pas
+# de support Linux officiel), winboat-bin (apps Windows).
+# ============================================================================
+DESK="${1:-cli}"
+log() { echo "[FWS-hw] $*"; }
+
+# ── 1) Drivers GPU (détection lspci) ─────────────────────────────────────────
+DRIVERS="mesa"
+GPU="$(lspci 2>/dev/null | grep -iE 'VGA compatible controller|3D controller|Display controller')"
+case "$GPU" in
+ *NVIDIA*|*GeForce*) DRIVERS="$DRIVERS nvidia-dkms nvidia-utils linux-headers dkms" ;;
+ *AMD*|*ATI*|*Radeon*) DRIVERS="$DRIVERS vulkan-radeon libva-mesa-driver" ;;
+ *Intel*) DRIVERS="$DRIVERS vulkan-intel intel-media-driver" ;;
+esac
+
+# ── 2) Outils invité selon l'hyperviseur ─────────────────────────────────────
+VMSVC=""
+case "$(systemd-detect-virt 2>/dev/null)" in
+ vmware) DRIVERS="$DRIVERS open-vm-tools"; VMSVC="vmtoolsd vmware-vmblock-fuse" ;;
+ oracle) DRIVERS="$DRIVERS virtualbox-guest-utils"; VMSVC="vboxservice" ;;
+ qemu|kvm) DRIVERS="$DRIVERS qemu-guest-agent spice-vdagent"; VMSVC="qemu-guest-agent spice-vdagentd" ;;
+ microsoft) DRIVERS="$DRIVERS hyperv"; VMSVC="hv_fcopy_daemon hv_kvp_daemon hv_vss_daemon" ;;
+esac
+
+# ── 3) Stack de base : son / micro / casque, Bluetooth, luminosité, conteneurs
+# brightnessctl : réglage de luminosité sans root (via logind) — consommé par
+# les touches XF86MonBrightness* et la molette du module backlight de waybar.
+BASE="pipewire pipewire-alsa pipewire-pulse pipewire-jack wireplumber \
+ pavucontrol bluez bluez-utils brightnessctl podman"
+# Gestionnaire Bluetooth graphique : KDE et GNOME ont le leur ; blueman pour
+# les bureaux qui n'en fournissent pas (tray + assistant d'appairage).
+case "$DESK" in
+ hyprland|xfce|i3) BASE="$BASE blueman" ;;
+esac
+
+# ── 4) Stack desktop : jeux, Windows-compat, VR (si un bureau est choisi) ────
+DESKTOP=""
+if [ "$DESK" != "cli" ]; then
+ # multilib requis (Steam / Wine 32 bits).
+ grep -qE '^\[multilib\]' /etc/pacman.conf \
+ || printf '\n[multilib]\nInclude = /etc/pacman.d/mirrorlist\n' >> /etc/pacman.conf
+ # mangohud : overlay FPS/perfs en jeu (mangohud %command% dans Steam).
+ DESKTOP="steam wine winetricks wine-mono wine-gecko \
+ lib32-mesa lib32-pipewire monado gamemode \
+ mangohud lib32-mangohud \
+ noto-fonts ttf-liberation"
+fi
+
+# ── Sync + filtre aux paquets RÉELLEMENT existants ───────────────────────────
+# (un nom de paquet manquant/renommé ne doit pas faire échouer tout le reste).
+pacman -Sy --noconfirm || log "synchro pacman échouée (réseau ?)"
+VALID=""
+for p in $DRIVERS $BASE $DESKTOP; do
+ pacman -Si "$p" >/dev/null 2>&1 && VALID="$VALID $p"
+done
+if [ -n "$VALID" ]; then
+ log "installation :$VALID"
+ pacman -S --noconfirm --needed $VALID || log "certains paquets non installés"
+fi
+
+# NB : winboat-bin (AUR) n'est PAS installé ici (fws-local n'existe pas sur la
+# cible) : il est intégré à l'image FWS via packages.x86_64 et recopié par le
+# payload LiveOS d'Anaconda → déjà présent sur le système installé.
+
+# ── Services matériels ───────────────────────────────────────────────────────
+[ -n "$VMSVC" ] && systemctl enable $VMSVC 2>/dev/null
+systemctl enable bluetooth.service 2>/dev/null
+systemctl enable podman.socket 2>/dev/null
+exit 0
diff --git a/configs/releng/airootfs/usr/local/bin/fws-update b/configs/releng/airootfs/usr/local/bin/fws-update
new file mode 100755
index 0000000..c8acd4a
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-update
@@ -0,0 +1,33 @@
+#!/bin/bash
+# FWS — mise à jour du système en une commande (bouton de la barre, fws-hello,
+# ou terminal). pacman -Syu + nettoyage du cache + rapport des orphelins.
+set -u
+say() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
+
+if [ "$(id -u)" -ne 0 ]; then
+ exec sudo "$0" "$@"
+fi
+
+say "Mise à jour du système (pacman -Syu)…"
+pacman -Syu || { echo "Échec de la mise à jour (réseau ?)"; read -rp "Entrée pour fermer. " _; exit 1; }
+
+if command -v paccache >/dev/null 2>&1; then
+ say "Nettoyage du cache des paquets (2 dernières versions gardées)…"
+ paccache -rk2
+fi
+
+ORPHANS="$(pacman -Qtdq 2>/dev/null)"
+if [ -n "$ORPHANS" ]; then
+ say "Paquets orphelins (plus requis par rien) :"
+ echo "$ORPHANS"
+ read -rp "Les supprimer ? [o/N] " _rm
+ case "$_rm" in o|O|oui) echo "$ORPHANS" | pacman -Rns --noconfirm - ;; esac
+fi
+
+if command -v flatpak >/dev/null 2>&1; then
+ say "Mise à jour des Flatpaks…"
+ flatpak update -y || true
+fi
+
+say "Système à jour ✔"
+read -rp "Entrée pour fermer. " _
diff --git a/configs/releng/airootfs/usr/local/bin/fws-updates-check b/configs/releng/airootfs/usr/local/bin/fws-updates-check
new file mode 100755
index 0000000..860430a
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-updates-check
@@ -0,0 +1,8 @@
+#!/bin/sh
+# FWS — indicateur « mises à jour disponibles » pour waybar/polybar.
+# checkupdates (pacman-contrib) : lit les dépôts SANS toucher la base pacman.
+# Sortie vide = rien à faire → le module de la barre se cache.
+command -v checkupdates >/dev/null 2>&1 || exit 0
+n="$(checkupdates 2>/dev/null | wc -l)"
+[ "$n" -gt 0 ] && printf ' %s\n' "$n"
+exit 0
diff --git a/configs/releng/airootfs/usr/local/bin/fws-vr-setup b/configs/releng/airootfs/usr/local/bin/fws-vr-setup
new file mode 100755
index 0000000..34beca4
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-vr-setup
@@ -0,0 +1,115 @@
+#!/bin/bash
+# ============================================================================
+# FWS — installation des drivers/runtimes VR selon le casque.
+#
+# À lancer depuis un terminal (Super+Shift+V sous i3, ou l'entrée
+# « Installation VR (FWS) » du menu d'applications). Réservé aux sessions
+# X11 (i3) : SteamVR ne fonctionne PAS sous Wayland/Hyprland.
+#
+# Réalité du support Linux (résumé honnête) :
+# • HTC Vive / Vive Pro / Valve Index (stations lighthouse) → NATIF via
+# SteamVR. C'est le chemin le plus fiable.
+# • Meta/Oculus Quest 1-3, Pico → PAS de support officiel ; streaming
+# Wi-Fi via ALVR (+ SteamVR). ALVR n'est pas dans les dépôts Arch :
+# le script installe la base et donne la marche à suivre.
+# • Oculus Rift CV1/S (filaire) → pas de support utilisable.
+# • Monado : runtime OpenXR libre (déjà proposé par fws-setup-hardware),
+# utile pour les applis OpenXR natives sans SteamVR.
+# ============================================================================
+set -u
+say() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
+warn() { printf '\033[1;33m ⚠ %s\033[0m\n' "$*"; }
+
+if [ "$(id -u)" -ne 0 ]; then
+ exec sudo "$0" "$@"
+fi
+
+# VM = cul-de-sac VR : pas d'accès direct au GPU ni à l'USB/DisplayPort du
+# casque, et Steam (steamwebhelper) est INSTABLE en rendu logiciel (llvmpipe).
+# On prévient au lieu de laisser l'utilisateur croire à une panne de FWS.
+if systemd-detect-virt --quiet 2>/dev/null; then
+ warn "Machine VIRTUELLE détectée ($(systemd-detect-virt 2>/dev/null))."
+ warn "La VR ne fonctionnera PAS ici : casque et GPU inaccessibles depuis une VM."
+ warn "Steam lui-même peut planter en rendu logiciel (steamwebhelper)."
+ echo " → Fais cette installation sur la machine RÉELLE avec le casque branché."
+ read -rp " Continuer quand même (tests uniquement) ? [o/N] " _ok
+ case "$_ok" in o|O|oui) ;; *) exit 0 ;; esac
+fi
+
+# lib32 Vulkan selon le GPU (indispensable à SteamVR, qui est 32/64 bits).
+GPU="$(lspci 2>/dev/null | grep -iE 'VGA compatible controller|3D controller|Display controller')"
+LIB32="lib32-mesa"
+case "$GPU" in
+ *NVIDIA*|*GeForce*) LIB32="$LIB32 lib32-nvidia-utils" ;;
+ *AMD*|*ATI*|*Radeon*) LIB32="$LIB32 lib32-vulkan-radeon" ;;
+ *Intel*) LIB32="$LIB32 lib32-vulkan-intel" ;;
+esac
+
+install_pkgs() {
+ # multilib requis pour Steam/lib32.
+ grep -qE '^\[multilib\]' /etc/pacman.conf \
+ || printf '\n[multilib]\nInclude = /etc/pacman.d/mirrorlist\n' >> /etc/pacman.conf
+ pacman -Sy --noconfirm || { warn "synchro pacman échouée (réseau ?)"; return 1; }
+ local valid="" p
+ for p in "$@"; do
+ pacman -Si "$p" >/dev/null 2>&1 && valid="$valid $p" || warn "paquet inconnu ignoré : $p"
+ done
+ # shellcheck disable=SC2086
+ pacman -S --noconfirm --needed $valid
+}
+
+steamvr_base() {
+ say "Installation de la base SteamVR (Steam + Vulkan 32 bits : $LIB32)…"
+ # ttf-liberation : polices attendues par l'interface Steam (évite des
+ # rendus cassés/plantages cosmétiques sur install minimale).
+ # shellcheck disable=SC2086
+ install_pkgs steam $LIB32 ttf-liberation || return 1
+ say "Base installée. Étapes suivantes :"
+ echo " 1. Lance Steam, connecte-toi, puis installe « SteamVR » (Bibliothèque → Outils)."
+ echo " 2. Branche le casque et les stations, puis lance SteamVR."
+ echo " 3. Premier lancement : SteamVR peut demander à ajuster les règles udev — accepte."
+ say "Si Steam PLANTE (fenêtre qui se ferme, souvent steamwebhelper) :"
+ echo " • Vérifie l'espace disque (SteamVR ≈ 5 Go) : df -h ~"
+ echo " • GPU faible / VM / rendu logiciel — lance :"
+ echo " steam -cef-disable-gpu -cef-disable-gpu-compositing"
+ echo " • Pour voir l'erreur exacte : lance « steam » depuis un terminal,"
+ echo " ou regarde ~/.steam/steam/logs/ (console_log.txt, bootstrap_log.txt)."
+}
+
+case "${1:-menu}" in
+ menu)
+ cat <<'MENU'
+
+ ╔══════════════════════════════════════════════════════════╗
+ ║ FWS — Installation VR (session X11 requise) ║
+ ╠══════════════════════════════════════════════════════════╣
+ ║ 1) HTC Vive / Vive Pro / Valve Index (natif, SteamVR) ║
+ ║ 2) Meta Quest / Pico (streaming ALVR) ║
+ ║ 3) Monado (runtime OpenXR libre, sans SteamVR) ║
+ ║ q) Quitter ║
+ ╚══════════════════════════════════════════════════════════╝
+MENU
+ read -rp " Choix : " choice
+ exec "$0" "$choice"
+ ;;
+ 1|vive|index)
+ steamvr_base || exit 1
+ ;;
+ 2|quest|pico)
+ steamvr_base || exit 1
+ say "Streaming Quest/Pico : il faut AUSSI ALVR (pas dans les dépôts Arch) :"
+ echo " 1. Télécharge le .tar.gz « ALVR Launcher » : https://github.com/alvr-org/ALVR/releases"
+ echo " 2. Sur le casque, installe le client ALVR (App Lab / SideQuest)."
+ echo " 3. Lance ALVR sur le PC (même Wi-Fi 5 GHz que le casque) — il pilote SteamVR."
+ ;;
+ 3|monado)
+ say "Installation de Monado (OpenXR) + outils…"
+ install_pkgs monado || exit 1
+ say "Monado installé. Les applis OpenXR le trouvent via /usr/share/openxr/."
+ ;;
+ q|Q) exit 0 ;;
+ *) warn "choix inconnu : ${1}"; exec "$0" menu ;;
+esac
+
+echo
+read -rp "Terminé — appuie sur Entrée pour fermer. " _
diff --git a/configs/releng/airootfs/usr/local/share/applications/fws-recovery.desktop b/configs/releng/airootfs/usr/local/share/applications/fws-recovery.desktop
new file mode 100644
index 0000000..dce1081
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/share/applications/fws-recovery.desktop
@@ -0,0 +1,20 @@
+[Desktop Entry]
+# FWS — lanceur de l'application de réparation dans l'OS INSTALLÉ (copié tel
+# quel du live). L'app tourne en utilisateur ; chaque appel du moteur passe
+# par pkexec (cf. org.fws.recovery.policy).
+Type=Application
+Name=Récupération FWS
+Name[en]=FWS Recovery
+GenericName=Réparation du système
+GenericName[en]=System repair
+Comment=Réparer GRUB, l'initramfs ou pacman — le point de restauration FWS
+Comment[en]=Repair GRUB, the initramfs or pacman — the FWS restore point
+Exec=/usr/local/bin/fws-recovery-gui
+Icon=fws-recovery
+# Associe la fenêtre (WM_CLASS via GLib.set_prgname) à ce lanceur → bonne
+# icône dans la barre de titre / barre des tâches (sinon icône générique).
+StartupWMClass=fws-recovery-gui
+Terminal=false
+Categories=System;
+Keywords=recovery;réparation;restauration;grub;boot;secours;dépannage;
+Keywords[en]=recovery;repair;restore;grub;boot;rescue;
diff --git a/configs/releng/airootfs/usr/local/share/applications/xfce4-about.desktop b/configs/releng/airootfs/usr/local/share/applications/xfce4-about.desktop
new file mode 100644
index 0000000..74b6c36
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/share/applications/xfce4-about.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+# FWS — masque « À propos de Xfce » du menu d'applications. Le binaire
+# xfce4-about vient de libxfce4ui (dépendance de Thunar) : il n'implique
+# AUCUNE session XFCE. /usr/local/share étant prioritaire sur /usr/share
+# dans XDG_DATA_DIRS, cette entrée NoDisplay masque celle du paquet partout.
+Type=Application
+Name=About Xfce
+Exec=xfce4-about
+NoDisplay=true
diff --git a/configs/releng/airootfs/usr/local/share/fws/hyprland.conf.tmpl b/configs/releng/airootfs/usr/local/share/fws/hyprland.conf.tmpl
new file mode 100644
index 0000000..52487d8
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/share/fws/hyprland.conf.tmpl
@@ -0,0 +1,180 @@
+# FWS — configuration Hyprland par défaut.
+# Le clavier (__KB_LAYOUT__ / __KB_VARIANT__) est injecté à l'installation,
+# depuis le choix fait dans Anaconda. Édite ce fichier librement.
+#
+# Raccourcis : transposition de la config i3/sway FWS (mêmes touches).
+# Couleurs : nuances de bleu (palette Catppuccin Mocha).
+#
+# Les raccourcis sont déclarés avec « bindd » (bind + description) : la
+# description alimente l'écran « Raccourcis clavier » (bouton ⌨ de la barre
+# ou SUPER+K → fws-keys, qui lit les binds RÉELS via hyprctl). Ajoute
+# tes propres bindd : ils apparaîtront automatiquement dans la liste.
+# ⚠ Pas de virgule dans les descriptions (séparateur de champs Hyprland).
+
+monitor = , preferred, auto, 1
+
+input {
+ kb_layout = __KB_LAYOUT__
+ kb_variant = __KB_VARIANT__
+ follow_mouse = 1
+ numlock_by_default = true
+ touchpad {
+ natural_scroll = true
+ tap-to-click = true
+ }
+}
+
+general {
+ gaps_in = 4
+ gaps_out = 8
+ border_size = 2
+ # Nuances de bleu : bleu → sapphire en dégradé sur la fenêtre active.
+ col.active_border = rgb(89b4fa) rgb(74c7ec) 45deg
+ col.inactive_border = rgb(313244)
+ layout = dwindle
+}
+
+decoration {
+ rounding = 8
+ # Flou et ombres COUPÉS : rendu logiciel (llvmpipe) en VM — cf. le
+ # wrapper fws-hyprland écrit par l'installateur. Sur un vrai GPU tu
+ # peux les réactiver (enabled = true).
+ blur {
+ enabled = false
+ }
+ shadow {
+ enabled = false
+ }
+}
+
+animations {
+ enabled = true
+ bezier = fws, 0.22, 1.0, 0.36, 1.0
+ animation = windows, 1, 3, fws
+ animation = workspaces, 1, 3, fws
+ animation = fade, 1, 2, default
+}
+
+misc {
+ disable_hyprland_logo = true
+ disable_splash_rendering = true # pas de « Thanks xxx! » en bas d'écran
+ force_default_wallpaper = 0
+ background_color = 0x1a1a2e # bleu nuit FWS (aucun fond requis)
+}
+
+# Démarrage de session : barre d'état, notifications, réseau dans le tray,
+# agent d'authentification GUI (indispensable pour les apps qui demandent
+# les droits admin : gestionnaire de disques, blueman…).
+exec-once = waybar
+exec-once = mako
+exec-once = nm-applet
+# Applet Bluetooth : seulement si un adaptateur existe (en VM il n'y en a
+# pas → blueman afficherait une erreur « Connection to BlueZ failed »).
+exec-once = sh -c '[ -n "$(ls -A /sys/class/bluetooth 2>/dev/null)" ] && exec blueman-applet'
+exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
+# Fenêtre de bienvenue (1er démarrage seulement — se retire ensuite).
+exec-once = fws-hello --autostart
+
+$mod = SUPER
+
+# --- Applications ---------------------------------------------------------
+bindd = $mod, Return, Ouvrir un terminal (kitty), exec, kitty
+bindd = $mod SHIFT, Return, Terminal flottant, exec, kitty --class floating-term
+bindd = $mod, R, Menu des applications, exec, wofi --show drun
+bindd = $mod, E, Gestionnaire de fichiers, exec, dolphin
+bindd = $mod, B, Ouvrir Firefox, exec, firefox
+bindd = $mod, K, Afficher les raccourcis clavier, exec, fws-keys
+bindd = $mod, X, Menu d'alimentation (arrêt/redémarrage…), exec, fws-powermenu
+
+# --- Fenêtres ---------------------------------------------------------------
+bindd = $mod, Q, Fermer la fenêtre, killactive,
+bindd = $mod, V, Fenêtre flottante ou ancrée, togglefloating,
+bindd = $mod, F, Plein écran, fullscreen,
+bindd = $mod, J, Basculer la découpe horizontale/verticale, layoutmsg, togglesplit
+bindd = $mod, L, Verrouiller l'écran, exec, hyprlock
+bindd = $mod SHIFT, E, Quitter la session (retour à SDDM), exit,
+bindd = $mod SHIFT, C, Recharger la configuration Hyprland, exec, hyprctl reload
+
+# --- Focus (flèches) ---------------------------------------------------------
+bindd = $mod, left, Focus à gauche, movefocus, l
+bindd = $mod, right, Focus à droite, movefocus, r
+bindd = $mod, up, Focus en haut, movefocus, u
+bindd = $mod, down, Focus en bas, movefocus, d
+
+# --- Déplacer la fenêtre (Maj + flèches) ---------------------------------------
+bindd = $mod SHIFT, left, Déplacer la fenêtre à gauche, movewindow, l
+bindd = $mod SHIFT, right, Déplacer la fenêtre à droite, movewindow, r
+bindd = $mod SHIFT, up, Déplacer la fenêtre en haut, movewindow, u
+bindd = $mod SHIFT, down, Déplacer la fenêtre en bas, movewindow, d
+
+# --- Redimensionner (Ctrl + flèches, maintien possible) ------------------------
+binde = $mod CTRL, left, resizeactive, -30 0
+binde = $mod CTRL, right, resizeactive, 30 0
+binde = $mod CTRL, up, resizeactive, 0 -30
+binde = $mod CTRL, down, resizeactive, 0 30
+
+# --- Espaces de travail 1-10 (0 = espace 10, comme i3) --------------------------
+bindd = $mod, 1, Aller à l'espace 1, workspace, 1
+bindd = $mod, 2, Aller à l'espace 2, workspace, 2
+bindd = $mod, 3, Aller à l'espace 3, workspace, 3
+bindd = $mod, 4, Aller à l'espace 4, workspace, 4
+bindd = $mod, 5, Aller à l'espace 5, workspace, 5
+bindd = $mod, 6, Aller à l'espace 6, workspace, 6
+bindd = $mod, 7, Aller à l'espace 7, workspace, 7
+bindd = $mod, 8, Aller à l'espace 8, workspace, 8
+bindd = $mod, 9, Aller à l'espace 9, workspace, 9
+bindd = $mod, 0, Aller à l'espace 10, workspace, 10
+bindd = $mod SHIFT, 1, Envoyer la fenêtre vers l'espace 1, movetoworkspace, 1
+bindd = $mod SHIFT, 2, Envoyer la fenêtre vers l'espace 2, movetoworkspace, 2
+bindd = $mod SHIFT, 3, Envoyer la fenêtre vers l'espace 3, movetoworkspace, 3
+bindd = $mod SHIFT, 4, Envoyer la fenêtre vers l'espace 4, movetoworkspace, 4
+bindd = $mod SHIFT, 5, Envoyer la fenêtre vers l'espace 5, movetoworkspace, 5
+bindd = $mod SHIFT, 6, Envoyer la fenêtre vers l'espace 6, movetoworkspace, 6
+bindd = $mod SHIFT, 7, Envoyer la fenêtre vers l'espace 7, movetoworkspace, 7
+bindd = $mod SHIFT, 8, Envoyer la fenêtre vers l'espace 8, movetoworkspace, 8
+bindd = $mod SHIFT, 9, Envoyer la fenêtre vers l'espace 9, movetoworkspace, 9
+bindd = $mod SHIFT, 0, Envoyer la fenêtre vers l'espace 10, movetoworkspace, 10
+
+# --- Multi-écrans (comme i3 : Super+Ctrl+1/2 = écran gauche/droit) -------------
+bindd = $mod CTRL, 1, Envoyer la fenêtre vers l'écran de gauche, movewindow, mon:l
+bindd = $mod CTRL, 2, Envoyer la fenêtre vers l'écran de droite, movewindow, mon:r
+
+# --- Molette = changer d'espace de travail --------------------------------------
+bindd = $mod, mouse_up, Espace de travail précédent, workspace, e-1
+bindd = $mod, mouse_down, Espace de travail suivant, workspace, e+1
+
+# --- Scratchpad (espace spécial) -------------------------------------------------
+bindd = $mod, S, Afficher le scratchpad, togglespecialworkspace, magic
+bindd = $mod SHIFT, S, Envoyer la fenêtre au scratchpad, movetoworkspace, special:magic
+
+# --- Captures d'écran (grim/slurp → presse-papier ou ~/Images) -------------------
+bindd = , Print, Capture plein écran vers le presse-papier, exec, grim - | wl-copy
+bindd = $mod, Print, Capture d'une zone vers le presse-papier, exec, grim -g "$(slurp)" - | wl-copy
+bindd = $mod SHIFT, Print, Capture d'une zone vers ~/Images, exec, sh -c 'mkdir -p ~/Images && grim -g "$(slurp)" ~/Images/capture_$(date +%Y-%m-%d_%H-%M-%S).png'
+
+# --- Multimédia (touches XF86 : volume via PipeWire, lecture via playerctl) ------
+bindel = , XF86AudioRaiseVolume, exec, wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+
+bindel = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
+bindl = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
+bindl = , XF86AudioPlay, exec, playerctl play-pause
+bindl = , XF86AudioNext, exec, playerctl next
+bindl = , XF86AudioPrev, exec, playerctl previous
+
+# --- Luminosité de l'écran (portables : touches Fn) ------------------------------
+bindel = , XF86MonBrightnessUp, exec, brightnessctl set +5%
+bindel = , XF86MonBrightnessDown, exec, brightnessctl set 5%-
+
+# --- Souris : déplacer / redimensionner les fenêtres -----------------------------
+bindm = $mod, mouse:272, movewindow
+bindm = $mod, mouse:273, resizewindow
+
+# --- Règles de fenêtres (comme i3) ------------------------------------------------
+# Syntaxe Hyprland ≥ 0.55 : champs « » + cibles « match:… »
+# (validé contre le binaire avec --verify-config).
+windowrule = float on, match:class ^(floating-term)$
+windowrule = size 900 550, match:class ^(floating-term)$
+windowrule = float on, match:class ^([Pp]avucontrol|org.pulseaudio.pavucontrol)$
+windowrule = float on, match:class ^(.?[Bb]lueman-manager.*)$
+windowrule = float on, match:class ^([Ff]ws-hello)$
+windowrule = float on, match:class ^([Ff]ws-about)$
+windowrule = float on, match:title ^(Picture-in-Picture)$
diff --git a/configs/releng/airootfs/usr/local/share/fws/i3-config b/configs/releng/airootfs/usr/local/share/fws/i3-config
new file mode 100644
index 0000000..8b6ead8
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/share/fws/i3-config
@@ -0,0 +1,160 @@
+# FWS — configuration i3 par défaut (session X11, COMPATIBLE VR).
+# SteamVR fonctionne en X11 : c'est le bureau à choisir pour un casque VR
+# (HTC Vive, Valve Index…). Lance « fws-vr-setup » (Super+Shift+V, ou menu)
+# pour installer les drivers/runtimes selon ton casque.
+# Le clavier X11 est configuré système via /etc/X11/xorg.conf.d/00-keyboard.conf
+# (écrit par l'installateur selon ton choix Anaconda) : rien à régler ici.
+
+set $mod Mod4
+set $term kitty
+set $filemanager dolphin
+set $menu rofi -show drun -show-icons
+set $browser firefox
+
+# Couleurs — nuances de bleu (Catppuccin Mocha, comme SDDM/waybar FWS)
+set $base #1e1e2e
+set $surface0 #313244
+set $text #cdd6f4
+set $blue #89b4fa
+set $sapphire #74c7ec
+set $red #f38ba8
+
+# classe bordure fond texte indicateur child_border
+client.focused $blue $blue $base $sapphire $blue
+client.focused_inactive $surface0 $surface0 $text $surface0 $surface0
+client.unfocused $surface0 $surface0 $text $surface0 $surface0
+client.urgent $red $red $base $red $red
+client.background $base
+
+font pango:Noto Sans 10
+default_border pixel 2
+gaps inner 4
+gaps outer 4
+
+##################
+## APPLICATIONS ##
+##################
+bindsym $mod+Return exec $term
+bindsym $mod+Shift+Return exec kitty --class floating-term
+bindsym $mod+r exec $menu
+bindsym $mod+e exec $filemanager
+bindsym $mod+b exec $browser
+# VR : installation des drivers/runtimes casque (HTC Vive, Index, Quest…)
+bindsym $mod+Shift+v exec kitty -e fws-vr-setup
+# Panneau des raccourcis clavier (aussi via le bouton ⌌ de la barre)
+bindsym $mod+k exec fws-keys
+# Menu d'alimentation (verrouiller/déconnexion/redémarrer/éteindre)
+bindsym $mod+x exec fws-powermenu
+
+##############
+## FENETRES ##
+##############
+bindsym $mod+q kill
+bindsym $mod+v floating toggle
+bindsym $mod+f fullscreen toggle
+bindsym $mod+j split toggle
+bindsym $mod+l exec i3lock -c 1e1e2e
+# Quitter i3 → retour à SDDM
+bindsym $mod+Shift+e exec "i3-nagbar -t warning -m 'Quitter i3 et revenir à SDDM ?' -B 'Oui, quitter' 'i3-msg exit'"
+bindsym $mod+Shift+c reload
+
+# Focus (flèches)
+bindsym $mod+Left focus left
+bindsym $mod+Right focus right
+bindsym $mod+Up focus up
+bindsym $mod+Down focus down
+# Déplacer la fenêtre
+bindsym $mod+Shift+Left move left
+bindsym $mod+Shift+Right move right
+bindsym $mod+Shift+Up move up
+bindsym $mod+Shift+Down move down
+# Redimensionner (Ctrl + flèches)
+bindsym $mod+Ctrl+Left resize shrink width 30 px
+bindsym $mod+Ctrl+Right resize grow width 30 px
+bindsym $mod+Ctrl+Up resize shrink height 30 px
+bindsym $mod+Ctrl+Down resize grow height 30 px
+
+################
+## WORKSPACES ##
+################
+bindsym $mod+1 workspace number 1
+bindsym $mod+2 workspace number 2
+bindsym $mod+3 workspace number 3
+bindsym $mod+4 workspace number 4
+bindsym $mod+5 workspace number 5
+bindsym $mod+6 workspace number 6
+bindsym $mod+7 workspace number 7
+bindsym $mod+8 workspace number 8
+bindsym $mod+9 workspace number 9
+bindsym $mod+0 workspace number 10
+bindsym $mod+Shift+1 move container to workspace number 1
+bindsym $mod+Shift+2 move container to workspace number 2
+bindsym $mod+Shift+3 move container to workspace number 3
+bindsym $mod+Shift+4 move container to workspace number 4
+bindsym $mod+Shift+5 move container to workspace number 5
+bindsym $mod+Shift+6 move container to workspace number 6
+bindsym $mod+Shift+7 move container to workspace number 7
+bindsym $mod+Shift+8 move container to workspace number 8
+bindsym $mod+Shift+9 move container to workspace number 9
+bindsym $mod+Shift+0 move container to workspace number 10
+# Multi-écrans
+bindsym $mod+Ctrl+1 move container to output left
+bindsym $mod+Ctrl+2 move container to output right
+# Molette = changer de workspace
+bindsym $mod+button4 workspace prev
+bindsym $mod+button5 workspace next
+# Scratchpad
+bindsym $mod+s scratchpad show
+bindsym $mod+Shift+s move scratchpad
+
+##############
+## CAPTURES ##
+##############
+bindsym Print exec --no-startup-id "maim | xclip -selection clipboard -t image/png"
+bindsym $mod+Print exec --no-startup-id "maim -s | xclip -selection clipboard -t image/png"
+bindsym $mod+Shift+Print exec --no-startup-id "mkdir -p ~/Images && maim -s ~/Images/capture_$(date +%Y-%m-%d_%H-%M-%S).png"
+
+################
+## MULTIMEDIA ##
+################
+bindsym XF86AudioRaiseVolume exec --no-startup-id wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+
+bindsym XF86AudioLowerVolume exec --no-startup-id wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-
+bindsym XF86AudioMute exec --no-startup-id wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
+bindsym XF86AudioPlay exec --no-startup-id playerctl play-pause
+bindsym XF86AudioNext exec --no-startup-id playerctl next
+bindsym XF86AudioPrev exec --no-startup-id playerctl previous
+bindsym XF86MonBrightnessUp exec --no-startup-id brightnessctl set +5%
+bindsym XF86MonBrightnessDown exec --no-startup-id brightnessctl set 5%-
+
+#####################
+## REGLES FENETRES ##
+#####################
+for_window [class="floating-term"] floating enable, resize set 900 550
+for_window [class="(?i)fws-hello"] floating enable
+for_window [class="(?i)fws-about"] floating enable
+for_window [class="(?i)pavucontrol"] floating enable, resize set 700 500
+for_window [class="(?i)blueman-manager"] floating enable
+for_window [title="Picture-in-Picture"] floating enable
+
+###########
+## BARRE ##
+###########
+# Polybar (config ~/.config/polybar/config.ini) : clone visuel de la waybar
+# Hyprland FWS — pilules, icônes, horloge+date au centre, bouton Raccourcis.
+# exec_always + pkill : la barre survit proprement aux reload ($mod+Shift+c).
+exec_always --no-startup-id sh -c 'pkill -x polybar; sleep 0.3; exec polybar -q fws'
+
+###############
+## AUTOSTART ##
+###############
+# picom : coins arrondis (parité visuelle avec Hyprland) — config FWS légère.
+# PAS en machine virtuelle : le compositing sur GPU virtuel (vmwgfx/llvmpipe)
+# rate le repaint des fenêtres GTK → contenus noirs/artefacts. Les coins
+# arrondis sont sacrifiés en VM ; sur matériel réel picom se lance normalement.
+exec --no-startup-id sh -c 'systemd-detect-virt --quiet || exec picom --config ~/.config/picom.conf'
+exec --no-startup-id dunst
+exec --no-startup-id nm-applet
+exec --no-startup-id /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
+exec --no-startup-id sh -c '[ -n "$(ls -A /sys/class/bluetooth 2>/dev/null)" ] && exec blueman-applet'
+# Fenêtre de bienvenue (1er démarrage seulement — se retire ensuite).
+exec --no-startup-id fws-hello --autostart
diff --git a/configs/releng/airootfs/usr/local/share/fws/os-release b/configs/releng/airootfs/usr/local/share/fws/os-release
index 115f156..ed96250 100644
--- a/configs/releng/airootfs/usr/local/share/fws/os-release
+++ b/configs/releng/airootfs/usr/local/share/fws/os-release
@@ -3,7 +3,7 @@ PRETTY_NAME="FWS Linux"
ID=fws
ID_LIKE=arch
BUILD_ID=rolling
-VERSION_ID="1.0.0"
+VERSION_ID="0.6.0"
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://github.com/You-re-like-Windows-a-bitch/fws"
SUPPORT_URL="https://github.com/You-re-like-Windows-a-bitch/fws/issues"
diff --git a/configs/releng/airootfs/usr/local/share/fws/picom.conf b/configs/releng/airootfs/usr/local/share/fws/picom.conf
new file mode 100644
index 0000000..6edbb89
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/share/fws/picom.conf
@@ -0,0 +1,19 @@
+# FWS — picom minimal (session i3) : coins arrondis comme sous Hyprland.
+# xrender : fiable partout, y compris en VM (rendu logiciel) ; pas de flou ni
+# d'ombres (parité avec la config Hyprland FWS, et léger pour la VR).
+backend = "xrender";
+vsync = true;
+
+corner-radius = 8;
+rounded-corners-exclude = [
+ "window_type = 'dock'", # polybar gère son propre arrondi
+ "class_g = 'i3-frame'"
+];
+
+shadow = false;
+fading = true;
+fade-in-step = 0.08;
+fade-out-step = 0.08;
+
+# VR / jeux plein écran : ne pas composer par-dessus (latence).
+unredir-if-possible = true;
diff --git a/configs/releng/airootfs/usr/local/share/fws/polybar-config.ini b/configs/releng/airootfs/usr/local/share/fws/polybar-config.ini
new file mode 100644
index 0000000..8babbec
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/share/fws/polybar-config.ini
@@ -0,0 +1,174 @@
+; FWS — polybar (session i3) : clone visuel de la waybar Hyprland.
+; Barre flottante arrondie, modules en « pilules » Catppuccin Mocha,
+; icônes Nerd Font (ttf-nerd-fonts-symbols). Horloge + date au centre,
+; bouton « ⌌ Raccourcis » (fws-keys), volume/réseau/CPU/RAM/tray à droite.
+
+[colors]
+base = #E01e1e2e
+surface0 = #313244
+text = #cdd6f4
+subtext = #a6adc8
+blue = #89b4fa
+sapphire = #74c7ec
+teal = #94e2d5
+mauve = #cba6f7
+maroon = #eba0ac
+yellow = #f9e2af
+red = #f38ba8
+darkbase = #1e1e2e
+
+[bar/fws]
+monitor = ${env:MONITOR:}
+width = 100%:-20
+offset-x = 10
+offset-y = 6
+height = 36
+radius = 14
+background = ${colors.base}
+foreground = ${colors.text}
+border-size = 1
+border-color = #5989b4fa
+padding-left = 1
+padding-right = 1
+module-margin = 1
+line-size = 0
+font-0 = Noto Sans:size=10;2
+font-1 = Symbols Nerd Font:size=12;3
+font-2 = Noto Sans:weight=bold:size=10;2
+modules-left = i3 xwindow
+modules-center = date
+modules-right = updates keyhelp backlight pulseaudio network cpu memory tray power
+enable-ipc = true
+wm-restack = i3
+
+[module/i3]
+type = internal/i3
+pin-workspaces = true
+show-urgent = true
+strip-wsnumbers = false
+format =
+label-focused = %name%
+label-focused-background = ${colors.blue}
+label-focused-foreground = ${colors.darkbase}
+label-focused-padding = 2
+label-unfocused = %name%
+label-unfocused-background = ${colors.surface0}
+label-unfocused-foreground = ${colors.subtext}
+label-unfocused-padding = 2
+label-visible = %name%
+label-visible-background = ${colors.surface0}
+label-visible-padding = 2
+label-urgent = %name%
+label-urgent-background = ${colors.red}
+label-urgent-foreground = ${colors.darkbase}
+label-urgent-padding = 2
+
+[module/xwindow]
+type = internal/xwindow
+label = %title:0:45:…%
+label-foreground = ${colors.subtext}
+format-padding = 1
+
+[module/date]
+type = internal/date
+interval = 1
+date = %A %d %B %Y
+time = %H:%M
+label = %time% %date%
+label-font = 3
+format-background = ${colors.surface0}
+format-padding = 3
+
+[module/updates]
+type = custom/script
+exec = fws-updates-check
+interval = 1800
+click-left = kitty -e fws-update
+format-background = ${colors.surface0}
+format-foreground = ${colors.yellow}
+format-padding = 2
+
+[module/keyhelp]
+type = custom/script
+exec = echo " Raccourcis"
+interval = 3600
+click-left = fws-keys
+format-background = ${colors.blue}
+format-foreground = ${colors.darkbase}
+format-padding = 2
+label-font = 3
+
+[module/backlight]
+type = internal/backlight
+enable-scroll = true
+format =