aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-09 20:37:01 +0100
committerDanilo M. <danix@danix.xyz>2026-11-09 20:37:01 +0100
commit853f1ed1928aaeca9ea956c24bee12cd56a7cfdd (patch)
treec0ec5e0a1fcc3461d1d063139a77c428f4dce072
parentedaad8e9f6e918c783fd8fb9237fa33032194ed5 (diff)
downloaddeck-it-853f1ed1928aaeca9ea956c24bee12cd56a7cfdd.tar.gz
deck-it-853f1ed1928aaeca9ea956c24bee12cd56a7cfdd.zip
feat: launcher icons follow the icon theme Keystone sets
-rw-r--r--config.toml3
-rw-r--r--deckit/server.py20
-rw-r--r--tests/test_server.py7
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