From fa71508066128124feb1b1110ca8e8f65e831508 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 10 Nov 2026 10:32:59 +0100 Subject: fix: window list flicker and icons missing from the icon theme The Windows tab rebuilt every row once a second, and each rebuild walked every window icon through its fallback names again, so the rows blinked. It now re-renders only when the state changed. Icon lookup only searched the configured theme, so an app the theme lacks (pcmanfm-qt under candy-icons) lost its icon and showed its label. It now falls back to hicolor, as the icon theme spec does, in hicolor's size/category layout. Co-Authored-By: Claude Opus 5.5 --- deckit/server.py | 15 +++++++++------ static/modules/windows.js | 5 +++++ tests/test_server.py | 10 ++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/deckit/server.py b/deckit/server.py index e392041..6bea422 100644 --- a/deckit/server.py +++ b/deckit/server.py @@ -84,12 +84,15 @@ 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" - if path.is_file(): - return path + # hicolor last: the spec's fallback every theme inherits, where apps + # install their own icon when the theme has none for them. + for th in (theme or icon_theme(), "hicolor"): + for base in bases: + # Themes lay out category/size (candy-icons) or size/category (hicolor). + for sub in ("apps/scalable", "apps/64", "apps/48", "apps/32", "scalable/apps"): + path = Path(base) / th / sub / f"{name}.svg" + if path.is_file(): + return path return None diff --git a/static/modules/windows.js b/static/modules/windows.js index d3e882e..7404f03 100644 --- a/static/modules/windows.js +++ b/static/modules/windows.js @@ -110,6 +110,11 @@ export default { }, update(st, el) { + // Polled every second: rebuilding unchanged rows reloads every icon + // through its fallbacks, which flickers, so only a change re-renders. + const key = JSON.stringify(st); + if (key === el._last) return; + el._last = key; const ws = el.querySelector(".ws"); ws.textContent = ""; for (const id of st.workspaces ?? []) { diff --git a/tests/test_server.py b/tests/test_server.py index a8fdf0d..7f2ee1c 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -128,6 +128,16 @@ class Server(unittest.TestCase): self.assertIsNone(srv.icon_path("../x", "candy-icons", [base])) self.assertIsNone(srv.icon_path("nope", "candy-icons", [base])) + def test_icon_path_falls_back_to_hicolor(self): + import tempfile + from pathlib import Path + base = Path(tempfile.mkdtemp()) + (base / "candy-icons" / "apps" / "scalable").mkdir(parents=True) + d = base / "hicolor" / "scalable" / "apps" + d.mkdir(parents=True) + (d / "pcmanfm-qt.svg").write_text("") + self.assertEqual(srv.icon_path("pcmanfm-qt", "candy-icons", [base]), d / "pcmanfm-qt.svg") + def test_art_route(self): from unittest import mock with mock.patch.object(srv, "art", return_value=(b"PNGDATA", "image/png")): -- cgit v1.2.3