1use crate::errors::{PocketError, PocketResult, IntoPocketError};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::sync::{Arc, Mutex};
7use once_cell::sync::OnceCell;
8use log::{info, debug, error};
9
10static CONFIG: OnceCell<Arc<Mutex<ConfigManager>>> = OnceCell::new();
12
13#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct Config {
16 #[serde(default = "default_config_version")]
18 pub version: String,
19
20 #[serde(default)]
22 pub editor: Option<String>,
23
24 #[serde(default = "default_content_type")]
26 pub default_content_type: String,
27
28 #[serde(default = "default_log_level")]
30 pub log_level: String,
31
32 #[serde(default)]
34 pub hooks_dir: Option<PathBuf>,
35
36 #[serde(default)]
38 pub bin_dir: Option<PathBuf>,
39
40 #[serde(default = "default_max_search_results")]
42 pub max_search_results: usize,
43
44 #[serde(default = "default_search_algorithm")]
46 pub search_algorithm: String,
47
48 #[serde(default)]
50 pub cards: HashMap<String, serde_json::Value>,
51}
52
53fn default_config_version() -> String {
54 "1.0".to_string()
55}
56
57fn default_content_type() -> String {
58 "Code".to_string()
59}
60
61fn default_log_level() -> String {
62 "info".to_string()
63}
64
65fn default_max_search_results() -> usize {
66 10
67}
68
69fn default_search_algorithm() -> String {
70 "fuzzy".to_string()
71}
72
73impl Default for Config {
74 fn default() -> Self {
75 Self {
76 version: default_config_version(),
77 editor: None,
78 default_content_type: default_content_type(),
79 log_level: default_log_level(),
80 hooks_dir: None,
81 bin_dir: None,
82 max_search_results: default_max_search_results(),
83 search_algorithm: default_search_algorithm(),
84 cards: HashMap::new(),
85 }
86 }
87}
88
89#[derive(Debug)]
91pub struct ConfigManager {
92 config: Config,
94
95 config_path: PathBuf,
97
98 data_dir: PathBuf,
100
101 dirty: bool,
103}
104
105impl ConfigManager {
106 pub fn new(data_dir: impl AsRef<Path>) -> PocketResult<Self> {
108 let data_dir = data_dir.as_ref().to_path_buf();
109 let config_path = data_dir.join("config.toml");
110
111 let config = if config_path.exists() {
113 let config_str = fs::read_to_string(&config_path)
115 .config_err(&format!("Failed to read config file: {}", config_path.display()))?;
116
117 match toml::from_str::<Config>(&config_str) {
118 Ok(config) => {
119 debug!("Loaded config from {}", config_path.display());
120 config
121 }
122 Err(e) => {
123 error!("Failed to parse config file: {}", e);
124 error!("Using default config");
125 Config::default()
126 }
127 }
128 } else {
129 info!("No config file found, creating default config");
130 Config::default()
131 };
132
133 let config_exists = config_path.exists();
134
135 let manager = Self {
136 config,
137 config_path,
138 data_dir,
139 dirty: false,
140 };
141
142 if !config_exists {
144 manager.save()?;
145 }
146
147 Ok(manager)
148 }
149
150 pub fn get_config(&self) -> Config {
152 self.config.clone()
153 }
154
155 pub fn update_config(&mut self, config: Config) -> PocketResult<()> {
157 self.config = config;
158 self.dirty = true;
159 Ok(())
160 }
161
162 pub fn get_card_config(&self, card_name: &str) -> Option<serde_json::Value> {
164 self.config.cards.get(card_name).cloned()
165 }
166
167 pub fn set_card_config(&mut self, card_name: &str, config: serde_json::Value) -> PocketResult<()> {
169 self.config.cards.insert(card_name.to_string(), config);
170 self.dirty = true;
171 Ok(())
172 }
173
174 pub fn save(&self) -> PocketResult<()> {
176 let config_str = toml::to_string_pretty(&self.config)
177 .config_err("Failed to serialize config")?;
178
179 if let Some(parent) = self.config_path.parent() {
181 fs::create_dir_all(parent)
182 .config_err(&format!("Failed to create config directory: {}", parent.display()))?;
183 }
184
185 fs::write(&self.config_path, config_str)
187 .config_err(&format!("Failed to write config to {}", self.config_path.display()))?;
188
189 debug!("Saved config to {}", self.config_path.display());
190 Ok(())
191 }
192
193 pub fn get_data_dir(&self) -> PathBuf {
195 self.data_dir.clone()
196 }
197
198 pub fn get_hooks_dir(&self) -> PathBuf {
200 match &self.config.hooks_dir {
201 Some(dir) => dir.clone(),
202 None => self.data_dir.join("hooks"),
203 }
204 }
205
206 pub fn get_bin_dir(&self) -> PathBuf {
208 match &self.config.bin_dir {
209 Some(dir) => dir.clone(),
210 None => self.data_dir.join("bin"),
211 }
212 }
213}
214
215impl Drop for ConfigManager {
216 fn drop(&mut self) {
217 if self.dirty {
218 match self.save() {
219 Ok(_) => {}
220 Err(e) => error!("Failed to save config on drop: {}", e),
221 }
222 }
223 }
224}
225
226pub fn init(data_dir: impl AsRef<Path>) -> PocketResult<()> {
228 let config_manager = ConfigManager::new(data_dir)?;
229 let _ = CONFIG.set(Arc::new(Mutex::new(config_manager)));
230 Ok(())
231}
232
233pub fn get() -> PocketResult<Arc<Mutex<ConfigManager>>> {
235 match CONFIG.get() {
236 Some(config) => Ok(config.clone()),
237 None => Err(PocketError::Config("Configuration not initialized".to_string())),
238 }
239}
240
241pub fn get_config() -> PocketResult<Config> {
243 let config = get()?;
244 let config_guard = config.lock()
245 .map_err(|_| PocketError::Config("Failed to lock config".to_string()))?;
246 Ok(config_guard.get_config())
247}
248
249pub fn save() -> PocketResult<()> {
251 let config = get()?;
252 let config_guard = config.lock()
253 .map_err(|_| PocketError::Config("Failed to lock config".to_string()))?;
254 config_guard.save()
255}