pocket_cli/
config.rs

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
10/// Global configuration instance
11static CONFIG: OnceCell<Arc<Mutex<ConfigManager>>> = OnceCell::new();
12
13/// Configuration for the Pocket CLI
14#[derive(Debug, Serialize, Deserialize, Clone)]
15pub struct Config {
16    /// Version of the configuration
17    #[serde(default = "default_config_version")]
18    pub version: String,
19    
20    /// Editor command to use (respects $EDITOR env var)
21    #[serde(default)]
22    pub editor: Option<String>,
23    
24    /// Default content type
25    #[serde(default = "default_content_type")]
26    pub default_content_type: String,
27    
28    /// Log level
29    #[serde(default = "default_log_level")]
30    pub log_level: String,
31    
32    /// Path to the hooks directory
33    #[serde(default)]
34    pub hooks_dir: Option<PathBuf>,
35    
36    /// Path to the bin directory for executable hooks
37    #[serde(default)]
38    pub bin_dir: Option<PathBuf>,
39    
40    /// Maximum search results to display
41    #[serde(default = "default_max_search_results")]
42    pub max_search_results: usize,
43    
44    /// Search algorithm to use
45    #[serde(default = "default_search_algorithm")]
46    pub search_algorithm: String,
47    
48    /// Card configurations
49    #[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/// Manager for the Pocket CLI configuration
90#[derive(Debug)]
91pub struct ConfigManager {
92    /// The configuration itself
93    config: Config,
94    
95    /// Path to the configuration file
96    config_path: PathBuf,
97    
98    /// Path to the data directory
99    data_dir: PathBuf,
100    
101    /// Dirty flag to indicate if the config needs to be saved
102    dirty: bool,
103}
104
105impl ConfigManager {
106    /// Create a new configuration manager
107    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        // Create a default config
112        let config = if config_path.exists() {
113            // Load the existing config
114            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        // Save the default config if it doesn't exist
143        if !config_exists {
144            manager.save()?;
145        }
146        
147        Ok(manager)
148    }
149    
150    /// Get the configuration
151    pub fn get_config(&self) -> Config {
152        self.config.clone()
153    }
154    
155    /// Update the configuration
156    pub fn update_config(&mut self, config: Config) -> PocketResult<()> {
157        self.config = config;
158        self.dirty = true;
159        Ok(())
160    }
161    
162    /// Get a value from the card configuration
163    pub fn get_card_config(&self, card_name: &str) -> Option<serde_json::Value> {
164        self.config.cards.get(card_name).cloned()
165    }
166    
167    /// Set a value in the card configuration
168    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    /// Save the configuration to disk
175    pub fn save(&self) -> PocketResult<()> {
176        let config_str = toml::to_string_pretty(&self.config)
177            .config_err("Failed to serialize config")?;
178        
179        // Create parent directories if they don't exist
180        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        // Write the config file
186        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    /// Get the data directory
194    pub fn get_data_dir(&self) -> PathBuf {
195        self.data_dir.clone()
196    }
197    
198    /// Get the hooks directory
199    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    /// Get the bin directory
207    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
226/// Initialize the global configuration
227pub 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
233/// Get the global configuration manager
234pub 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
241/// Get a copy of the current configuration
242pub 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
249/// Save the current configuration
250pub 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}