2c5f08330a
Rename fws-hypr-keys to fws-keys and extend it to support both Hyprland and i3 desktop environments. The tool now detects the session type and uses appropriate methods: - Hyprland: queries hyprctl binds for real loaded bindings (wofi dmenu) - i3: parses ~/.config/i3/config with variable resolution (rofi dmenu) Update all references in configs and templates. Add VM detection warning in fws-vr-setup to prevent VR attempts in virtual machines where GPU/USB access is unavailable. Add ttf-liberation font dependency for Steam UI stability on minimal installations and improve error guidance for Steam crashes.
130 lines
4.4 KiB
Python
Executable File
130 lines
4.4 KiB
Python
Executable File
#!/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())
|