From 996d02cc608bdc1d1728a602fc382d23675be756 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Wed, 13 May 2026 02:23:21 -0700 Subject: [PATCH] Add Hyprland rofi action picker --- dotfiles/config/hypr/hyprland/binds.lua | 1 + dotfiles/config/hypr/hyprland/core.lua | 61 +++++++++++- dotfiles/lib/bin/hypr_rofi_action | 118 ++++++++++++++++++++++++ nixos/hyprland.nix | 13 +++ 4 files changed, 189 insertions(+), 4 deletions(-) create mode 100755 dotfiles/lib/bin/hypr_rofi_action diff --git a/dotfiles/config/hypr/hyprland/binds.lua b/dotfiles/config/hypr/hyprland/binds.lua index b6a614aa..4d7738ba 100644 --- a/dotfiles/config/hypr/hyprland/binds.lua +++ b/dotfiles/config/hypr/hyprland/binds.lua @@ -72,6 +72,7 @@ function M.setup(ctx) bind(hyper .. " + K", exec("rofi_kill_process.sh"), desc("Open process kill menu")) bind(hyper .. " + SHIFT + K", exec("rofi_kill_all.sh"), desc("Open kill-all menu")) bind(hyper .. " + R", exec("rofi-systemd"), desc("Open systemd unit menu")) + bind(hyper .. " + X", exec("hypr_rofi_action"), desc("Open Hyprland action menu")) bind(hyper .. " + I", exec("rofi_select_input.hs"), desc("Open input selection menu")) bind(hyper .. " + Y", exec("rofi_agentic_skill"), desc("Open agentic skill menu")) end diff --git a/dotfiles/config/hypr/hyprland/core.lua b/dotfiles/config/hypr/hyprland/core.lua index ab9bc486..1ef5d191 100644 --- a/dotfiles/config/hypr/hyprland/core.lua +++ b/dotfiles/config/hypr/hyprland/core.lua @@ -15,10 +15,6 @@ function M.setup(ctx) verify_config = command_line_contains("--verify-config") - local function bind(keys, dispatcher, opts) - hl.bind(keys, dispatcher, opts) - end - local function exec(command) return hl.dsp.exec_cmd(command) end @@ -27,6 +23,63 @@ function M.setup(ctx) return hl.dispatch(dispatcher) end + local action_registry = {} + + local function action_text(value) + return tostring(value or ""):gsub("[\t\r\n]", " "):gsub(" +", " "):match("^%s*(.-)%s*$") + end + + local function action_registry_path() + local runtime_dir = os.getenv("XDG_RUNTIME_DIR") or "/tmp" + return runtime_dir .. "/hyprland-actions.tsv" + end + + local function register_action(keys, dispatcher, opts) + local description = opts and opts.description + if not description or description == "" then + return + end + + local id = tostring(#action_registry + 1) + action_registry[#action_registry + 1] = { + id = id, + keys = action_text(keys), + description = action_text(description), + dispatcher = dispatcher, + } + end + + local function bind(keys, dispatcher, opts) + hl.bind(keys, dispatcher, opts) + register_action(keys, dispatcher, opts) + end + + _G.im_hyprland_write_actions = function() + local actions_file = io.open(action_registry_path(), "w") + if not actions_file then + return + end + + for _, action in ipairs(action_registry) do + actions_file:write(action.id, "\t", action.description, "\t", action.keys, "\n") + end + + actions_file:close() + end + + _G.im_hyprland_run_action = function(id) + local action = action_registry[tonumber(id)] + if not action then + return + end + + if type(action.dispatcher) == "function" then + action.dispatcher() + else + dispatch(action.dispatcher) + end + end + local function shell_quote(value) return "'" .. tostring(value):gsub("'", "'\\''") .. "'" end diff --git a/dotfiles/lib/bin/hypr_rofi_action b/dotfiles/lib/bin/hypr_rofi_action new file mode 100755 index 00000000..f2583e10 --- /dev/null +++ b/dotfiles/lib/bin/hypr_rofi_action @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +import os +import shutil +import subprocess +import sys +from pathlib import Path + + +PROMPT = "Hyprland action" + + +def ensure_hyprland_instance(): + if os.environ.get("HYPRLAND_INSTANCE_SIGNATURE"): + return + + runtime = Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")) + hypr_dir = runtime / "hypr" + if not hypr_dir.is_dir(): + return + + sockets = [] + for socket in hypr_dir.glob("*/.socket.sock"): + try: + sockets.append((socket.stat().st_mtime, socket.parent.name)) + except OSError: + pass + + if sockets: + os.environ["HYPRLAND_INSTANCE_SIGNATURE"] = max(sockets)[1] + + +def actions_path(): + return Path(os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")) / "hyprland-actions.tsv" + + +def rofi_index(entries): + menu = "".join(f"{entry}\n" for entry in entries) + result = subprocess.run( + ["rofi", "-dmenu", "-i", "-p", PROMPT, "-format", "i"], + input=menu, + check=False, + text=True, + capture_output=True, + ) + if result.returncode != 0: + return None + + try: + return int(result.stdout.strip()) + except ValueError: + return None + + +def notify(message): + if shutil.which("notify-send"): + subprocess.run(["notify-send", "Hyprland action", message], check=False) + else: + sys.stderr.write(message + "\n") + + +def hyprland_eval(expression): + return subprocess.run(["hyprctl", "-q", "eval", expression], check=False) + + +def request_actions_file(): + ensure_hyprland_instance() + result = hyprland_eval("_G.im_hyprland_write_actions()") + return result.returncode == 0 + + +def read_actions(): + if not request_actions_file(): + return [] + + actions_file = actions_path() + if not actions_file.is_file(): + return [] + + actions = [] + try: + with actions_file.open() as handle: + for line in handle: + action_id, description, keys = (line.rstrip("\n").split("\t", 2) + ["", ""])[:3] + if action_id and description: + actions.append({"id": action_id, "description": description, "keys": keys}) + except OSError: + return [] + + actions.sort(key=lambda action: (action["description"].lower(), action["keys"].lower())) + return actions + + +def dispatch(action): + result = hyprland_eval(f'_G.im_hyprland_run_action("{action["id"]}")') + if result.returncode != 0: + notify(f"Failed to run action: {action['description']}") + return result.returncode + + +def main(): + actions = read_actions() + if not actions: + notify("No registered Hyprland actions found.") + return 1 + + width = min(max(len(action["description"]) for action in actions), 48) + labels = [f"{action['description']:<{width}} {action['keys']}" for action in actions] + index = rofi_index(labels) + if index is None: + return 0 + if index < 0 or index >= len(actions): + return 1 + + return dispatch(actions[index]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/nixos/hyprland.nix b/nixos/hyprland.nix index a4ee1646..deb03289 100644 --- a/nixos/hyprland.nix +++ b/nixos/hyprland.nix @@ -151,6 +151,18 @@ exec ${../dotfiles/lib/bin/hypr_rofi_layout} "$@" ''; }; + hyprRofiAction = pkgs.writeShellApplication { + name = "hypr_rofi_action"; + runtimeInputs = [ + pkgs.libnotify + pkgs.python3 + pkgs.rofi + hyprlandPackage + ]; + text = '' + exec python3 ${../dotfiles/lib/bin/hypr_rofi_action} "$@" + ''; + }; hyprscratchSettings = { daemon_options = "clean"; global_options = ""; @@ -303,6 +315,7 @@ # For scripts hyprRofiLayout + hyprRofiAction hyprRofiWindow hyprShellUi jq