aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md3
-rw-r--r--deckit/theme.py26
-rw-r--r--static/app.js22
-rw-r--r--static/deck.css55
-rw-r--r--tests/test_theme.py10
5 files changed, 87 insertions, 29 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 490e769..c25dd1f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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")