fix: make 'New theme' command work in Tauri app (#156)
Three root causes fixed: 1. _themes/ directory was never auto-seeded for existing vaults - list_themes now seeds built-in themes if the directory is missing 2. Creating a theme produced no visible feedback - now switches to the newly created theme after creation 3. No live reload when theme files are edited externally - added focus-based theme reload so edits are picked up when the window regains focus Also adds seed_default_themes to run_startup_tasks for the default vault, ensuring themes are available on first launch. Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@@ -327,6 +327,7 @@ fn create_theme(vault_path: String, source_id: Option<String>) -> Result<String,
|
||||
theme::create_theme(&vault_path, source_id.as_deref())
|
||||
}
|
||||
|
||||
|
||||
fn log_startup_result(label: &str, result: Result<usize, String>) {
|
||||
match result {
|
||||
Ok(n) if n > 0 => log::info!("{}: {} files", label, n),
|
||||
@@ -353,6 +354,9 @@ fn run_startup_tasks() {
|
||||
vault::migrate_is_a_to_type(vp_str),
|
||||
);
|
||||
|
||||
// Seed _themes/ with built-in themes if missing
|
||||
theme::seed_default_themes(vp_str);
|
||||
|
||||
// Register Laputa MCP server in Claude Code and Cursor configs
|
||||
match mcp::register_mcp(vp_str) {
|
||||
Ok(status) => log::info!("MCP registration: {status}"),
|
||||
|
||||
@@ -28,8 +28,12 @@ pub struct VaultSettings {
|
||||
}
|
||||
|
||||
/// List all theme files in _themes/ directory of the vault.
|
||||
/// Seeds built-in themes if the directory is missing.
|
||||
pub fn list_themes(vault_path: &str) -> Result<Vec<ThemeFile>, String> {
|
||||
let themes_dir = Path::new(vault_path).join("_themes");
|
||||
if !themes_dir.is_dir() {
|
||||
seed_default_themes(vault_path);
|
||||
}
|
||||
if !themes_dir.is_dir() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -112,6 +116,22 @@ pub fn get_theme(vault_path: &str, theme_id: &str) -> Result<ThemeFile, String>
|
||||
parse_theme_file(&path)
|
||||
}
|
||||
|
||||
/// Seed the `_themes/` directory with built-in themes if it doesn't exist yet.
|
||||
/// Safe to call multiple times — only writes files that are missing.
|
||||
pub fn seed_default_themes(vault_path: &str) {
|
||||
let themes_dir = Path::new(vault_path).join("_themes");
|
||||
if themes_dir.is_dir() {
|
||||
return;
|
||||
}
|
||||
if fs::create_dir_all(&themes_dir).is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = fs::write(themes_dir.join("default.json"), DEFAULT_THEME);
|
||||
let _ = fs::write(themes_dir.join("dark.json"), DARK_THEME);
|
||||
let _ = fs::write(themes_dir.join("minimal.json"), MINIMAL_THEME);
|
||||
log::info!("Seeded _themes/ with built-in themes");
|
||||
}
|
||||
|
||||
/// Create a new theme file by copying the active theme (or default).
|
||||
/// Returns the ID of the new theme.
|
||||
pub fn create_theme(vault_path: &str, source_id: Option<&str>) -> Result<String, String> {
|
||||
@@ -318,12 +338,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_themes_empty_when_no_dir() {
|
||||
fn test_list_themes_seeds_defaults_when_no_dir() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let vault = dir.path().join("empty-vault");
|
||||
fs::create_dir_all(&vault).unwrap();
|
||||
let themes = list_themes(vault.to_str().unwrap()).unwrap();
|
||||
assert!(themes.is_empty());
|
||||
assert_eq!(themes.len(), 3);
|
||||
let names: Vec<&str> = themes.iter().map(|t| t.name.as_str()).collect();
|
||||
assert!(names.contains(&"Default"));
|
||||
assert!(names.contains(&"Dark"));
|
||||
assert!(names.contains(&"Minimal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -286,7 +286,11 @@ function App() {
|
||||
onGoBack: handleGoBack, onGoForward: handleGoForward,
|
||||
canGoBack: navHistory.canGoBack, canGoForward: navHistory.canGoForward,
|
||||
themes: themeManager.themes, activeThemeId: themeManager.activeThemeId,
|
||||
onSwitchTheme: themeManager.switchTheme, onCreateTheme: () => themeManager.createTheme(),
|
||||
onSwitchTheme: themeManager.switchTheme,
|
||||
onCreateTheme: async () => {
|
||||
const newId = await themeManager.createTheme()
|
||||
if (newId) await themeManager.switchTheme(newId)
|
||||
},
|
||||
})
|
||||
|
||||
const { status: updateStatus, actions: updateActions } = useUpdater()
|
||||
|
||||
@@ -161,14 +161,15 @@ describe('useThemeManager', () => {
|
||||
|
||||
it('handles load failure gracefully', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
mockInvokeFn.mockRejectedValueOnce(new Error('disk error'))
|
||||
mockInvokeFn.mockRejectedValue(new Error('disk error'))
|
||||
|
||||
const { result } = renderHook(() => useThemeManager('/vault'))
|
||||
await new Promise(r => setTimeout(r, 50))
|
||||
await waitFor(() => {
|
||||
expect(warnSpy).toHaveBeenCalledWith('Failed to load themes:', expect.any(Error))
|
||||
})
|
||||
|
||||
expect(result.current.themes).toHaveLength(0)
|
||||
expect(result.current.activeThemeId).toBeNull()
|
||||
expect(warnSpy).toHaveBeenCalledWith('Failed to load themes:', expect.any(Error))
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ function applyThemeToDom(theme: ThemeFile): void {
|
||||
const root = document.documentElement
|
||||
for (const [key, value] of Object.entries(theme.colors)) {
|
||||
root.style.setProperty(`--theme-${key}`, value)
|
||||
// Also set the shadcn-compatible variables
|
||||
root.style.setProperty(`--${key}`, value)
|
||||
}
|
||||
for (const [key, value] of Object.entries(theme.typography)) {
|
||||
@@ -21,7 +20,6 @@ function applyThemeToDom(theme: ThemeFile): void {
|
||||
for (const [key, value] of Object.entries(theme.spacing)) {
|
||||
root.style.setProperty(`--theme-${key}`, value)
|
||||
}
|
||||
// Map sidebar-background to --sidebar (shadcn convention)
|
||||
if (theme.colors['sidebar-background']) {
|
||||
root.style.setProperty('--sidebar', theme.colors['sidebar-background'])
|
||||
}
|
||||
@@ -89,6 +87,13 @@ export function useThemeManager(vaultPath: string | null): ThemeManager {
|
||||
useEffect(() => { loadThemes() }, [loadThemes]) // eslint-disable-line react-hooks/set-state-in-effect -- trigger initial load
|
||||
useEffect(() => { syncThemeDom(prevThemeRef, activeTheme) }, [activeTheme])
|
||||
|
||||
// Reload themes when window regains focus (live reload for external edits)
|
||||
useEffect(() => {
|
||||
const onFocus = () => { loadThemes() }
|
||||
window.addEventListener('focus', onFocus)
|
||||
return () => window.removeEventListener('focus', onFocus)
|
||||
}, [loadThemes])
|
||||
|
||||
const switchTheme = useCallback(async (themeId: string) => {
|
||||
if (!vaultPath) return
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user