refactor(fws-keys): make keyboard shortcuts tool universal for hyprland and i3
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.
This commit is contained in:
@@ -1,101 +0,0 @@
|
||||
#!/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 os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
_STYLE = "/usr/local/share/fws/wofi-keys.css"
|
||||
|
||||
# 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.
|
||||
cmd = ["wofi", "--dmenu", "--prompt", "Raccourcis clavier — taper pour filtrer",
|
||||
"--width", "820", "--height", "600", "--insensitive",
|
||||
"--cache-file", "/dev/null"]
|
||||
if os.path.exists(_STYLE):
|
||||
# Style dédié : monospace (colonnes alignées) + palette FWS.
|
||||
cmd += ["--style", _STYLE]
|
||||
subprocess.run(cmd, input="\n".join(lines), text=True, check=False)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
#!/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())
|
||||
@@ -24,6 +24,18 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exec sudo "$0" "$@"
|
||||
fi
|
||||
|
||||
# VM = cul-de-sac VR : pas d'accès direct au GPU ni à l'USB/DisplayPort du
|
||||
# casque, et Steam (steamwebhelper) est INSTABLE en rendu logiciel (llvmpipe).
|
||||
# On prévient au lieu de laisser l'utilisateur croire à une panne de FWS.
|
||||
if systemd-detect-virt --quiet 2>/dev/null; then
|
||||
warn "Machine VIRTUELLE détectée ($(systemd-detect-virt 2>/dev/null))."
|
||||
warn "La VR ne fonctionnera PAS ici : casque et GPU inaccessibles depuis une VM."
|
||||
warn "Steam lui-même peut planter en rendu logiciel (steamwebhelper)."
|
||||
echo " → Fais cette installation sur la machine RÉELLE avec le casque branché."
|
||||
read -rp " Continuer quand même (tests uniquement) ? [o/N] " _ok
|
||||
case "$_ok" in o|O|oui) ;; *) exit 0 ;; esac
|
||||
fi
|
||||
|
||||
# lib32 Vulkan selon le GPU (indispensable à SteamVR, qui est 32/64 bits).
|
||||
GPU="$(lspci 2>/dev/null | grep -iE 'VGA compatible controller|3D controller|Display controller')"
|
||||
LIB32="lib32-mesa"
|
||||
@@ -48,12 +60,20 @@ install_pkgs() {
|
||||
|
||||
steamvr_base() {
|
||||
say "Installation de la base SteamVR (Steam + Vulkan 32 bits : $LIB32)…"
|
||||
# ttf-liberation : polices attendues par l'interface Steam (évite des
|
||||
# rendus cassés/plantages cosmétiques sur install minimale).
|
||||
# shellcheck disable=SC2086
|
||||
install_pkgs steam $LIB32 || return 1
|
||||
install_pkgs steam $LIB32 ttf-liberation || return 1
|
||||
say "Base installée. Étapes suivantes :"
|
||||
echo " 1. Lance Steam, connecte-toi, puis installe « SteamVR » (Bibliothèque → Outils)."
|
||||
echo " 2. Branche le casque et les stations, puis lance SteamVR."
|
||||
echo " 3. Premier lancement : SteamVR peut demander à ajuster les règles udev — accepte."
|
||||
say "Si Steam PLANTE (fenêtre qui se ferme, souvent steamwebhelper) :"
|
||||
echo " • Vérifie l'espace disque (SteamVR ≈ 5 Go) : df -h ~"
|
||||
echo " • GPU faible / VM / rendu logiciel — lance :"
|
||||
echo " steam -cef-disable-gpu -cef-disable-gpu-compositing"
|
||||
echo " • Pour voir l'erreur exacte : lance « steam » depuis un terminal,"
|
||||
echo " ou regarde ~/.steam/steam/logs/ (console_log.txt, bootstrap_log.txt)."
|
||||
}
|
||||
|
||||
case "${1:-menu}" in
|
||||
|
||||
Reference in New Issue
Block a user