From fdf222aff9ac376bd1395b1c4d1c872fcb276620 Mon Sep 17 00:00:00 2001 From: Rodrigo Bernardi Date: Thu, 23 Jul 2026 10:59:09 -0300 Subject: [PATCH 1/2] fix: prevent fork loop and JSON hang in keybinds hint keybinds_hint.sh: replace 'exec $0' with 'exit 0' to prevent infinite self-re-execution when rofi produces no output or dispatch fails. This was causing fork storms (~568 forks/s of python3) that led to system load spikes. hint-hyprland.py: replace infinite 'while True' retry loop with a bounded approach. When 'hyprctl binds -j' returns invalid JSON (known issue on Hyprland 0.56.0), fall back to parsing the text output of 'hyprctl binds' instead of sleeping and retrying forever. Added timeout=5 to subprocess calls to prevent indefinite hangs. Tested on Hyprland 0.56.0 + kernel 7.1.4 where 'hyprctl binds -j' produces malformed JSON (fields shifted, non-numeric values in numeric fields). --- .../.local/lib/hyde/keybinds/hint-hyprland.py | 76 ++++++++++++++----- Configs/.local/lib/hyde/keybinds_hint.sh | 4 +- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/Configs/.local/lib/hyde/keybinds/hint-hyprland.py b/Configs/.local/lib/hyde/keybinds/hint-hyprland.py index 27c5d31fbc..25a0f55b46 100755 --- a/Configs/.local/lib/hyde/keybinds/hint-hyprland.py +++ b/Configs/.local/lib/hyde/keybinds/hint-hyprland.py @@ -8,27 +8,69 @@ def get_hyprctl_binds(): - while True: + try: + result = subprocess.run( + ["hyprctl", "binds", "-j"], capture_output=True, text=True, check=True, timeout=5 + ) try: - result = subprocess.run( - ["hyprctl", "binds", "-j"], capture_output=True, text=True, check=True - ) - while result.returncode != 0: - print("Waiting for hyprctl command to succeed...") - time.sleep(1) - result = subprocess.run( - ["hyprctl", "binds", "-j"], - capture_output=True, - text=True, - check=True, - ) binds = json.loads(result.stdout) return binds - except subprocess.CalledProcessError as e: - print(f"Error executing hyprctl: {e}") - return None except json.JSONDecodeError: - time.sleep(1) + pass + except (subprocess.CalledProcessError, subprocess.TimeoutExpired): + pass + + try: + result = subprocess.run( + ["hyprctl", "binds"], capture_output=True, text=True, check=True, timeout=5 + ) + binds = [] + current = {} + for line in result.stdout.strip().split("\n"): + line = line.strip() + if line == "bindd" or line == "bind": + if current: + binds.append(current) + current = {} + elif ":" in line: + key, _, value = line.partition(":") + key = key.strip() + value = value.strip() + if key == "modmask": + current["modmask"] = int(value) if value.isdigit() else 0 + elif key == "submap": + current["submap"] = value + elif key == "key": + current["key"] = value + elif key == "keycode": + current["keycode"] = int(value) if value.isdigit() else 0 + elif key == "catchall": + current["catch_all"] = value.lower() == "true" + elif key == "description": + current["description"] = value + current["has_description"] = bool(value) + elif key == "dispatcher": + current["dispatcher"] = value + elif key == "arg": + current["arg"] = value + elif key == "repeat": + current["repeat"] = value.lower() == "true" + elif key == "locked": + current["locked"] = value.lower() == "true" + elif key == "mouse": + current["mouse"] = value.lower() == "true" + elif key == "release": + current["release"] = value.lower() == "true" + elif key == "non_consuming": + current["non_consuming"] = value.lower() == "true" + elif key == "allow_input_capture": + current["allow_input_capture"] = value.lower() == "true" + if current: + binds.append(current) + return binds if binds else None + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: + print(f"Error executing hyprctl: {e}") + return None def parse_description(description): diff --git a/Configs/.local/lib/hyde/keybinds_hint.sh b/Configs/.local/lib/hyde/keybinds_hint.sh index bfd78342bc..2b224f2afa 100755 --- a/Configs/.local/lib/hyde/keybinds_hint.sh +++ b/Configs/.local/lib/hyde/keybinds_hint.sh @@ -58,7 +58,7 @@ dispatch=$(awk -F ':::' '{print $2}' <<< "$selected" | xargs) arg=$(awk -F ':::' '{print $3}' <<< "$selected" | xargs) repeat=$(awk -F ':::' '{print $4}' <<< "$selected" | xargs) RUN() { - case "$(eval "hyprctl dispatch '$dispatch' '$arg'")" in *"Not enough arguments"*) exec $0 ;; esac + case "$(eval "hyprctl dispatch '$dispatch' '$arg'")" in *"Not enough arguments"*) exit 0 ;; esac } if [ -n "$dispatch" ] && [ "$(echo "$dispatch" | wc -l)" -eq 1 ]; then if [ "$repeat" = repeat ]; then @@ -74,5 +74,5 @@ if [ -n "$dispatch" ] && [ "$(echo "$dispatch" | wc -l)" -eq 1 ]; then RUN fi else - exec $0 + exit 0 fi From c6e759cff2c8a70f17d3b3369a49106e76eba0b3 Mon Sep 17 00:00:00 2001 From: Rodrigo Bernardi Date: Tue, 28 Jul 2026 15:24:32 -0300 Subject: [PATCH 2/2] fix: address CodeRabbit review comments - Recognize all bind header variants (bindl=, bindm=, bindr=, etc.) not just 'bind' and 'bindd', using line.startswith('bind') - Add parse_bool() helper to accept numeric flags (1/0) in addition to string 'true'/'false' for repeat, locked, mouse, release, non_consuming, allow_input_capture, and catchall fields - Accept signed keycode values (e.g. -1) via try/except instead of value.isdigit() which rejected negative numbers --- .../.local/lib/hyde/keybinds/hint-hyprland.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/Configs/.local/lib/hyde/keybinds/hint-hyprland.py b/Configs/.local/lib/hyde/keybinds/hint-hyprland.py index 25a0f55b46..31ebc7b097 100755 --- a/Configs/.local/lib/hyde/keybinds/hint-hyprland.py +++ b/Configs/.local/lib/hyde/keybinds/hint-hyprland.py @@ -7,6 +7,11 @@ import time +def parse_bool(value): + """Parse boolean values accepting both string ('true'/'false') and numeric (1/0) forms.""" + return value.lower() in {"1", "true"} + + def get_hyprctl_binds(): try: result = subprocess.run( @@ -28,7 +33,7 @@ def get_hyprctl_binds(): current = {} for line in result.stdout.strip().split("\n"): line = line.strip() - if line == "bindd" or line == "bind": + if line.startswith("bind"): if current: binds.append(current) current = {} @@ -43,9 +48,12 @@ def get_hyprctl_binds(): elif key == "key": current["key"] = value elif key == "keycode": - current["keycode"] = int(value) if value.isdigit() else 0 + try: + current["keycode"] = int(value) + except ValueError: + current["keycode"] = 0 elif key == "catchall": - current["catch_all"] = value.lower() == "true" + current["catch_all"] = parse_bool(value) elif key == "description": current["description"] = value current["has_description"] = bool(value) @@ -54,17 +62,17 @@ def get_hyprctl_binds(): elif key == "arg": current["arg"] = value elif key == "repeat": - current["repeat"] = value.lower() == "true" + current["repeat"] = parse_bool(value) elif key == "locked": - current["locked"] = value.lower() == "true" + current["locked"] = parse_bool(value) elif key == "mouse": - current["mouse"] = value.lower() == "true" + current["mouse"] = parse_bool(value) elif key == "release": - current["release"] = value.lower() == "true" + current["release"] = parse_bool(value) elif key == "non_consuming": - current["non_consuming"] = value.lower() == "true" + current["non_consuming"] = parse_bool(value) elif key == "allow_input_capture": - current["allow_input_capture"] = value.lower() == "true" + current["allow_input_capture"] = parse_bool(value) if current: binds.append(current) return binds if binds else None