From 853f1ed1928aaeca9ea956c24bee12cd56a7cfdd Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 20:37:01 +0100 Subject: feat: launcher icons follow the icon theme Keystone sets --- config.toml | 3 ++- deckit/server.py | 20 +++++++++++++++++--- tests/test_server.py | 7 +++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/config.toml b/config.toml index 66ece24..797d3d8 100644 --- a/config.toml +++ b/config.toml @@ -61,7 +61,8 @@ host = "buildsystem" workspaces = [1, 2, 3, 4, 5, 6, 7, 8] [icons] -# Launcher icons are read from this installed icon theme. +# Launcher icons follow the icon theme Keystone sets (gsettings +# org.gnome.desktop.interface icon-theme); this is only the fallback. theme = "candy-icons" [launch] diff --git a/deckit/server.py b/deckit/server.py index 01e7ec1..c6cc7e8 100644 --- a/deckit/server.py +++ b/deckit/server.py @@ -14,6 +14,7 @@ import secrets import socketserver import subprocess import threading +import time from http.cookies import CookieError, SimpleCookie from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path @@ -29,7 +30,8 @@ ICON = re.compile(r"[A-Za-z0-9._-]{1,64}") ICON_BASES = (Path.home() / ".local/share/icons", Path("/usr/share/icons"), Path.home() / ".icons") CFG = config.load_config() -ICON_THEME = CFG.get("icons", {}).get("theme", "candy-icons") +FALLBACK_ICON_THEME = CFG.get("icons", {}).get("theme", "candy-icons") +_theme_cache = {"t": 0.0, "v": None} MONITOR = CFG.get("display", {}).get("name", "TAB") PORT = CFG.get("ports", {}).get("command_center", 8765) VNC_PORT = CFG.get("ports", {}).get("display", 5900) @@ -51,10 +53,22 @@ def _run(cmd, timeout=2): return "" -def icon_path(name: str, theme: str = ICON_THEME, bases=ICON_BASES): +def icon_theme() -> str: + """The icon theme Keystone's appearance drawer sets via gsettings.""" + now = time.monotonic() + if _theme_cache["v"] and now - _theme_cache["t"] < 5: + return _theme_cache["v"] + out = _run(["gsettings", "get", "org.gnome.desktop.interface", "icon-theme"]) + theme = out.strip().strip("'\"") or FALLBACK_ICON_THEME + _theme_cache.update(t=now, v=theme) + return theme + + +def icon_path(name: str, theme: str | None = None, bases=ICON_BASES): """An app icon from the installed icon theme, so launchers follow the theme.""" if not ICON.fullmatch(name): return None + theme = theme or icon_theme() for base in bases: for sub in ("apps/scalable", "apps/64", "apps/48", "apps/32"): path = Path(base) / theme / sub / f"{name}.svg" @@ -133,7 +147,7 @@ class Handler(BaseHTTPRequestHandler): if path.startswith("/icons/"): name = path[len("/icons/"):].removesuffix(".svg") icon = icon_path(name) - return self._send(200, icon.read_bytes(), "image/svg+xml", cache=True) if icon else self._send(404) + return self._send(200, icon.read_bytes(), "image/svg+xml") if icon else self._send(404) if path.startswith("/api/"): if not self._authed(): return self._send(401) diff --git a/tests/test_server.py b/tests/test_server.py index 1e942c2..1244f25 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -94,6 +94,13 @@ class Server(unittest.TestCase): def test_icons_route_rejects_traversal(self): self.assertEqual(self.req("GET", "/icons/..%2Fconfig.toml", HOST)[0], 404) + def test_icon_theme_follows_gsettings(self): + from unittest import mock + srv._theme_cache.update(t=0, v=None) + with mock.patch.object(srv, "_run", return_value="'some-theme'"): + self.assertEqual(srv.icon_theme(), "some-theme") + srv._theme_cache.update(t=0, v=None) + def test_icon_path_resolves_and_rejects(self): import tempfile from pathlib import Path -- cgit v1.2.3