diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-10 10:03:00 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-10 10:03:00 +0100 |
| commit | 819745a39220b49e848422b4bf0ee71dc317fc4d (patch) | |
| tree | 17475ac554a44eec1f9adee45ad0be3a437509d3 /deckit/modules | |
| parent | 5ebfb5850713c038e37baddbadde3320aca2c4e4 (diff) | |
| download | deck-it-819745a39220b49e848422b4bf0ee71dc317fc4d.tar.gz deck-it-819745a39220b49e848422b4bf0ee71dc317fc4d.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'deckit/modules')
| -rw-r--r-- | deckit/modules/windows.py | 80 |
1 files changed, 41 insertions, 39 deletions
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, |
