feat(anaconda): add desktop environment selection spoke for FWS

Implement a new Anaconda spoke that allows users to select their preferred desktop environment (CLI, GNOME, KDE Plasma, Hyprland, or XFCE) during installation.

The spoke:
- Displays radio buttons for five desktop choices with CLI as the default
- Persists the selection to /tmp/fws-desktop for the kickstart %post script
- Supports revisiting the spoke and resuming previous selections
- Includes a GTK UI definition file (glade) with translatable labels
- Integrates as a SoftwareCategory spoke in the Anaconda installer

The implementation leverages Anaconda's runtime spoke discovery mechanism via the airootfs overlay, avoiding the need to rebuild the anaconda package.
This commit is contained in:
2026-06-24 17:58:52 +02:00
parent d409406fde
commit 7daca57714
4 changed files with 260 additions and 0 deletions
@@ -0,0 +1,105 @@
# 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"),
("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
@@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- FWS — onglet « Desktop Environment » (choix du bureau à installer). -->
<interface>
<requires lib="gtk+" version="3.6"/>
<requires lib="AnacondaWidgets" version="1.0"/>
<object class="AnacondaSpokeWindow" id="fwsDesktopWindow">
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="window-name" translatable="yes">DESKTOP ENVIRONMENT</property>
<signal name="button-clicked" handler="on_back_clicked" swapped="no"/>
<child internal-child="main_box">
<object class="GtkBox" id="AnacondaSpokeWindow-main_box1">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child internal-child="nav_box">
<object class="GtkEventBox" id="AnacondaSpokeWindow-nav_box1">
<property name="can-focus">False</property>
<child internal-child="nav_area">
<object class="GtkGrid" id="AnacondaSpokeWindow-nav_area1">
<property name="can-focus">False</property>
<property name="margin-left">6</property>
<property name="margin-right">6</property>
<property name="margin-top">6</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="alignment">
<object class="GtkAlignment" id="AnacondaSpokeWindow-alignment1">
<property name="can-focus">False</property>
<property name="margin-top">12</property>
<property name="yalign">0</property>
<property name="xscale">0</property>
<property name="yscale">0</property>
<property name="bottom-padding">48</property>
<property name="left-padding">24</property>
<property name="right-padding">24</property>
<child internal-child="action_area">
<object class="GtkBox" id="AnacondaSpokeWindow-action_area1">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">8</property>
<child>
<object class="GtkLabel" id="fws_desktop_desc">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xalign">0</property>
<property name="margin-bottom">12</property>
<property name="wrap">True</property>
<property name="max-width-chars">60</property>
<property name="label" translatable="yes">Choose the desktop environment to install. A web browser (Firefox) is installed automatically with any desktop.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="radio_cli">
<property name="label" translatable="yes">Command line only (no desktop)</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw-indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="radio_gnome">
<property name="label" translatable="yes">GNOME</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
<property name="group">radio_cli</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="radio_kde">
<property name="label" translatable="yes">KDE Plasma</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
<property name="group">radio_cli</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="radio_hyprland">
<property name="label" translatable="yes">Hyprland</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
<property name="group">radio_cli</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="radio_xfce">
<property name="label" translatable="yes">XFCE</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
<property name="group">radio_cli</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child internal-child="accessible">
<object class="AtkObject" id="fwsDesktopWindow-atkobject">
<property name="AtkObject::accessible-name" translatable="yes">DESKTOP ENVIRONMENT</property>
</object>
</child>
</object>
</interface>
@@ -216,6 +216,8 @@ case "$DESK" in
hyprland) PKGS="hyprland kitty wofi waybar sddm"; DM="sddm" ;; hyprland) PKGS="hyprland kitty wofi waybar sddm"; DM="sddm" ;;
cli|*) PKGS="" ;; cli|*) PKGS="" ;;
esac esac
# Navigateur de base ajouté DÈS QU'un bureau est choisi (jamais en CLI).
[ -n "$PKGS" ] && PKGS="$PKGS firefox"
if [ -n "$PKGS" ]; then if [ -n "$PKGS" ]; then
pacman-key --init &>/dev/null || true pacman-key --init &>/dev/null || true
pacman-key --populate archlinux &>/dev/null || true pacman-key --populate archlinux &>/dev/null || true