From 6465ffceeef5c3df404972bf4eaa1b24e451a7c4 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 20:41:29 +0100 Subject: feat: window rows get app icons and a close/move menu --- deckit/modules/windows.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) (limited to 'deckit/modules') diff --git a/deckit/modules/windows.py b/deckit/modules/windows.py index 8642b1c..69b1210 100644 --- a/deckit/modules/windows.py +++ b/deckit/modules/windows.py @@ -8,6 +8,7 @@ from pathlib import Path from ..module import Module, ModuleError ADDR = re.compile(r"0x[0-9a-f]{1,16}") +DESKTOP_BASES = (Path("/usr/share/applications"), Path.home() / ".local/share/applications") def _run(cmd, timeout=2): @@ -21,6 +22,51 @@ def _popen(cmd): subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) +def _dispatch(cmd): + """Run a hyprctl dispatch and wait: the two-step move depends on the order.""" + try: + subprocess.run(cmd, capture_output=True, timeout=2) + except (OSError, subprocess.TimeoutExpired): + pass + + +def _desktop_icons(bases=DESKTOP_BASES) -> dict: + """Window class -> candidate icon names, from the installed .desktop files.""" + mapping = {} + for base in bases: + if not base.is_dir(): + continue + for entry in base.glob("*.desktop"): + try: + text = entry.read_text(errors="replace") + except OSError: + continue + section = None + icon = wmclass = None + for line in text.splitlines(): + line = line.strip() + if line.startswith("[") and line.endswith("]"): + section = line + if section != "[Desktop Entry]": + break + continue + if section != "[Desktop Entry]": + continue + if line.startswith("Icon="): + icon = line.split("=", 1)[1].strip() + elif line.startswith("StartupWMClass="): + wmclass = line.split("=", 1)[1].strip() + if not icon: + continue + candidates = [icon] + if wmclass and wmclass != icon: + candidates.append(wmclass) + for key in (entry.stem, wmclass): + if key: + mapping.setdefault(key.lower(), candidates) + return mapping + + def hypr_lua(cfg: dict) -> bool: mode = cfg.get("hyprland", "auto") if mode == "auto": @@ -64,6 +110,7 @@ def make(cfg: dict) -> Module: lua = hypr_lua(cfg) launch = cfg.get("launch", {}) workspaces = cfg.get("windows", {}).get("workspaces", list(range(1, 11))) + icons = _desktop_icons() def launch_spec(spec): if isinstance(spec, dict): @@ -88,6 +135,18 @@ def make(cfg: dict) -> Module: raise ModuleError("bad window address") _popen(window_cmd("close", addr, lua)) + def move(body): + addr = str(body.get("address", "")) + n = body.get("n") + if not ADDR.fullmatch(addr) or not isinstance(n, int) or not 1 <= n <= 99: + raise ModuleError("bad window or workspace") + if lua: + # window.move acts on the focused window, so focus it first. + _dispatch(["hyprctl", "eval", f'hl.dispatch(hl.dsp.focus({{window = "address:{addr}"}}))']) + _dispatch(["hyprctl", "eval", f'hl.dispatch(hl.dsp.window.move({{workspace = "{n}"}}))']) + else: + _dispatch(["hyprctl", "dispatch", "movetoworkspacesilent", f"{n},address:{addr}"]) + def launch_key(body): key = str(body.get("key", "")) command, _, _ = launch_spec(launch.get(key)) @@ -97,6 +156,8 @@ def make(cfg: dict) -> Module: def state(): st = _state(run) + for win in st["windows"]: + win["icons"] = icons.get(win["class"].lower(), [win["class"].lower()]) known = sorted(set(int(n) for n in workspaces) | set(st["workspaces"])) launches = [] for key, spec in launch.items(): @@ -105,4 +166,5 @@ def make(cfg: dict) -> Module: return {**st, "workspaces": known, "launches": launches} return Module(name="windows", title="Windows", state=state, - actions={"workspace": workspace, "focus": focus, "close": close, "launch": launch_key}) + actions={"workspace": workspace, "focus": focus, "close": close, + "move": move, "launch": launch_key}) -- cgit v1.2.3