From 819745a39220b49e848422b4bf0ee71dc317fc4d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 10 Nov 2026 10:03:00 +0100 Subject: feat: dashboard-only mode and keystone settings integration The second display is now optional: [display] enabled = false skips the headless output, wayvnc, AVNC and the VNC tunnel, so the tablet runs the dashboard alone. `deck-it env` reports it as EXTEND. Keystone's new deck-it page (quickshell, desktop/modules/deckit) edits the config and the autostart line, which needed: - The user config is merged over the shipped config.toml key by key within each table instead of replacing it whole, so the page writes a sparse file and an unset key falls back to the default. - `deck-it defaults` prints the shipped config as JSON for the page's placeholders, so keystone never copies the defaults. - `deck-it status` prints one JSON line (server, display, waiting, tablet, extend) for the page's status line and restart check. - [launch] is now `apps`, a list of desktop ids run with gtk-launch, with name and icon read from the .desktop file, so the page can pick launchers from the installed apps. The old command tables are gone; the client still sends only an id, which must be listed and installed. A launcher whose icon the theme lacks shows its name. - [wifi] address gives the tablet's address in config. The last address that worked is kept in ~/.config/deck-it/last-wifi: `stop` deletes the runtime serial file, so a restart from the page used to wait forever. - `stop` also ends a login's `wifi --wait`, matched by its own command line only, so repeated restarts do not pile up waiters. Also fixed: `deck-it env` fell through into the usage text, and the usage text printed the license header since the header was added. Co-Authored-By: Claude Opus 5.5 --- deckit/modules/windows.py | 80 ++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 39 deletions(-) (limited to 'deckit/modules') diff --git a/deckit/modules/windows.py b/deckit/modules/windows.py index 5457b16..1907b59 100644 --- a/deckit/modules/windows.py +++ b/deckit/modules/windows.py @@ -14,7 +14,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Hyprland windows, workspaces, and launching config-declared commands.""" +"""Hyprland windows, workspaces, and launching config-listed apps.""" import json import re @@ -24,7 +24,8 @@ 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") +# The user's own entries first, so they win over the system's, as in a launcher. +DESKTOP_BASES = (Path.home() / ".local/share/applications", Path("/usr/share/applications")) def _run(cmd, timeout=2): @@ -46,40 +47,47 @@ def _dispatch(cmd): pass -def _desktop_icons(bases=DESKTOP_BASES) -> dict: - """Window class -> candidate icon names, from the installed .desktop files.""" - mapping = {} +def _desktop_entries(bases=DESKTOP_BASES) -> dict: + """Desktop id -> {name, icon, wmclass}, from each [Desktop Entry] section.""" + entries = {} for base in bases: if not base.is_dir(): continue for entry in base.glob("*.desktop"): + if entry.stem in entries: + continue try: text = entry.read_text(errors="replace") except OSError: continue - section = None - icon = wmclass = None + found, inside = {}, False for line in text.splitlines(): line = line.strip() - if line.startswith("[") and line.endswith("]"): - section = line - if section != "[Desktop Entry]": + if line.startswith("["): + if inside: 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) + inside = line == "[Desktop Entry]" + elif inside and "=" in line: + key, value = line.split("=", 1) + if key in ("Name", "Icon", "StartupWMClass"): + found.setdefault(key, value.strip()) + entries[entry.stem] = {"name": found.get("Name"), "icon": found.get("Icon"), + "wmclass": found.get("StartupWMClass")} + return entries + + +def _desktop_icons(entries: dict) -> dict: + """Window class -> candidate icon names, by desktop id and StartupWMClass.""" + mapping = {} + for stem, e in entries.items(): + if not e["icon"]: + continue + candidates = [e["icon"]] + if e["wmclass"] and e["wmclass"] != e["icon"]: + candidates.append(e["wmclass"]) + for key in (stem, e["wmclass"]): + if key: + mapping.setdefault(key.lower(), candidates) return mapping @@ -124,14 +132,10 @@ def _state(run): def make(cfg: dict) -> Module: run = cfg.get("run", _run) lua = hypr_lua(cfg) - launch = cfg.get("launch", {}) + apps = [str(a) for a in cfg.get("launch", {}).get("apps", [])] workspaces = cfg.get("windows", {}).get("workspaces", list(range(1, 11))) - icons = _desktop_icons() - - def launch_spec(spec): - if isinstance(spec, dict): - return spec.get("command"), spec.get("icon"), spec.get("label") - return spec, None, None + entries = _desktop_entries() + icons = _desktop_icons(entries) def workspace(body): n = body.get("n") @@ -164,21 +168,19 @@ def make(cfg: dict) -> Module: _dispatch(["hyprctl", "dispatch", "movetoworkspacesilent", f"{n},address:{addr}"]) def launch_key(body): + # Only a configured, installed desktop id; gtk-launch runs its Exec. key = str(body.get("key", "")) - command, _, _ = launch_spec(launch.get(key)) - if command is None: + if key not in apps or key not in entries: raise ModuleError("unknown launch key") - _popen(["sh", "-c", command] if isinstance(command, str) else command) + _popen(["gtk-launch", key]) 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(): - _, icon, label = launch_spec(spec) - launches.append({"key": key, "icon": icon, "label": label or key}) + launches = [{"key": a, "icon": entries[a]["icon"], "label": entries[a]["name"] or a} + for a in apps if a in entries] return {**st, "workspaces": known, "launches": launches} return Module(name="windows", title="Windows", state=state, -- cgit v1.2.3