From 8c47071465af1747f9553808e94cefae1588211c Mon Sep 17 00:00:00 2001 From: Jacob Arnould Date: Mon, 23 Feb 2026 19:19:30 +0100 Subject: [PATCH] Add dark mode and live theme sync for custom visualization --- README.md | 2 ++ visualization/src/index.js | 67 +++++++++++++++++++++++++++++++++++++ visualization/src/index.pug | 13 ++++++- 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdde0c4..ad5b08f 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ Once the server is restarted, you can open the Activity view in the web UI, clic It should now work. Click save and you're done! +The custom visualization supports both light and dark mode, and will follow the ActivityWatch Theme setting (including "Auto (System)"). + # Notes This was massively inspired by ulogme by @karpathy, here's a screenshot of how it looks: diff --git a/visualization/src/index.js b/visualization/src/index.js index 34ce534..34e9708 100644 --- a/visualization/src/index.js +++ b/visualization/src/index.js @@ -7,6 +7,70 @@ function add(accumulator, a) { return accumulator + a; } +const VALID_THEMES = ["light", "dark", "auto"]; + +function normalizeTheme(theme) { + return VALID_THEMES.includes(theme) ? theme : null; +} + +function getStoredThemePreference() { + try { + return normalizeTheme(localStorage.getItem("theme")); + } catch (_err) { + return null; + } +} + +function resolveThemePreference() { + return getStoredThemePreference() || "auto"; +} + +function resolveActualTheme(themePreference) { + if (themePreference === "light" || themePreference === "dark") { + return themePreference; + } + + if (window.matchMedia) { + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + return "light"; +} + +function applyTheme(actualTheme) { + document.documentElement.dataset.theme = actualTheme; +} + +function applyCurrentTheme() { + const themePreference = resolveThemePreference(); + const actualTheme = resolveActualTheme(themePreference); + applyTheme(actualTheme); +} + +function installThemeSync() { + window.addEventListener("storage", (event) => { + if (event.key === "theme") { + applyCurrentTheme(); + } + }); + + if (!window.matchMedia) { + return; + } + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const onSystemThemeChange = () => { + if (resolveThemePreference() === "auto") { + applyCurrentTheme(); + } + }; + + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener("change", onSystemThemeChange); + } else if (mediaQuery.addListener) { + mediaQuery.addListener(onSystemThemeChange); + } +} + // TODO: Avoid testing-port assumption const testing = url.port != 5600; @@ -27,6 +91,9 @@ const aw = new aw_client.AWClient("aw-watcher-input", { }); function load() { + applyCurrentTheme(); + installThemeSync(); + const statusEl = document.getElementById("status"); const pressesEl = document.getElementById("presses"); const clicksEl = document.getElementById("clicks"); diff --git a/visualization/src/index.pug b/visualization/src/index.pug index 279938c..50933b1 100644 --- a/visualization/src/index.pug +++ b/visualization/src/index.pug @@ -2,8 +2,19 @@ html head title aw-watcher-input visualization style + | :root { + | --bg: #ffffff; + | --text: #000000; + | --status: #000000; + | } + | html[data-theme="dark"] { + | --bg: #1a1d24; + | --text: #e4e9f2; + | --status: #a5b4cc; + | } | th { text-align: left; } - | body { font-family: sans-serif; } + | body { font-family: sans-serif; background: var(--bg); color: var(--text); } + | #status { color: var(--status); } body p#status