diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index b83a0ef3..475f4f17 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -539,7 +539,7 @@ Managed by `useIndexing` hook: ### Vault Config -Per-vault settings stored in `config/ui.config.md`: +Per-vault settings stored in `ui.config.md` at vault root: - Editable as a normal note (YAML frontmatter) - Managed by `useVaultConfig` hook and `vaultConfigStore` - Settings: zoom, view mode, tag colors, status colors, property display modes diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 17e8e733..5c2dbcc3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -466,7 +466,7 @@ Managed by `useVaultSwitcher` hook. Switching vaults closes all tabs and resets ### Vault Config -Per-vault UI settings stored in `config/ui.config.md` (YAML frontmatter in a markdown note): +Per-vault UI settings stored in `ui.config.md` at vault root (YAML frontmatter in a markdown note): - `zoom`: Float zoom level (0.8–1.5) - `view_mode`: "all" | "editor-list" | "editor-only" - `editor_mode`: "raw" | "preview" (persists across tab switches and sessions) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index c56e6246..22b2fc64 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -588,6 +588,8 @@ pub fn repair_vault(vault_path: String) -> Result { // Migrate legacy theme/ directory to root, then repair themes theme::migrate_theme_dir_to_root(&vault_path); theme::restore_default_themes(&vault_path)?; + // Migrate legacy config/ui.config.md → root ui.config.md + vault_config::migrate_ui_config_to_root(&vault_path); // Repair config files (AGENTS.md at root, config.md type def) vault::repair_config_files(&vault_path)?; // Ensure .gitignore with sensible defaults exists diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a59ebdc1..502e81c8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -44,6 +44,8 @@ fn run_startup_tasks() { "Migrated is_a to type on startup", vault::migrate_is_a_to_type(vp_str), ); + // Migrate legacy config/ui.config.md → root ui.config.md + vault_config::migrate_ui_config_to_root(vp_str); log_startup_result( "Migrated hidden_sections to visible property", vault_config::migrate_hidden_sections_to_visible(vp_str), diff --git a/src-tauri/src/vault_config.rs b/src-tauri/src/vault_config.rs index e8629f5c..cea97000 100644 --- a/src-tauri/src/vault_config.rs +++ b/src-tauri/src/vault_config.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::Path; -/// Vault-wide UI configuration stored in `config/ui.config.md`. +/// Vault-wide UI configuration stored in `ui.config.md` at vault root. /// /// This file is a regular vault note with YAML frontmatter, visible in the /// sidebar under the "Config" section and editable like any note. @@ -21,14 +21,13 @@ pub struct VaultConfig { pub property_display_modes: Option>, } -const CONFIG_DIR: &str = "config"; const CONFIG_FILENAME: &str = "ui.config.md"; fn config_path(vault_path: &str) -> std::path::PathBuf { - Path::new(vault_path).join(CONFIG_DIR).join(CONFIG_FILENAME) + Path::new(vault_path).join(CONFIG_FILENAME) } -/// Read the vault-wide UI config from `config/ui.config.md`. +/// Read the vault-wide UI config from `ui.config.md` at vault root. /// Returns default values if the file doesn't exist. pub fn get_vault_config(vault_path: &str) -> Result { let path = config_path(vault_path); @@ -69,19 +68,43 @@ fn parse_vault_config(content: &str) -> Result { .map_err(|e| format!("Failed to parse config: {e}")) } -/// Save the vault-wide UI config to `config/ui.config.md`. -/// Creates the directory and file if they don't exist. +/// Save the vault-wide UI config to `ui.config.md` at vault root. +/// Creates the file if it doesn't exist. pub fn save_vault_config(vault_path: &str, config: VaultConfig) -> Result<(), String> { let path = config_path(vault_path); - let dir = Path::new(vault_path).join(CONFIG_DIR); - if !dir.exists() { - std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create config dir: {e}"))?; - } - let content = serialize_config(&config); std::fs::write(&path, content).map_err(|e| format!("Failed to write config: {e}")) } +/// Migrate legacy `config/ui.config.md` → root `ui.config.md`. +/// If the root file already exists, the legacy file is simply removed. +/// Cleans up empty `config/` directory after migration. +pub fn migrate_ui_config_to_root(vault_path: &str) { + let vault = Path::new(vault_path); + let legacy = vault.join("config").join(CONFIG_FILENAME); + let root = vault.join(CONFIG_FILENAME); + + if legacy.exists() { + if !root.exists() { + // Move legacy content to root + if let Ok(content) = std::fs::read_to_string(&legacy) { + let _ = std::fs::write(&root, content); + } + } + let _ = std::fs::remove_file(&legacy); + } + + // Clean up empty config/ directory + let config_dir = vault.join("config"); + if config_dir.is_dir() { + let is_empty = + std::fs::read_dir(&config_dir).map_or(true, |mut d| d.next().is_none()); + if is_empty { + let _ = std::fs::remove_dir(&config_dir); + } + } +} + /// Serialize VaultConfig to a markdown file with YAML frontmatter. fn serialize_config(config: &VaultConfig) -> String { let mut lines = vec!["---".to_string(), "type: config".to_string()]; @@ -142,7 +165,7 @@ fn yaml_safe_value(value: &str) -> String { } } -/// Migrate `hidden_sections` from `config/ui.config.md` to `visible: false` +/// Migrate `hidden_sections` from `ui.config.md` to `visible: false` /// on Type notes. Returns the number of Type notes updated. /// /// For each type name in `hidden_sections`: @@ -354,11 +377,9 @@ property_display_modes: let dir = tempfile::TempDir::new().unwrap(); let vault_path = dir.path().to_str().unwrap(); - // Create config with hidden_sections - let config_dir = dir.path().join("config"); - std::fs::create_dir_all(&config_dir).unwrap(); + // Create config with hidden_sections at vault root std::fs::write( - config_dir.join("ui.config.md"), + dir.path().join("ui.config.md"), "---\ntype: config\nhidden_sections:\n - Bookmark\n - Recipe\n---\n", ) .unwrap(); @@ -375,7 +396,8 @@ property_display_modes: assert!(recipe.contains("visible: false")); // Config should no longer have hidden_sections - let config_content = std::fs::read_to_string(config_dir.join("ui.config.md")).unwrap(); + let config_content = + std::fs::read_to_string(dir.path().join("ui.config.md")).unwrap(); assert!(!config_content.contains("hidden_sections")); } @@ -384,11 +406,9 @@ property_display_modes: let dir = tempfile::TempDir::new().unwrap(); let vault_path = dir.path().to_str().unwrap(); - // Create config with hidden_sections - let config_dir = dir.path().join("config"); - std::fs::create_dir_all(&config_dir).unwrap(); + // Create config with hidden_sections at vault root std::fs::write( - config_dir.join("ui.config.md"), + dir.path().join("ui.config.md"), "---\ntype: config\nhidden_sections:\n - Project\n---\n", ) .unwrap(); @@ -416,10 +436,8 @@ property_display_modes: let dir = tempfile::TempDir::new().unwrap(); let vault_path = dir.path().to_str().unwrap(); - let config_dir = dir.path().join("config"); - std::fs::create_dir_all(&config_dir).unwrap(); std::fs::write( - config_dir.join("ui.config.md"), + dir.path().join("ui.config.md"), "---\ntype: config\nzoom: 1.0\n---\n", ) .unwrap(); @@ -440,19 +458,15 @@ property_display_modes: let dir = tempfile::TempDir::new().unwrap(); let vault_path = dir.path().to_str().unwrap(); - let config_dir = dir.path().join("config"); - std::fs::create_dir_all(&config_dir).unwrap(); std::fs::write( - config_dir.join("ui.config.md"), + dir.path().join("ui.config.md"), "---\ntype: config\nhidden_sections:\n - Note\n---\n", ) .unwrap(); - // Type note already has visible: false - let type_dir = dir.path().join("type"); - std::fs::create_dir_all(&type_dir).unwrap(); + // Type note already has visible: false at vault root std::fs::write( - type_dir.join("note.md"), + dir.path().join("note.md"), "---\ntype: Type\ntitle: Note\nvisible: false\n---\n", ) .unwrap(); @@ -460,7 +474,7 @@ property_display_modes: let count = migrate_hidden_sections_to_visible(vault_path).unwrap(); assert_eq!(count, 1); - let content = std::fs::read_to_string(type_dir.join("note.md")).unwrap(); + let content = std::fs::read_to_string(dir.path().join("note.md")).unwrap(); // Should have exactly one visible: false, not two assert_eq!(content.matches("visible:").count(), 1); } @@ -478,4 +492,94 @@ property_display_modes: assert_eq!(yaml_safe_key("has space"), "\"has space\""); assert_eq!(yaml_safe_key("has:colon"), "\"has:colon\""); } + + #[test] + fn migrate_ui_config_moves_legacy_to_root() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path().to_str().unwrap(); + + let config_dir = dir.path().join("config"); + std::fs::create_dir_all(&config_dir).unwrap(); + std::fs::write( + config_dir.join("ui.config.md"), + "---\ntype: config\nzoom: 1.5\n---\n", + ) + .unwrap(); + + migrate_ui_config_to_root(vault_path); + + // Root file should exist with migrated content + let content = std::fs::read_to_string(dir.path().join("ui.config.md")).unwrap(); + assert!(content.contains("zoom: 1.5")); + // Legacy file and empty config dir should be gone + assert!(!config_dir.join("ui.config.md").exists()); + assert!(!config_dir.exists()); + } + + #[test] + fn migrate_ui_config_preserves_existing_root() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path().to_str().unwrap(); + + // Root already has content + std::fs::write( + dir.path().join("ui.config.md"), + "---\ntype: config\nzoom: 2.0\n---\n", + ) + .unwrap(); + + // Legacy also exists + let config_dir = dir.path().join("config"); + std::fs::create_dir_all(&config_dir).unwrap(); + std::fs::write( + config_dir.join("ui.config.md"), + "---\ntype: config\nzoom: 1.0\n---\n", + ) + .unwrap(); + + migrate_ui_config_to_root(vault_path); + + // Root should keep its original content + let content = std::fs::read_to_string(dir.path().join("ui.config.md")).unwrap(); + assert!(content.contains("zoom: 2.0")); + // Legacy file should be removed + assert!(!config_dir.join("ui.config.md").exists()); + } + + #[test] + fn migrate_ui_config_keeps_nonempty_config_dir() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path().to_str().unwrap(); + + let config_dir = dir.path().join("config"); + std::fs::create_dir_all(&config_dir).unwrap(); + std::fs::write( + config_dir.join("ui.config.md"), + "---\ntype: config\n---\n", + ) + .unwrap(); + std::fs::write(config_dir.join("other.md"), "Other file").unwrap(); + + migrate_ui_config_to_root(vault_path); + + // config/ should still exist because it has other files + assert!(config_dir.exists()); + assert!(config_dir.join("other.md").exists()); + assert!(!config_dir.join("ui.config.md").exists()); + } + + #[test] + fn save_config_writes_to_vault_root() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path().to_str().unwrap(); + let config = VaultConfig { + zoom: Some(1.0), + ..Default::default() + }; + save_vault_config(vault_path, config).unwrap(); + + // File should be at vault root, not in config/ + assert!(dir.path().join("ui.config.md").exists()); + assert!(!dir.path().join("config").exists()); + } } diff --git a/src/types.ts b/src/types.ts index 5c060cb8..2cfcaa6e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -155,7 +155,7 @@ export interface VaultSettings { theme: string | null } -/** Vault-wide UI configuration stored in config/ui.config.md. */ +/** Vault-wide UI configuration stored in ui.config.md at vault root. */ export interface VaultConfig { zoom: number | null view_mode: string | null