From 70deef7bc561f377eda231cf2b20e7e605b72623 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 18:15:22 +0100 Subject: feat: windows module --- deckit/modules/windows.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 deckit/modules/windows.py (limited to 'deckit/modules') diff --git a/deckit/modules/windows.py b/deckit/modules/windows.py new file mode 100644 index 0000000..022bdd7 --- /dev/null +++ b/deckit/modules/windows.py @@ -0,0 +1,92 @@ +"""Hyprland windows, workspaces, and launching config-declared commands.""" + +import json +import re +import subprocess +from pathlib import Path + +from ..module import Module, ModuleError + +ADDR = re.compile(r"0x[0-9a-f]{1,16}") + + +def _run(cmd, timeout=2): + try: + return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout).stdout.strip() + except (OSError, subprocess.TimeoutExpired): + return "" + + +def _popen(cmd): + subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def hypr_lua(cfg: dict) -> bool: + mode = cfg.get("hyprland", "auto") + if mode == "auto": + return (Path.home() / ".config/hypr/hyprland.lua").is_file() + return mode == "lua" + + +def workspace_cmd(n: int, lua: bool): + if lua: + return ["hyprctl", "eval", f'hl.dispatch(hl.dsp.focus({{workspace = "{n}"}}))'] + return ["hyprctl", "dispatch", "workspace", str(n)] + + +def window_cmd(verb: str, addr: str, lua: bool): + if lua: + dsp = "hl.dsp.focus" if verb == "focus" else "hl.dsp.window.close" + return ["hyprctl", "eval", f'{dsp}({{window = "address:{addr}"}})'] + return ["hyprctl", "dispatch", "focuswindow" if verb == "focus" else "closewindow", f"address:{addr}"] + + +def _state(run): + try: + clients = json.loads(run(["hyprctl", "clients", "-j"]) or "[]") + active = json.loads(run(["hyprctl", "activewindow", "-j"]) or "{}").get("address") + monitors = json.loads(run(["hyprctl", "monitors", "-j"]) or "[]") + except json.JSONDecodeError: + return {"windows": [], "workspaces": [], "activeClass": None} + wins = sorted( + ({"address": c["address"], "class": c["class"], "title": c["title"], + "workspace": c["workspace"]["id"], "active": c["address"] == active} + for c in clients if c.get("mapped", True) and c["workspace"]["id"] > 0), + key=lambda w: (w["workspace"], w["class"])) + active_class = next((w["class"] for w in wins if w["active"]), None) + return {"windows": wins, "workspaces": [m["activeWorkspace"]["id"] for m in monitors], + "activeClass": active_class} + + +def make(cfg: dict) -> Module: + run = cfg.get("run", _run) + lua = hypr_lua(cfg) + launch = cfg.get("launch", {}) + + def workspace(body): + n = body.get("n") + if not isinstance(n, int) or not 1 <= n <= 99: + raise ModuleError("workspace must be a number from 1 to 99") + _popen(workspace_cmd(n, lua)) + + def focus(body): + addr = str(body.get("address", "")) + if not ADDR.fullmatch(addr): + raise ModuleError("bad window address") + _popen(window_cmd("focus", addr, lua)) + + def close(body): + addr = str(body.get("address", "")) + if not ADDR.fullmatch(addr): + raise ModuleError("bad window address") + _popen(window_cmd("close", addr, lua)) + + def launch_key(body): + key = str(body.get("key", "")) + command = launch.get(key) + if command is None: + raise ModuleError("unknown launch key") + _popen(["sh", "-c", command] if isinstance(command, str) else command) + + return Module(name="windows", title="Windows", state=lambda: _state(run), + actions={"workspace": workspace, "focus": focus, "close": close, "launch": launch_key}) -- cgit v1.2.3