diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-10 10:02:44 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-10 10:02:44 +0100 |
| commit | 5ebfb5850713c038e37baddbadde3320aca2c4e4 (patch) | |
| tree | a2842edacb1e7f93f34d2492c6cab9f8cd51dbe7 | |
| parent | 2c7cef2024c201fa9c7c406954121de337071689 (diff) | |
| download | deck-it-5ebfb5850713c038e37baddbadde3320aca2c4e4.tar.gz deck-it-5ebfb5850713c038e37baddbadde3320aca2c4e4.zip | |
fix: follow UDT's font size and live theme changes
UDT gives font sizes to keystone's fonts page, which writes
~/.config/keystone/fonts.json. deck-it hardcoded px sizes, so changing
the size on the fonts page did nothing on the tablet. theme.css now
carries --udt-font-size (override.deckit, else base, 8 to 40px, as
quickshell checks) on html, and deck.css sizes text in rem, scaled from
the old 14px design.
The page loaded theme.css once, so a scheme, wallpaper or size change
reached the tablet only after a reload. The shell now re-reads it every
5s and on visibility, swapping it into a <style> only when it changed,
and sets meta theme-color from --udt-base instead of Macchiato's
hardcoded base.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | AGENTS.md | 3 | ||||
| -rw-r--r-- | deckit/theme.py | 26 | ||||
| -rw-r--r-- | static/app.js | 22 | ||||
| -rw-r--r-- | static/deck.css | 55 | ||||
| -rw-r--r-- | tests/test_theme.py | 10 |
5 files changed, 87 insertions, 29 deletions
@@ -23,6 +23,9 @@ Keep the server dependency-free apart from that one package. mounts panels and polls `/api/<module>`. - `deckit/theme.py` reads UDT's palette (`~/.config/rofi/udt/palette.rasi`) and `~/.cache/udt/udt-border.lua`; the frontend uses only those CSS variables. + Text size is keystone's (`~/.config/keystone/fonts.json`, `override.deckit` + else `base`) as `--udt-font-size` on `html`, so `deck.css` sizes text in + `rem`. The shell re-reads `/theme.css` every 5s, so changes reach an open page. - Launcher icons follow the icon theme Keystone sets (`gsettings org.gnome.desktop.interface icon-theme`); window icons resolve through the `.desktop` `Icon`/`StartupWMClass` mapping. diff --git a/deckit/theme.py b/deckit/theme.py index 2ed3d51..142a79f 100644 --- a/deckit/theme.py +++ b/deckit/theme.py @@ -21,6 +21,7 @@ wallpaper accent to ~/.cache/udt/udt-accent.rasi. deck-it reads both rather than restating the colours, so a scheme or wallpaper change reaches the tablet. """ +import json import re from pathlib import Path @@ -31,6 +32,9 @@ BORDER = re.compile(r"rgb\(\s*([0-9a-fA-F]{6})\s*\)") PALETTE_PATH = Path.home() / ".config" / "rofi" / "udt" / "palette.rasi" ACCENT_PATH = Path.home() / ".cache" / "udt" / "udt-accent.rasi" BORDER_PATH = Path.home() / ".cache" / "udt" / "udt-border.lua" +# Keystone's fonts page owns text sizes: deck-it's own override, else the base. +FONTS_PATH = Path.home() / ".config" / "keystone" / "fonts.json" +FONT_SIZE = 14 # px, the size deck.css was designed at # Catppuccin Macchiato, used before UDT has ever generated its files. FALLBACK = { @@ -58,8 +62,22 @@ def parse_border(text: str) -> list[str]: return ["#" + value.lower() for value in BORDER.findall(text)] -def css(colors: dict, accent: str | None, border: list[str] | None = None) -> str: +def parse_font_size(text: str) -> int | None: + """deck-it's px size from keystone's fonts.json, range-checked as quickshell does.""" + try: + data = json.loads(text) + px = (data.get("override") or {}).get("deckit", data.get("base")) + except (ValueError, AttributeError): + return None + if isinstance(px, (int, float)) and not isinstance(px, bool) and 8 <= px <= 40: + return round(px) + return None + + +def css(colors: dict, accent: str | None, border: list[str] | None = None, + font_size: int | None = None) -> str: lines = [":root {"] + lines.append(f" --udt-font-size: {font_size or FONT_SIZE}px;") lines += [f" --udt-{name}: {value};" for name, value in colors.items()] lines.append(f" --udt-accent: {accent or FALLBACK['accent']};") stops = border if border and len(border) >= 3 else [ @@ -86,4 +104,8 @@ def load() -> str: border = parse_border(BORDER_PATH.read_text()) except OSError: border = None - return css(colors, accent, border) + try: + font_size = parse_font_size(FONTS_PATH.read_text()) + except OSError: + font_size = None + return css(colors, accent, border, font_size) diff --git a/static/app.js b/static/app.js index b89fbaa..e2da02a 100644 --- a/static/app.js +++ b/static/app.js @@ -230,6 +230,28 @@ tabBody.addEventListener("pointerup", (e) => { }); tabBody.addEventListener("pointercancel", () => { swipe = null; }); +// theme.css is rendered per request from UDT's live files, so re-reading it +// carries a scheme, wallpaper or font-size change to an open page. The <style> +// comes after the <link>, so it wins once filled. +const themeStyle = document.head.appendChild(document.createElement("style")); +const themeColor = document.querySelector('meta[name="theme-color"]'); + +async function refreshTheme() { + try { + const r = await fetch("theme.css", { cache: "no-store" }); + if (!r.ok) return; + const text = await r.text(); + if (text !== themeStyle.textContent) themeStyle.textContent = text; + } catch { + // keep the current theme; the next tick retries + } + const base = getComputedStyle(document.documentElement).getPropertyValue("--udt-base").trim(); + if (base) themeColor.content = base; +} +refreshers.push(refreshTheme); +refreshTheme(); +setInterval(refreshTheme, 5000); + document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") refreshers.forEach((fn) => fn()); }); diff --git a/static/deck.css b/static/deck.css index 684d336..2484e1b 100644 --- a/static/deck.css +++ b/static/deck.css @@ -18,6 +18,7 @@ * { box-sizing: border-box; margin: 0; } +html { font-size: var(--udt-font-size, 14px); } html, body { height: 100%; } body { @@ -25,7 +26,7 @@ body { flex-direction: column; background: var(--udt-base); color: var(--udt-text); - font: 400 14px/20px "Noto Sans", system-ui, sans-serif; + font: 400 1rem/1.43 "Noto Sans", system-ui, sans-serif; -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; -webkit-touch-callout: none; @@ -126,7 +127,7 @@ body { .panel .panel-head h2 { flex: 1; margin: 0; - font-size: 13px; + font-size: 0.929rem; font-weight: 600; letter-spacing: .02em; color: var(--udt-text); @@ -145,7 +146,7 @@ body { .panel-body { flex: 1; min-height: 0; overflow: auto; padding: 14px; } .section-label { - font-size: 11px; + font-size: 0.786rem; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); @@ -167,11 +168,11 @@ body { align-items: center; justify-content: center; color: var(--udt-overlay0); - font-size: 22px; + font-size: 1.571rem; } .music .meta { min-width: 0; } .music .title { - font-size: 17px; + font-size: 1.214rem; font-weight: 600; overflow: hidden; text-overflow: ellipsis; @@ -179,13 +180,13 @@ body { } .music .controls button { min-height: 40px; min-width: 44px; } .music .artist, .music .album { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.music .album { font-size: 12px; color: var(--udt-overlay0); } +.music .album { font-size: 0.857rem; color: var(--udt-overlay0); } .music .seek { display: flex; align-items: center; gap: 10px; margin: 6px 0 12px; } .music .seek input { flex: 1; accent-color: var(--udt-accent); } -.music .time { font-size: 12px; color: var(--udt-subtext0); white-space: nowrap; } +.music .time { font-size: 0.857rem; color: var(--udt-subtext0); white-space: nowrap; } .music .controls { display: flex; flex-wrap: wrap; gap: 8px; } -.sbb .sbb-now { margin-bottom: 8px; font-size: 12px; font-weight: 600; } +.sbb .sbb-now { margin-bottom: 8px; font-size: 0.857rem; font-weight: 600; } .grid-stack-item .ui-resizable-se { width: 22px; height: 22px; right: 2px; bottom: 2px; opacity: .45; } .grid-stack-item:hover .ui-resizable-se { opacity: 1; } @@ -237,17 +238,17 @@ select { background: var(--udt-surface1); min-width: 0; } -.mlabel { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } +.mlabel { font-size: 0.714rem; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } .mvalue { - font-size: 26px; + font-size: 1.857rem; font-weight: 600; line-height: 1.1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.msub { font-size: 12px; color: var(--udt-subtext0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.mfoot { margin-top: 10px; font-size: 12px; color: var(--udt-overlay0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.msub { font-size: 0.857rem; color: var(--udt-subtext0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.mfoot { margin-top: 10px; font-size: 0.857rem; color: var(--udt-overlay0); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .windows { display: flex; flex-direction: column; gap: 14px; } .windows .ws { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; } @@ -258,7 +259,7 @@ select { display: flex; align-items: center; justify-content: center; - font-size: 18px; + font-size: 1.286rem; font-weight: 600; border-radius: 12px; background: var(--udt-surface1); @@ -282,7 +283,7 @@ select { .wrow.on { color: var(--udt-accent); } .wt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } .wicon { width: 24px; height: 24px; object-fit: contain; flex: none; } -.wmenu { flex: none; width: 40px; height: 40px; padding: 0; font-size: 18px; line-height: 1; } +.wmenu { flex: none; width: 40px; height: 40px; padding: 0; font-size: 1.286rem; line-height: 1; } .windows .launches { display: flex; flex-wrap: wrap; gap: 8px; } .wpop { @@ -299,11 +300,11 @@ select { border-radius: 12px; box-shadow: 0 12px 30px rgba(0, 0, 0, .35); } -.wpop-title { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.wpop-label { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } +.wpop-title { font-weight: 600; font-size: 0.929rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.wpop-label { font-size: 0.786rem; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } .wpop-close { text-align: left; } .wpop-ws { display: flex; flex-wrap: wrap; gap: 6px; } -.wpop-ws button { min-width: 44px; min-height: 44px; padding: 0; font-size: 16px; font-weight: 600; } +.wpop-ws button { min-width: 44px; min-height: 44px; padding: 0; font-size: 1.143rem; font-weight: 600; } .llm { display: flex; flex-direction: column; gap: 10px; } .llm-sec { @@ -328,7 +329,7 @@ select { .brand.claude { -webkit-mask-image: url(/logos/claude.svg); mask-image: url(/logos/claude.svg); } .brand.opencode { -webkit-mask-image: url(/logos/opencode.svg); mask-image: url(/logos/opencode.svg); } .llm-head h3 { - font-size: 12px; + font-size: 0.857rem; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-subtext0); @@ -337,7 +338,7 @@ select { .llm-head .model { margin-left: auto; max-width: 60%; - font-size: 12px; + font-size: 0.857rem; color: var(--udt-overlay0); overflow: hidden; text-overflow: ellipsis; @@ -346,8 +347,8 @@ select { .gauges { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 12px; } .gauge { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; border-radius: 12px; background: var(--udt-surface1); } -.glabel { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } -.gvalue { font-size: 24px; font-weight: 600; line-height: 1.1; } +.glabel { font-size: 0.714rem; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); } +.gvalue { font-size: 1.714rem; font-weight: 600; line-height: 1.1; } .gbar { height: 6px; background: var(--udt-surface2); border-radius: 3px; overflow: hidden; } .gbar > i { display: block; height: 100%; background: var(--udt-accent); } .lbig { display: flex; flex-direction: column; gap: 2px; padding: 10px 12px; border-radius: 12px; background: var(--udt-surface1); } @@ -370,7 +371,7 @@ select { min-height: 92px; padding: 12px; font: inherit; - font-size: 17px; + font-size: 1.214rem; font-weight: 600; border-radius: 16px; cursor: pointer; @@ -379,7 +380,7 @@ select { color: var(--udt-text); } .deck-btn .ico { width: 28px; height: 28px; } -.deck-btn .num { font-size: 20px; } +.deck-btn .num { font-size: 1.429rem; } .deck-btn.rec.on { background: var(--udt-red); border-color: var(--udt-red); color: var(--udt-crust); } .deck-btn.live.on { background: var(--udt-green); border-color: var(--udt-green); color: var(--udt-crust); } .deck-btn:active { background: var(--udt-surface1); } @@ -394,7 +395,7 @@ select { padding: 12px; } .deck-block h3 { - font-size: 11px; + font-size: 0.786rem; text-transform: uppercase; letter-spacing: .08em; color: var(--udt-overlay0); @@ -410,7 +411,7 @@ select { min-height: 74px; padding: 10px; font: inherit; - font-size: 14px; + font-size: 1rem; text-align: center; border-radius: 12px; cursor: pointer; @@ -421,7 +422,7 @@ select { } .tile { position: relative; } .tile .ico { width: 22px; height: 22px; flex: none; } -.tile .gear { position: absolute; right: 6px; top: 4px; font-style: normal; font-size: 11px; opacity: .6; } +.tile .gear { position: absolute; right: 6px; top: 4px; font-style: normal; font-size: 0.786rem; opacity: .6; } .tile span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tile.on { background: var(--udt-accent); @@ -444,7 +445,7 @@ select { .chan .mute.on { background: var(--udt-red); border-color: var(--udt-red); color: var(--udt-crust); } .chan-name { flex: none; min-width: 84px; max-width: 34%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chan input[type=range] { flex: 1; min-width: 60px; accent-color: var(--udt-accent); } -.chan-pct { flex: none; min-width: 40px; text-align: right; font-size: 12px; } +.chan-pct { flex: none; min-width: 40px; text-align: right; font-size: 0.857rem; } .deck-offline { flex: 1; diff --git a/tests/test_theme.py b/tests/test_theme.py index c1fe1de..f14b0f1 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -55,6 +55,16 @@ class Theme(unittest.TestCase): for index, value in enumerate(["#111111", "#222222", "#333333"], 1): self.assertIn(f"--udt-border-{index}: {value}", out) + def test_font_size_prefers_override_then_base(self): + self.assertEqual(theme.parse_font_size('{"base": 16, "override": {"deckit": 18.6}}'), 19) + self.assertEqual(theme.parse_font_size('{"base": 16, "override": {"rofi": 14}}'), 16) + for bad in ('{"base": 99}', '{"base": "16"}', "not json", "[]"): + self.assertIsNone(theme.parse_font_size(bad)) + + def test_css_exposes_font_size(self): + self.assertIn("--udt-font-size: 17px", theme.css({}, None, font_size=17)) + self.assertIn(f"--udt-font-size: {theme.FONT_SIZE}px", theme.css({}, None)) + def test_load_falls_back_when_files_are_missing(self): saved = (theme.PALETTE_PATH, theme.ACCENT_PATH) theme.PALETTE_PATH = Path("/nonexistent/palette.rasi") |
