diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-09 18:15:22 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-09 18:15:22 +0100 |
| commit | 70deef7bc561f377eda231cf2b20e7e605b72623 (patch) | |
| tree | aa59c0407d742fa545bf693cf89a88afd288e888 | |
| parent | 8b677faf7c69edb355bb96f4beb040178bdece8b (diff) | |
| download | deck-it-70deef7bc561f377eda231cf2b20e7e605b72623.tar.gz deck-it-70deef7bc561f377eda231cf2b20e7e605b72623.zip | |
feat: windows module
| -rw-r--r-- | deckit/modules/windows.py | 92 | ||||
| -rw-r--r-- | tests/test_windows.py | 23 |
2 files changed, 115 insertions, 0 deletions
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}) diff --git a/tests/test_windows.py b/tests/test_windows.py new file mode 100644 index 0000000..1573a77 --- /dev/null +++ b/tests/test_windows.py @@ -0,0 +1,23 @@ +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import unittest + +import deckit.modules.windows as windows + + +class Commands(unittest.TestCase): + def test_lua_and_classic(self): + self.assertEqual(windows.workspace_cmd(3, lua=False), + ["hyprctl", "dispatch", "workspace", "3"]) + self.assertEqual(windows.workspace_cmd(3, lua=True)[:2], ["hyprctl", "eval"]) + + def test_address_guard(self): + self.assertIsNone(windows.ADDR.fullmatch("0x1;rm")) + self.assertTrue(windows.ADDR.fullmatch("0x55897e5c0800")) + + +if __name__ == "__main__": + unittest.main() |
