#!/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())