diff --git a/configs/releng/airootfs/usr/local/bin/fws-about b/configs/releng/airootfs/usr/local/bin/fws-about
new file mode 100755
index 0000000..223db89
--- /dev/null
+++ b/configs/releng/airootfs/usr/local/bin/fws-about
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+# FWS — fenêtre « À propos » : deux onglets, « Le projet FWS » (ce qu'est FWS,
+# version, liens) et « Système » (matériel + logiciel détectés). Remplace le
+# « About Xfce » de libxfce4ui par une page de marque FWS. GTK3 (python-gobject
+# installé avec le bureau).
+
+import os
+import re
+import subprocess
+
+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
+
+GLib.set_prgname("fws-about")
+
+
+def _osrelease():
+ data = {}
+ for path in ("/etc/os-release", "/usr/lib/os-release"):
+ try:
+ with open(path) as fh:
+ for line in fh:
+ m = re.match(r'^([A-Z_]+)=(.*)$', line.strip())
+ if m:
+ data[m.group(1)] = m.group(2).strip('"')
+ break
+ except OSError:
+ continue
+ return data
+
+
+def _cmd(*argv):
+ try:
+ return subprocess.check_output(argv, text=True,
+ stderr=subprocess.DEVNULL).strip()
+ except (OSError, subprocess.CalledProcessError):
+ return ""
+
+
+def _cpu():
+ try:
+ with open("/proc/cpuinfo") as fh:
+ for line in fh:
+ if line.startswith("model name"):
+ return line.split(":", 1)[1].strip()
+ except OSError:
+ pass
+ return "inconnu"
+
+
+def _mem():
+ try:
+ with open("/proc/meminfo") as fh:
+ for line in fh:
+ if line.startswith("MemTotal"):
+ kib = int(line.split()[1])
+ return f"{kib / 1048576:.1f} Gio"
+ except OSError:
+ pass
+ return "inconnu"
+
+
+def _gpus():
+ out = []
+ for line in _cmd("lspci").splitlines():
+ low = line.lower()
+ if "vga" in low or "3d" in low or "display" in low:
+ out.append(line.split(":", 2)[-1].strip())
+ return out or ["inconnu"]
+
+
+def _desktop():
+ if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"):
+ return "Hyprland"
+ env = os.environ.get("XDG_CURRENT_DESKTOP") \
+ or os.environ.get("DESKTOP_SESSION") or ""
+ return env or "inconnu"
+
+
+def _session_type():
+ t = os.environ.get("XDG_SESSION_TYPE", "")
+ return {"wayland": "Wayland", "x11": "X11"}.get(t, t or "inconnu")
+
+
+def _disks():
+ out = []
+ for line in _cmd("lsblk", "-dno", "NAME,SIZE,MODEL,TYPE").splitlines():
+ f = line.split()
+ if f and f[-1] == "disk":
+ out.append(" ".join(f[:-1]))
+ return out or ["inconnu"]
+
+
+def _grid(rows):
+ grid = Gtk.Grid(column_spacing=18, row_spacing=8, margin=18)
+ for i, (key, val) in enumerate(rows):
+ k = Gtk.Label(xalign=1)
+ k.set_markup(f"{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 Arch 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())