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]);