use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::io; use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; use super::api::{Context, ContextConfig}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Config { pub context: String, pub contexts: HashMap, } impl Default for Config { fn default() -> Self { let mut contexts = HashMap::new(); contexts.insert("local".to_string(), ContextConfig::Local); Self { context: "local".to_string(), contexts, } } } /// Helper managing contexts pub struct ContextManager { context: RwLock>, config_path: PathBuf, config: RwLock, } pub static MANAGER: std::sync::LazyLock = std::sync::LazyLock::new(|| ContextManager::new().expect("Cannot setup context manager")); impl ContextManager { fn new() -> io::Result { let proj_dirs = ProjectDirs::from("com", "pkh", "pkh").ok_or_else(|| { io::Error::new( io::ErrorKind::NotFound, "Could not determine config directory", ) })?; let config_dir = proj_dirs.config_dir(); fs::create_dir_all(config_dir)?; let config_path = config_dir.join("contexts.json"); let config = if config_path.exists() { // Load existing configuration file let content = fs::read_to_string(&config_path)?; serde_json::from_str(&content) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? } else { // Create a new configuration file Config::default() }; Ok(Self { context: RwLock::new(Arc::new(Self::make_context( config.context.as_str(), &config, ))), config_path, config: RwLock::new(config), }) } /// Obtain current ContextManager configuration pub fn get_config(&self) -> std::sync::RwLockReadGuard<'_, Config> { self.config.read().unwrap() } /// Make a ContextManager using a specific configuration path pub fn with_path(path: PathBuf) -> Self { let config = Config::default(); Self { context: RwLock::new(Arc::new(Self::make_context("local", &config))), config_path: path, config: RwLock::new(config), } } /// Save current context configuration to disk pub fn save(&self) -> io::Result<()> { let config = self.config.read().unwrap(); let content = serde_json::to_string_pretty(&*config) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; fs::write(&self.config_path, content)?; Ok(()) } fn make_context(name: &str, config: &Config) -> Context { let context_config = config .contexts .get(name) .cloned() .expect("Context not found in config"); Context::new(context_config) } /// List contexts from configuration pub fn list_contexts(&self) -> Vec { self.config .read() .unwrap() .contexts .keys() .cloned() .collect() } /// Add a context to configuration pub fn add_context(&self, name: &str, config: ContextConfig) -> io::Result<()> { self.config .write() .unwrap() .contexts .insert(name.to_string(), config); self.save() } /// Remove context from configuration pub fn remove_context(&self, name: &str) -> io::Result<()> { let mut config = self.config.write().unwrap(); if name == "local" { return Err(io::Error::new( io::ErrorKind::InvalidInput, "Cannot remove local context", )); } if config.contexts.remove(name).is_some() { // If we are removing the current context, fallback to local if name == config.context { config.context = "local".to_string(); self.set_current_ephemeral(Self::make_context("local", &config)); } drop(config); // Drop write lock before saving self.save()?; } Ok(()) } /// Set current context from name (modifying configuration) pub fn set_current(&self, name: &str) -> io::Result<()> { let mut config = self.config.write().unwrap(); if config.contexts.contains_key(name) { config.context = name.to_string(); self.set_current_ephemeral(Self::make_context(name, &config)); drop(config); // Drop write lock before saving self.save()?; Ok(()) } else { Err(io::Error::new( io::ErrorKind::NotFound, format!("Context '{}' not found", name), )) } } /// Set current context, without modifying configuration pub fn set_current_ephemeral(&self, context: Context) { *self.context.write().unwrap() = context.into(); } /// Obtain current context handle pub fn current(&self) -> Arc { self.context.read().unwrap().clone() } /// Obtain current context name /// Will not work for ephemeral context (obtained from config) pub fn current_name(&self) -> String { self.config.read().unwrap().context.clone() } }