1use serde::{Deserialize, Serialize};
7
8use crate::error::Result;
9use crate::paths::Paths;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(default)]
13pub struct VaultConfig {
14 pub general: GeneralConfig,
15 pub capture: CaptureConfig,
16 pub appearance: AppearanceConfig,
17 pub categories: CategoriesConfig,
18 pub index: IndexConfig,
19 pub schema_version: u32,
21}
22
23impl Default for VaultConfig {
24 fn default() -> Self {
25 Self {
26 general: GeneralConfig::default(),
27 capture: CaptureConfig::default(),
28 appearance: AppearanceConfig::default(),
29 categories: CategoriesConfig::default(),
30 index: IndexConfig::default(),
31 schema_version: 2,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(default)]
38pub struct GeneralConfig {
39 pub trash_retention_days: u32,
40}
41
42impl Default for GeneralConfig {
43 fn default() -> Self {
44 Self {
45 trash_retention_days: 30,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(default)]
52pub struct CaptureConfig {
53 pub double_tap_threshold_ms: u32,
54 pub overlay_max_height: u32,
55}
56
57impl Default for CaptureConfig {
58 fn default() -> Self {
59 Self {
60 double_tap_threshold_ms: 350,
61 overlay_max_height: 400,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
67#[serde(rename_all = "lowercase")]
68pub enum Theme {
69 #[default]
70 System,
71 Light,
72 Dark,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(default)]
77pub struct AppearanceConfig {
78 pub theme: Theme,
79 pub show_dock_icon: bool,
80}
81
82impl Default for AppearanceConfig {
83 fn default() -> Self {
84 Self {
85 theme: Theme::System,
86 show_dock_icon: true,
87 }
88 }
89}
90pub const AUTO_COLORS: &[&str] = &[
97 "", "oklch(0.78 0.15 75)", "oklch(0.72 0.15 310)", "oklch(0.75 0.12 195)", "oklch(0.75 0.13 145)", ];
103
104pub fn resolve_category_color(id: &str, items: &[CategoryDef]) -> String {
109 if let Some(def) = items.iter().find(|c| c.id == id) {
110 return def.color.clone();
111 }
112 AUTO_COLORS[0].to_string()
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct CategoryDef {
117 pub id: String,
118 pub color: String,
119 #[serde(default)]
120 pub builtin: bool,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(default)]
125pub struct CategoriesConfig {
126 pub items: Vec<CategoryDef>,
127}
128
129impl Default for CategoriesConfig {
130 fn default() -> Self {
131 let ids = ["inbox", "todo", "idea", "bookmark", "snippet"];
132 let items = ids
133 .iter()
134 .zip(AUTO_COLORS.iter())
135 .map(|(id, color)| CategoryDef {
136 id: (*id).to_string(),
137 color: (*color).to_string(),
138 builtin: true,
139 })
140 .collect();
141 Self { items }
142 }
143}
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(default)]
146pub struct IndexConfig {
147 pub watcher_debounce_ms: u32,
148 pub watcher_retry_count: u32,
149 pub watcher_retry_interval_ms: u32,
150}
151
152impl Default for IndexConfig {
153 fn default() -> Self {
154 Self {
155 watcher_debounce_ms: 300,
156 watcher_retry_count: 2,
157 watcher_retry_interval_ms: 200,
158 }
159 }
160}
161
162impl VaultConfig {
163 pub fn load(paths: &Paths) -> Self {
166 let path = paths.config_path();
167 match std::fs::read_to_string(&path) {
168 Ok(text) => match toml::from_str::<Self>(&text) {
169 Ok(c) => c,
170 Err(e) => {
171 tracing::warn!(path = %path.display(), error = %e, "config.toml parse failed; using defaults");
172 Self::default()
173 }
174 },
175 Err(_) => Self::default(),
176 }
177 }
178
179 pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
181 toml::to_string_pretty(self)
182 }
183
184 pub fn save(&self, paths: &Paths) -> Result<()> {
187 let text = self.to_toml()?;
188 let path = paths.config_path();
189 let tmp = path.with_extension("toml.tmp");
193 std::fs::write(&tmp, text)?;
194 std::fs::rename(&tmp, path)?;
195 Ok(())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn defaults_roundtrip() {
205 let c = VaultConfig::default();
206 let s = c.to_toml().unwrap();
207 let back: VaultConfig = toml::from_str(&s).unwrap();
208 assert_eq!(back.general.trash_retention_days, 30);
209 assert_eq!(back.schema_version, 2);
210 }
211
212 #[test]
213 fn unknown_fields_ignored() {
214 let t = r#"schema_version = 1
215[general]
216trash_retention_days = 7
217unknown_future_field = true
218"#;
219 let c: VaultConfig = toml::from_str(t).unwrap();
220 assert_eq!(c.general.trash_retention_days, 7);
221 }
222 #[test]
223 fn save_roundtrips_categories() {
224 let dir = tempfile::tempdir().unwrap();
225 let paths = Paths::resolve(Some(dir.path()));
226 let mut cfg = VaultConfig::default();
227 cfg.categories.items.push(CategoryDef {
228 id: "custom".into(),
229 color: "oklch(0.7 0.1 200)".into(),
230 builtin: false,
231 });
232 cfg.save(&paths).unwrap();
233 let reloaded = VaultConfig::load(&paths);
234 assert!(reloaded.categories.items.iter().any(|c| c.id == "custom"));
235 }
236}