From daa6fbbfd71ebb6b3e4dbb37cac0b08783f8f7d8 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Sun, 7 Jun 2026 19:54:12 +0200 Subject: [PATCH] feat(dev): --theme-watch flag for live theme authoring (#1019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debug builds only: `--theme-watch ` polls a local theme file and pushes it into the running app on save (a lightweight Rust watcher thread emits a Tauri event). The frontend installs the CSS under its [data-theme=''] selector and applies it, so the existing syncInjectedThemes effect re-injects live — no zip re-import, no reload. Double-gated (debug_assertions + import.meta.env.DEV); never wired in production. --- src-tauri/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ src/App.tsx | 22 ++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6b005c8b..b9b8f1d2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -115,6 +115,38 @@ pub fn run() { let _ = window.set_title("Psysonic (Dev)"); } + // ── Dev: `--theme-watch ` live theme reload ───────── + // Poll a local theme.css and push it into the running app on save, + // so theme authors get a live loop without re-importing a zip. The + // frontend (dev only) installs it under the id in its + // `[data-theme='']` selector and applies it. Dev-builds only. + #[cfg(debug_assertions)] + { + let args: Vec = std::env::args().collect(); + if let Some(i) = args.iter().position(|a| a == "--theme-watch") { + match args.get(i + 1).cloned() { + Some(path) => { + eprintln!("[theme-watch] watching {path}"); + let handle = app.handle().clone(); + std::thread::spawn(move || { + let p = std::path::PathBuf::from(&path); + let mut last_css = String::new(); + loop { + if let Ok(css) = std::fs::read_to_string(&p) { + if css != last_css { + last_css = css.clone(); + let _ = handle.emit("theme-watch:css", css); + } + } + std::thread::sleep(std::time::Duration::from_millis(300)); + } + }); + } + None => eprintln!("[theme-watch] usage: --theme-watch "), + } + } + } + // ── Analysis cache (SQLite) ─────────────────────────────────── { let cache = analysis_cache::AnalysisCache::init(app.handle()) diff --git a/src/App.tsx b/src/App.tsx index 7193db6a..7109a497 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,28 @@ export default function App() { syncInjectedThemes(installedThemes); }, [installedThemes]); + // Dev only: `--theme-watch ` (debug builds) pushes a local theme's + // CSS in on every save. Install it under the id in its `[data-theme='']` + // selector and apply it — the syncInjectedThemes effect above re-injects, so + // authoring is live without re-importing a zip. Never wired in production. + useEffect(() => { + if (!import.meta.env.DEV) return; + let unlisten: (() => void) | undefined; + void import('@tauri-apps/api/event').then(({ listen }) => { + const sub = listen('theme-watch:css', ({ payload }) => { + const id = payload.match(/\[data-theme=['"]([^'"]+)['"]\]/)?.[1]; + if (!id) return; + useInstalledThemesStore.getState().install({ + id, name: id, author: 'dev', version: '0.0.0', description: '', mode: 'dark', css: payload, installedAt: Date.now(), + }); + useThemeStore.getState().setTheme(id); + }); + // Guard the mocked-in-tests case where listen() isn't a promise. + if (sub && typeof sub.then === 'function') sub.then(u => { unlisten = u; }); + }).catch(() => {}); + return () => unlisten?.(); + }, []); + useEffect(() => { document.documentElement.setAttribute('data-theme', effectiveTheme); }, [effectiveTheme]);