feat(releng): add fws-hypr-keys script to display hyprland keyboard shortcuts

Add a new Python utility script that displays actual Hyprland keyboard shortcuts from hyprctl binds output. The script:

- Queries hyprctl binds to get loaded key bindings from the compositor
- Formats keybinds with human-readable modifiers and key labels (French localization)
- Displays descriptions from bindd config entries or falls back to dispatcher actions
- Opens results in wofi dmenu for incremental search and filtering
- Handles special keys like mouse buttons, arrow keys, and common special keys
- Deduplicates repeated bindings (e.g., from resize actions)

No additional dependencies required beyond python3 and wofi (already in FWS + Hyprland packages).
This commit is contained in:
2026-07-03 23:59:49 +02:00
parent 7aa1276e2c
commit a28a64310a
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
# FWS — affiche les raccourcis clavier RÉELS de Hyprland.
#
# Source de vérité : « hyprctl binds -j » = les binds effectivement CHARGÉS
# par le compositeur (ceux de ~/.config/hypr/hyprland.conf, y compris ceux
# ajoutés par l'utilisateur après l'installation). Les descriptions viennent
# des « bindd » de la config ; un bind sans description affiche son action
# brute (dispatcher + argument). Affichage : wofi en mode dmenu (recherche
# incrémentale intégrée) — déclenché par le bouton ⌨ de la waybar ou SUPER+K.
#
# python3 et wofi sont déjà sur le système (image FWS + paquets Hyprland) :
# aucune dépendance supplémentaire.
import json
import subprocess
import sys
# Bits de modmask (masques X11) dans l'ordre d'affichage voulu.
_MODS = [
(64, "SUPER"),
(4, "CTRL"),
(8, "ALT"),
(1, "SHIFT"),
]
# Libellés humains pour les « touches » spéciales.
_KEY_LABELS = {
"mouse:272": "clic gauche",
"mouse:273": "clic droit",
"mouse:274": "clic molette",
"mouse_down": "molette bas",
"mouse_up": "molette haut",
"return": "Entrée",
"space": "Espace",
"escape": "Échap",
"tab": "Tab",
"left": "←",
"right": "→",
"up": "↑",
"down": "↓",
}
def _mods(mask):
return [name for bit, name in _MODS if mask & bit]
def _combo(bind):
parts = _mods(bind.get("modmask", 0))
key = bind.get("key") or ""
if not key and bind.get("keycode"):
key = f"code {bind['keycode']}"
parts.append(_KEY_LABELS.get(key.lower(), key))
return " + ".join(p for p in parts if p)
def _action(bind):
desc = (bind.get("description") or "").strip()
if desc:
return desc
arg = (bind.get("arg") or "").strip()
return f"{bind.get('dispatcher', '?')} {arg}".strip()
def main():
try:
raw = subprocess.check_output(["hyprctl", "binds", "-j"], text=True)
binds = json.loads(raw)
except (OSError, subprocess.CalledProcessError, ValueError) as exc:
subprocess.run(["notify-send", "Raccourcis FWS",
f"hyprctl binds a échoué : {exc}"], check=False)
return 1
lines = []
seen = set()
for bind in binds:
combo = _combo(bind)
if not combo or combo in seen: # binde répétés (resize) : une ligne
continue
seen.add(combo)
lines.append(f"{combo:<24} {_action(bind)}")
if not lines:
lines = ["(aucun raccourci déclaré)"]
# dmenu = liste + filtre incrémental ; la sélection n'a pas d'effet.
subprocess.run(
["wofi", "--dmenu", "--prompt", "Raccourcis clavier — taper pour filtrer",
"--width", "780", "--height", "560", "--insensitive", "--cache-file", "/dev/null"],
input="\n".join(lines), text=True, check=False)
return 0
if __name__ == "__main__":
sys.exit(main())