refactor: move vault UI config from ui.config.md to localStorage

Replace the in-vault ui.config.md file with localStorage persistence
keyed by vault path. Vaults now contain only user content — no app
config files. Removes the Rust vault_config module, Tauri commands
(get/save_vault_config), startup migrations, and demo vault config files.

The reactive store (vaultConfigStore) and consumer hooks (useZoom,
useViewMode, useRawMode) are unchanged — only the storage backend moved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-03-23 21:07:38 +01:00
parent 67e620c872
commit 8a86995c23
7 changed files with 35 additions and 645 deletions

View File

@@ -1,5 +0,0 @@
---
type: config
zoom: 1.3
view_mode: all
---

View File

@@ -1,5 +0,0 @@
---
type: config
zoom: 1.3
view_mode: all
---

View File

@@ -14,9 +14,8 @@ use crate::indexing::{IndexStatus, IndexingProgress};
use crate::search::SearchResponse;
use crate::settings::Settings;
use crate::vault::{RenameResult, VaultEntry};
use crate::vault_config::VaultConfig;
use crate::vault_list::VaultList;
use crate::{frontmatter, git, github, indexing, menu, search, vault, vault_config, vault_list};
use crate::{frontmatter, git, github, indexing, menu, search, vault, vault_list};
/// Expand a leading `~` or `~/` in a path string to the user's home directory.
/// Returns the original string unchanged if it doesn't start with `~` or if the
@@ -524,8 +523,6 @@ pub fn repair_vault(vault_path: String) -> Result<String, String> {
vault::migrate_is_a_to_type(&vault_path)?;
// Flatten vault: move notes from type-based subfolders to root
vault::flatten_vault(&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
@@ -578,18 +575,6 @@ pub fn save_vault_list(list: VaultList) -> Result<(), String> {
vault_list::save_vault_list(&list)
}
#[tauri::command]
pub fn get_vault_config(vault_path: String) -> Result<VaultConfig, String> {
let vault_path = expand_tilde(&vault_path);
vault_config::get_vault_config(&vault_path)
}
#[tauri::command]
pub fn save_vault_config(vault_path: String, config: VaultConfig) -> Result<(), String> {
let vault_path = expand_tilde(&vault_path);
vault_config::save_vault_config(&vault_path, config)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -10,7 +10,6 @@ pub mod menu;
pub mod search;
pub mod settings;
pub mod vault;
pub mod vault_config;
pub mod vault_list;
use std::process::Child;
@@ -43,13 +42,6 @@ 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),
);
// Migrate legacy config/agents.md → root AGENTS.md (one-time, idempotent)
vault::migrate_agents_md(vp_str);
// Seed AGENTS.md and config.md at vault root if missing
@@ -165,9 +157,7 @@ pub fn run() {
commands::get_default_vault_path,
commands::register_mcp_tools,
commands::check_mcp_status,
commands::repair_vault,
commands::get_vault_config,
commands::save_vault_config
commands::repair_vault
])
.build(tauri::generate_context!())
.expect("error while building tauri application")

View File

@@ -1,579 +0,0 @@
use gray_matter::engine::YAML;
use gray_matter::Matter;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
/// 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.
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct VaultConfig {
pub zoom: Option<f64>,
pub view_mode: Option<String>,
pub editor_mode: Option<String>,
#[serde(default)]
pub tag_colors: Option<HashMap<String, String>>,
#[serde(default)]
pub status_colors: Option<HashMap<String, String>>,
#[serde(default)]
pub property_display_modes: Option<HashMap<String, String>>,
}
const CONFIG_FILENAME: &str = "ui.config.md";
fn config_path(vault_path: &str) -> std::path::PathBuf {
Path::new(vault_path).join(CONFIG_FILENAME)
}
/// 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<VaultConfig, String> {
let path = config_path(vault_path);
if !path.exists() {
return Ok(VaultConfig::default());
}
let content =
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read config: {e}"))?;
parse_vault_config(&content)
}
/// Parse VaultConfig from markdown content with YAML frontmatter.
fn parse_vault_config(content: &str) -> Result<VaultConfig, String> {
let matter = Matter::<YAML>::new();
let parsed = matter.parse(content);
let hash = match parsed.data {
Some(gray_matter::Pod::Hash(map)) => map,
_ => return Ok(VaultConfig::default()),
};
let json_map: serde_json::Map<String, serde_json::Value> =
hash.into_iter().map(|(k, v)| (k, pod_to_json(v))).collect();
let json = serde_json::Value::Object(json_map);
serde_json::from_value(json.clone())
.or_else(|_| {
// If direct deserialization fails, strip the `type` field and retry
let mut map = match json {
serde_json::Value::Object(m) => m,
_ => return Ok(VaultConfig::default()),
};
map.remove("type");
serde_json::from_value(serde_json::Value::Object(map))
})
.map_err(|e| format!("Failed to parse config: {e}"))
}
/// 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 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()];
if let Some(zoom) = config.zoom {
lines.push(format!("zoom: {zoom}"));
}
if let Some(ref mode) = config.view_mode {
lines.push(format!("view_mode: {mode}"));
}
if let Some(ref mode) = config.editor_mode {
lines.push(format!("editor_mode: {mode}"));
}
append_string_map(&mut lines, "tag_colors", config.tag_colors.as_ref());
append_string_map(&mut lines, "status_colors", config.status_colors.as_ref());
append_string_map(
&mut lines,
"property_display_modes",
config.property_display_modes.as_ref(),
);
lines.push("---".to_string());
lines.join("\n") + "\n"
}
/// Append a YAML map section with sorted keys for stable output.
fn append_string_map(lines: &mut Vec<String>, key: &str, map: Option<&HashMap<String, String>>) {
if let Some(m) = map {
if !m.is_empty() {
lines.push(format!("{key}:"));
let mut entries: Vec<_> = m.iter().collect();
entries.sort_by_key(|(k, _)| k.to_owned());
for (k, v) in entries {
lines.push(format!(" {}: {}", yaml_safe_key(k), yaml_safe_value(v)));
}
}
}
}
/// Quote a YAML key if it contains special characters.
fn yaml_safe_key(key: &str) -> String {
if key.contains(':') || key.contains('#') || key.contains(' ') {
format!("\"{}\"", key.replace('"', "\\\""))
} else {
key.to_string()
}
}
/// Quote a YAML value if it contains special characters.
fn yaml_safe_value(value: &str) -> String {
if value.contains(':')
|| value.contains('#')
|| value.starts_with('"')
|| value.starts_with('\'')
{
format!("\"{}\"", value.replace('"', "\\\""))
} else {
value.to_string()
}
}
/// 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`:
/// - If `<slug>.md` exists at vault root, adds `visible: false` to its frontmatter
/// - If it doesn't exist, creates it with `type: Type`, `title: <name>`, `visible: false`
/// - Re-saves the config without `hidden_sections`
pub fn migrate_hidden_sections_to_visible(vault_path: &str) -> Result<usize, String> {
let path = config_path(vault_path);
if !path.exists() {
return Ok(0);
}
let content =
std::fs::read_to_string(&path).map_err(|e| format!("Failed to read config: {e}"))?;
let hidden = extract_hidden_sections(&content);
if hidden.is_empty() {
return Ok(0);
}
let vault = Path::new(vault_path);
let mut migrated = 0;
for type_name in &hidden {
let slug = type_name_to_slug(type_name);
let type_path = vault.join(format!("{slug}.md"));
if type_path.exists() {
let type_content = std::fs::read_to_string(&type_path)
.map_err(|e| format!("Failed to read {}: {e}", type_path.display()))?;
if !type_content.contains("visible:") {
let updated = crate::frontmatter::update_frontmatter_content(
&type_content,
"visible",
Some(crate::frontmatter::FrontmatterValue::Bool(false)),
)
.map_err(|e| format!("Failed to update {}: {e}", type_path.display()))?;
std::fs::write(&type_path, updated)
.map_err(|e| format!("Failed to write {}: {e}", type_path.display()))?;
}
} else {
let new_content = format!(
"---\ntype: Type\ntitle: {}\nvisible: false\n---\n\n# {}\n",
type_name, type_name
);
std::fs::write(&type_path, new_content)
.map_err(|e| format!("Failed to write {}: {e}", type_path.display()))?;
}
migrated += 1;
}
// Re-save config without hidden_sections
let config = parse_vault_config(&content)?;
save_vault_config(vault_path, config)?;
Ok(migrated)
}
/// Extract `hidden_sections` from raw YAML frontmatter.
fn extract_hidden_sections(content: &str) -> Vec<String> {
let matter = Matter::<YAML>::new();
let parsed = matter.parse(content);
let hash = match parsed.data {
Some(gray_matter::Pod::Hash(map)) => map,
_ => return vec![],
};
match hash.get("hidden_sections") {
Some(gray_matter::Pod::Array(arr)) => arr
.iter()
.filter_map(|v| match v {
gray_matter::Pod::String(s) => Some(s.clone()),
_ => None,
})
.collect(),
_ => vec![],
}
}
/// Convert a Type name to a filesystem slug.
fn type_name_to_slug(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_ascii_alphanumeric() {
c.to_ascii_lowercase()
} else {
'-'
}
})
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
/// Convert gray_matter::Pod to serde_json::Value.
fn pod_to_json(pod: gray_matter::Pod) -> serde_json::Value {
match pod {
gray_matter::Pod::String(s) => serde_json::Value::String(s),
gray_matter::Pod::Integer(i) => serde_json::json!(i),
gray_matter::Pod::Float(f) => serde_json::json!(f),
gray_matter::Pod::Boolean(b) => serde_json::Value::Bool(b),
gray_matter::Pod::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(pod_to_json).collect())
}
gray_matter::Pod::Hash(map) => {
let obj: serde_json::Map<String, serde_json::Value> =
map.into_iter().map(|(k, v)| (k, pod_to_json(v))).collect();
serde_json::Value::Object(obj)
}
gray_matter::Pod::Null => serde_json::Value::Null,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty_returns_defaults() {
let config = parse_vault_config("").unwrap();
assert!(config.zoom.is_none());
assert!(config.view_mode.is_none());
assert!(config.tag_colors.is_none());
}
#[test]
fn parse_full_config() {
let content = r#"---
type: config
zoom: 1.1
view_mode: all
tag_colors:
engineering: blue
personal: green
status_colors:
Active: green
Done: blue
property_display_modes:
deadline: date
---
"#;
let config = parse_vault_config(content).unwrap();
assert_eq!(config.zoom, Some(1.1));
assert_eq!(config.view_mode.as_deref(), Some("all"));
let tags = config.tag_colors.unwrap();
assert_eq!(tags.get("engineering").unwrap(), "blue");
assert_eq!(tags.get("personal").unwrap(), "green");
let statuses = config.status_colors.unwrap();
assert_eq!(statuses.get("Active").unwrap(), "green");
let props = config.property_display_modes.unwrap();
assert_eq!(props.get("deadline").unwrap(), "date");
}
#[test]
fn roundtrip_serialization() {
let mut tag_colors = HashMap::new();
tag_colors.insert("work".to_string(), "blue".to_string());
let config = VaultConfig {
zoom: Some(1.2),
view_mode: Some("editor-only".to_string()),
editor_mode: None,
tag_colors: Some(tag_colors),
status_colors: None,
property_display_modes: None,
};
let serialized = serialize_config(&config);
let parsed = parse_vault_config(&serialized).unwrap();
assert_eq!(parsed.zoom, Some(1.2));
assert_eq!(parsed.view_mode.as_deref(), Some("editor-only"));
assert_eq!(parsed.tag_colors.unwrap().get("work").unwrap(), "blue");
}
#[test]
fn get_config_missing_file() {
let dir = tempfile::TempDir::new().unwrap();
let config = get_vault_config(dir.path().to_str().unwrap()).unwrap();
assert!(config.zoom.is_none());
}
#[test]
fn save_and_read_config() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
let mut status_colors = HashMap::new();
status_colors.insert("Active".to_string(), "green".to_string());
let config = VaultConfig {
zoom: Some(0.9),
view_mode: None,
editor_mode: None,
tag_colors: None,
status_colors: Some(status_colors),
property_display_modes: None,
};
save_vault_config(vault_path, config).unwrap();
let loaded = get_vault_config(vault_path).unwrap();
assert_eq!(loaded.zoom, Some(0.9));
assert_eq!(
loaded.status_colors.unwrap().get("Active").unwrap(),
"green"
);
}
#[test]
fn migrate_hidden_sections_creates_type_notes_with_visible_false() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
// Create config with hidden_sections at vault root
std::fs::write(
dir.path().join("ui.config.md"),
"---\ntype: config\nhidden_sections:\n - Bookmark\n - Recipe\n---\n",
)
.unwrap();
let count = migrate_hidden_sections_to_visible(vault_path).unwrap();
assert_eq!(count, 2);
// Check type notes were created
let bookmark = std::fs::read_to_string(dir.path().join("bookmark.md")).unwrap();
assert!(bookmark.contains("visible: false"));
assert!(bookmark.contains("title: Bookmark"));
let recipe = std::fs::read_to_string(dir.path().join("recipe.md")).unwrap();
assert!(recipe.contains("visible: false"));
// Config should no longer have hidden_sections
let config_content = std::fs::read_to_string(dir.path().join("ui.config.md")).unwrap();
assert!(!config_content.contains("hidden_sections"));
}
#[test]
fn migrate_hidden_sections_updates_existing_type_note() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
// Create config with hidden_sections at vault root
std::fs::write(
dir.path().join("ui.config.md"),
"---\ntype: config\nhidden_sections:\n - Project\n---\n",
)
.unwrap();
// Create existing type note at vault root without visible
std::fs::write(
dir.path().join("project.md"),
"---\ntype: Type\ntitle: Project\nicon: briefcase\n---\n\n# Project\n",
)
.unwrap();
let count = migrate_hidden_sections_to_visible(vault_path).unwrap();
assert_eq!(count, 1);
let content = std::fs::read_to_string(dir.path().join("project.md")).unwrap();
assert!(content.contains("visible: false"));
assert!(
content.contains("icon: briefcase"),
"should preserve existing fields"
);
}
#[test]
fn migrate_hidden_sections_skips_when_no_hidden_sections() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
std::fs::write(
dir.path().join("ui.config.md"),
"---\ntype: config\nzoom: 1.0\n---\n",
)
.unwrap();
let count = migrate_hidden_sections_to_visible(vault_path).unwrap();
assert_eq!(count, 0);
}
#[test]
fn migrate_hidden_sections_skips_when_no_config_file() {
let dir = tempfile::TempDir::new().unwrap();
let count = migrate_hidden_sections_to_visible(dir.path().to_str().unwrap()).unwrap();
assert_eq!(count, 0);
}
#[test]
fn migrate_hidden_sections_does_not_duplicate_visible() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().to_str().unwrap();
std::fs::write(
dir.path().join("ui.config.md"),
"---\ntype: config\nhidden_sections:\n - Note\n---\n",
)
.unwrap();
// Type note already has visible: false at vault root
std::fs::write(
dir.path().join("note.md"),
"---\ntype: Type\ntitle: Note\nvisible: false\n---\n",
)
.unwrap();
let count = migrate_hidden_sections_to_visible(vault_path).unwrap();
assert_eq!(count, 1);
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);
}
#[test]
fn type_name_to_slug_converts_names() {
assert_eq!(type_name_to_slug("Project"), "project");
assert_eq!(type_name_to_slug("Weekly Review"), "weekly-review");
assert_eq!(type_name_to_slug("My Note!"), "my-note");
}
#[test]
fn yaml_safe_key_quoting() {
assert_eq!(yaml_safe_key("simple"), "simple");
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());
}
}

View File

@@ -1,6 +1,4 @@
import { useEffect, useCallback, useSyncExternalStore } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from '../mock-tauri'
import type { VaultConfig } from '../types'
import { initStatusColors } from '../utils/statusStyles'
import { initTagColors } from '../utils/tagStyles'
@@ -14,8 +12,32 @@ import {
} from '../utils/vaultConfigStore'
import { migrateLocalStorageToVaultConfig } from '../utils/configMigration'
function tauriCall<T>(command: string, tauriArgs: Record<string, unknown>, mockArgs?: Record<string, unknown>): Promise<T> {
return isTauri() ? invoke<T>(command, tauriArgs) : mockInvoke<T>(command, mockArgs ?? tauriArgs)
const STORAGE_PREFIX = 'laputa:vault-config:'
function storageKey(vaultPath: string): string {
return `${STORAGE_PREFIX}${vaultPath}`
}
function loadFromStorage(vaultPath: string): VaultConfig {
const DEFAULT: VaultConfig = {
zoom: null, view_mode: null, editor_mode: null,
tag_colors: null, status_colors: null, property_display_modes: null,
}
try {
const raw = localStorage.getItem(storageKey(vaultPath))
if (!raw) return DEFAULT
return { ...DEFAULT, ...JSON.parse(raw) }
} catch {
return DEFAULT
}
}
function saveToStorage(vaultPath: string, config: VaultConfig): void {
try {
localStorage.setItem(storageKey(vaultPath), JSON.stringify(config))
} catch (err) {
console.warn('Failed to save vault config:', err)
}
}
function applyToModules(c: VaultConfig): void {
@@ -24,34 +46,18 @@ function applyToModules(c: VaultConfig): void {
initDisplayModeOverrides(c.property_display_modes ?? {})
}
function persistConfig(vaultPath: string, config: VaultConfig): void {
tauriCall<void>('save_vault_config', { vaultPath, config })
.catch((err) => console.warn('Failed to save vault config:', err))
}
export function useVaultConfig(vaultPath: string) {
const config = useSyncExternalStore(subscribeVaultConfig, getVaultConfig)
useEffect(() => {
resetVaultConfigStore()
tauriCall<VaultConfig>('get_vault_config', { vaultPath })
.then((loaded) => {
const migrated = migrateLocalStorageToVaultConfig(loaded)
const needsSave = migrated !== loaded
bindVaultConfigStore(migrated, (c) => persistConfig(vaultPath, c))
applyToModules(migrated)
if (needsSave) persistConfig(vaultPath, migrated)
})
.catch((err) => {
console.warn('Failed to load vault config:', err)
const migrated = migrateLocalStorageToVaultConfig(null)
bindVaultConfigStore(migrated, (c) => persistConfig(vaultPath, c))
applyToModules(migrated)
if (migrated.zoom !== null || migrated.tag_colors !== null || migrated.status_colors !== null) {
persistConfig(vaultPath, migrated)
}
})
const loaded = loadFromStorage(vaultPath)
const migrated = migrateLocalStorageToVaultConfig(loaded)
const needsSave = migrated !== loaded
bindVaultConfigStore(migrated, (c) => saveToStorage(vaultPath, c))
applyToModules(migrated)
if (needsSave) saveToStorage(vaultPath, migrated)
return () => resetVaultConfigStore()
}, [vaultPath])

View File

@@ -3,7 +3,7 @@
* Each handler simulates a Tauri backend command.
*/
import type { VaultEntry, VaultConfig, ModifiedFile, Settings, DeviceFlowStart, DeviceFlowPollResult, GitHubUser, GitPullResult, GitPushResult, GitRemoteStatus, LastCommitInfo, PulseCommit } from '../types'
import type { VaultEntry, ModifiedFile, Settings, DeviceFlowStart, DeviceFlowPollResult, GitHubUser, GitPullResult, GitPushResult, GitRemoteStatus, LastCommitInfo, PulseCommit } from '../types'
import { MOCK_CONTENT } from './mock-content'
import { MOCK_ENTRIES } from './mock-entries'
@@ -269,8 +269,6 @@ export const mockHandlers: Record<string, (args: any) => any> = {
start_indexing: () => null,
trigger_incremental_index: () => null,
repair_vault: (): string => 'Vault repaired',
get_vault_config: (): VaultConfig => ({ zoom: null, view_mode: null, editor_mode: null, tag_colors: null, status_colors: null, property_display_modes: null }),
save_vault_config: (): null => null,
}
export function addMockEntry(_entry: VaultEntry, content: string): void {