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:
2026-07-04 03:59:09 +02:00
parent a63021cde7
commit 2c5f08330a
12 changed files with 350 additions and 125 deletions
+1 -1
View File
@@ -29,6 +29,6 @@ file_permissions=(
["/usr/bin/load_policy"]="0:0:755"
["/usr/bin/setstatus"]="0:0:755"
["/usr/local/bin/fws-setup-hardware"]="0:0:755"
["/usr/local/bin/fws-hypr-keys"]="0:0:755"
["/usr/local/bin/fws-keys"]="0:0:755"
["/usr/local/bin/fws-vr-setup"]="0:0:755"
)
@@ -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
View File
@@ -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
@@ -7,7 +7,7 @@
#
# Les raccourcis sont déclarés avec « bindd » (bind + description) : la
# description alimente l'écran « Raccourcis clavier » (bouton ⌨ de la barre
# ou SUPER+K → fws-hypr-keys, qui lit les binds RÉELS via hyprctl). Ajoute
# ou SUPER+K → fws-keys, qui lit les binds RÉELS via hyprctl). Ajoute
# tes propres bindd : ils apparaîtront automatiquement dans la liste.
# ⚠ Pas de virgule dans les descriptions (séparateur de champs Hyprland).
@@ -81,7 +81,7 @@ bindd = $mod SHIFT, Return, Terminal flottant, exec, kitty --class floating-term
bindd = $mod, R, Menu des applications, exec, wofi --show drun
bindd = $mod, E, Gestionnaire de fichiers, exec, thunar
bindd = $mod, B, Ouvrir Firefox, exec, firefox
bindd = $mod, K, Afficher les raccourcis clavier, exec, fws-hypr-keys
bindd = $mod, K, Afficher les raccourcis clavier, exec, fws-keys
# --- Fenêtres ---------------------------------------------------------------
bindd = $mod, Q, Fermer la fenêtre, killactive,
@@ -41,6 +41,8 @@ bindsym $mod+e exec $filemanager
bindsym $mod+b exec $browser
# VR : installation des drivers/runtimes casque (HTC Vive, Index, Quest…)
bindsym $mod+Shift+v exec kitty -e fws-vr-setup
# Panneau des raccourcis clavier (aussi via le bouton ⌌ de la barre)
bindsym $mod+k exec fws-keys
##############
## FENETRES ##
@@ -133,23 +135,16 @@ for_window [title="Picture-in-Picture"] floating enable
###########
## BARRE ##
###########
bar {
status_command i3status
position top
colors {
background $base
statusline $text
separator $surface0
# classe bordure fond texte
focused_workspace $blue $blue $base
inactive_workspace $surface0 $base $text
urgent_workspace $red $red $base
}
}
# Polybar (config ~/.config/polybar/config.ini) : clone visuel de la waybar
# Hyprland FWS — pilules, icônes, horloge+date au centre, bouton Raccourcis.
# exec_always + pkill : la barre survit proprement aux reload ($mod+Shift+c).
exec_always --no-startup-id sh -c 'pkill -x polybar; sleep 0.3; exec polybar -q fws'
###############
## AUTOSTART ##
###############
# picom : coins arrondis (parité visuelle avec Hyprland) — config FWS légère.
exec --no-startup-id picom --config ~/.config/picom.conf
exec --no-startup-id dunst
exec --no-startup-id nm-applet
exec --no-startup-id /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1
@@ -0,0 +1,19 @@
# FWS — picom minimal (session i3) : coins arrondis comme sous Hyprland.
# xrender : fiable partout, y compris en VM (rendu logiciel) ; pas de flou ni
# d'ombres (parité avec la config Hyprland FWS, et léger pour la VR).
backend = "xrender";
vsync = true;
corner-radius = 8;
rounded-corners-exclude = [
"window_type = 'dock'", # polybar gère son propre arrondi
"class_g = 'i3-frame'"
];
shadow = false;
fading = true;
fade-in-step = 0.08;
fade-out-step = 0.08;
# VR / jeux plein écran : ne pas composer par-dessus (latence).
unredir-if-possible = true;
@@ -0,0 +1,156 @@
; FWS — polybar (session i3) : clone visuel de la waybar Hyprland.
; Barre flottante arrondie, modules en « pilules » Catppuccin Mocha,
; icônes Nerd Font (ttf-nerd-fonts-symbols). Horloge + date au centre,
; bouton « ⌌ Raccourcis » (fws-keys), volume/réseau/CPU/RAM/tray à droite.
[colors]
base = #E01e1e2e
surface0 = #313244
text = #cdd6f4
subtext = #a6adc8
blue = #89b4fa
sapphire = #74c7ec
teal = #94e2d5
mauve = #cba6f7
maroon = #eba0ac
yellow = #f9e2af
red = #f38ba8
darkbase = #1e1e2e
[bar/fws]
monitor = ${env:MONITOR:}
width = 100%:-20
offset-x = 10
offset-y = 6
height = 36
radius = 14
background = ${colors.base}
foreground = ${colors.text}
border-size = 1
border-color = #5989b4fa
padding-left = 1
padding-right = 1
module-margin = 1
line-size = 0
font-0 = Noto Sans:size=10;2
font-1 = Symbols Nerd Font:size=12;3
font-2 = Noto Sans:weight=bold:size=10;2
modules-left = i3 xwindow
modules-center = date
modules-right = keyhelp backlight pulseaudio network cpu memory tray
enable-ipc = true
wm-restack = i3
[module/i3]
type = internal/i3
pin-workspaces = true
show-urgent = true
strip-wsnumbers = false
format = <label-state> <label-mode>
label-focused = %name%
label-focused-background = ${colors.blue}
label-focused-foreground = ${colors.darkbase}
label-focused-padding = 2
label-unfocused = %name%
label-unfocused-background = ${colors.surface0}
label-unfocused-foreground = ${colors.subtext}
label-unfocused-padding = 2
label-visible = %name%
label-visible-background = ${colors.surface0}
label-visible-padding = 2
label-urgent = %name%
label-urgent-background = ${colors.red}
label-urgent-foreground = ${colors.darkbase}
label-urgent-padding = 2
[module/xwindow]
type = internal/xwindow
label = %title:0:45:…%
label-foreground = ${colors.subtext}
format-padding = 1
[module/date]
type = internal/date
interval = 1
date = %A %d %B %Y
time = %H:%M
label = 󰥔 %time% %date%
label-font = 3
format-background = ${colors.surface0}
format-padding = 3
[module/keyhelp]
type = custom/script
exec = echo "󰌌 Raccourcis"
interval = 3600
click-left = fws-keys
format-background = ${colors.blue}
format-foreground = ${colors.darkbase}
format-padding = 2
label-font = 3
[module/backlight]
type = internal/backlight
enable-scroll = true
format = <label>
label = 󰃟 %percentage%%
format-background = ${colors.surface0}
format-foreground = ${colors.yellow}
format-padding = 2
[module/pulseaudio]
type = internal/pulseaudio
use-ui-max = false
click-right = pavucontrol
format-volume = <label-volume>
label-volume = 󰕾 %percentage%%
format-volume-background = ${colors.surface0}
format-volume-foreground = ${colors.blue}
format-volume-padding = 2
label-muted = 󰝟 muet
format-muted-background = ${colors.surface0}
format-muted-foreground = ${colors.subtext}
format-muted-padding = 2
[module/network]
type = internal/network
interface-type = wired
interval = 5
format-connected = <label-connected>
label-connected = 󰈀 %local_ip%
format-connected-background = ${colors.surface0}
format-connected-foreground = ${colors.teal}
format-connected-padding = 2
format-disconnected = <label-disconnected>
label-disconnected = 󰤭 hors ligne
format-disconnected-background = ${colors.surface0}
format-disconnected-foreground = ${colors.red}
format-disconnected-padding = 2
[module/cpu]
type = internal/cpu
interval = 3
label = %percentage%%
format-background = ${colors.surface0}
format-foreground = ${colors.mauve}
format-padding = 2
[module/memory]
type = internal/memory
interval = 3
label = %percentage_used%%
format-background = ${colors.surface0}
format-foreground = ${colors.maroon}
format-padding = 2
[module/tray]
type = internal/tray
format = <tray>
format-background = ${colors.surface0}
format-padding = 1
tray-spacing = 8px
tray-size = 16px
[settings]
screenchange-reload = true
pseudo-transparency = false
@@ -33,7 +33,7 @@
"custom/keyhelp": {
"format": "󰌌 Raccourcis",
"tooltip-format": "Raccourcis clavier (SUPER+K)",
"on-click": "fws-hypr-keys"
"on-click": "fws-keys"
},
"backlight": {
"format": "{icon} {percent}%",
@@ -1,4 +1,4 @@
/* FWS — style du panneau « Raccourcis clavier » (fws-hypr-keys).
/* FWS — style du panneau « Raccourcis clavier » (fws-keys).
Même palette que le menu wofi, mais police MONOSPACE : les colonnes
combinaison/description sont alignées par des espaces. */
@@ -285,7 +285,7 @@ case "$DESK" in
# xorg-server est requis (SDDM ne le tire pas) ; maim+xclip = captures X11,
# dunst = notifications X11 (équivalent de mako), i3lock = verrouillage.
# fws-vr-setup (Super+Shift+V ou menu) installe les drivers du casque.
i3) PKGS="i3-wm i3status i3lock xorg-server rofi kitty thunar sddm noto-fonts ttf-nerd-fonts-symbols maim xclip dunst libnotify polkit-gnome network-manager-applet gvfs playerctl brightnessctl pipewire pipewire-alsa pipewire-pulse wireplumber pavucontrol bluez bluez-utils blueman"; DM="sddm" ;;
i3) PKGS="i3-wm i3lock xorg-server polybar picom rofi kitty thunar sddm noto-fonts ttf-nerd-fonts-symbols maim xclip dunst libnotify polkit-gnome network-manager-applet gvfs playerctl brightnessctl pipewire pipewire-alsa pipewire-pulse wireplumber pavucontrol bluez bluez-utils blueman"; DM="sddm" ;;
cli|*) PKGS="" ;;
esac
# Navigateur de base ajouté DÈS QU'un bureau est choisi (jamais en CLI).
@@ -414,13 +414,13 @@ if [ "$DESK" = "hyprland" ] && [ -f /usr/local/share/fws/hyprland.conf.tmpl ]; t
sed -e "s/__KB_LAYOUT__/${XL:-us}/" -e "s/__KB_VARIANT__/${XV}/" \
/usr/local/share/fws/hyprland.conf.tmpl > /tmp/fws-hyprland.conf
install -Dm644 /tmp/fws-hyprland.conf /etc/skel/.config/hypr/hyprland.conf
# Waybar FWS : barre assortie + bouton « ⌨ Raccourcis » (fws-hypr-keys,
# Waybar FWS : barre assortie + bouton « ⌨ Raccourcis » (fws-keys,
# aussi sur SUPER+K) qui liste les binds RÉELS chargés par Hyprland
# (hyprctl binds -j) — les descriptions viennent des bindd de la config.
install -Dm644 /usr/local/share/fws/waybar-config.json /etc/skel/.config/waybar/config
install -Dm644 /usr/local/share/fws/waybar-style.css /etc/skel/.config/waybar/style.css
# wofi : style du menu d'applications (Super+R) — le panneau « Raccourcis »
# a son propre style (wofi-keys.css), passé en --style par fws-hypr-keys.
# a son propre style (wofi-keys.css), passé en --style par fws-keys.
install -Dm644 /usr/local/share/fws/wofi-config /etc/skel/.config/wofi/config
install -Dm644 /usr/local/share/fws/wofi-style.css /etc/skel/.config/wofi/style.css
while IFS=: read -r _u _x _uid _g _gec _home _sh; do
@@ -442,11 +442,18 @@ fi
# /etc/X11/xorg.conf.d/00-keyboard.conf, écrite à l'étape 3bis.
if [ "$DESK" = "i3" ] && [ -f /usr/local/share/fws/i3-config ]; then
install -Dm644 /usr/local/share/fws/i3-config /etc/skel/.config/i3/config
# Polybar (clone de la waybar) + picom (coins arrondis) : même rendu
# visuel que la session Hyprland FWS.
install -Dm644 /usr/local/share/fws/polybar-config.ini /etc/skel/.config/polybar/config.ini
install -Dm644 /usr/local/share/fws/picom.conf /etc/skel/.config/picom.conf
while IFS=: read -r _u _x _uid _g _gec _home _sh; do
[ "$_uid" -ge 1000 ] && [ "$_uid" -le 60000 ] && [ -d "$_home" ] || continue
install -Dm644 /usr/local/share/fws/i3-config "$_home/.config/i3/config"
install -Dm644 /usr/local/share/fws/polybar-config.ini "$_home/.config/polybar/config.ini"
install -Dm644 /usr/local/share/fws/picom.conf "$_home/.config/picom.conf"
chown "$_u": "$_home/.config" 2>/dev/null || true
chown -R "$_u": "$_home/.config/i3" 2>/dev/null || true
chown -R "$_u": "$_home/.config/i3" "$_home/.config/polybar" \
"$_home/.config/picom.conf" 2>/dev/null || true
done < /etc/passwd
fi
+1 -1
View File
@@ -25,6 +25,6 @@ file_permissions=(
["/usr/bin/load_policy"]="0:0:755"
["/usr/bin/setstatus"]="0:0:755"
["/usr/local/bin/fws-setup-hardware"]="0:0:755"
["/usr/local/bin/fws-hypr-keys"]="0:0:755"
["/usr/local/bin/fws-keys"]="0:0:755"
["/usr/local/bin/fws-vr-setup"]="0:0:755"
)