aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 18:15:22 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 18:15:22 +0100
commit70deef7bc561f377eda231cf2b20e7e605b72623 (patch)
treeaa59c0407d742fa545bf693cf89a88afd288e888
parent8b677faf7c69edb355bb96f4beb040178bdece8b (diff)
downloaddeck-it-70deef7bc561f377eda231cf2b20e7e605b72623.tar.gz
deck-it-70deef7bc561f377eda231cf2b20e7e605b72623.zip
feat: windows module
-rw-r--r--deckit/modules/windows.py92
-rw-r--r--tests/test_windows.py23
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()