Files
pkh/src/context/manager.rs
Valentin Haudiquet b3365afe5b
All checks were successful
CI / build (push) Successful in 7m21s
docs: added documentation, enforced documentation
2026-01-01 18:37:40 +01:00

180 lines
5.5 KiB
Rust

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<String, ContextConfig>,
}
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<Arc<Context>>,
config_path: PathBuf,
config: RwLock<Config>,
}
pub static MANAGER: std::sync::LazyLock<ContextManager> =
std::sync::LazyLock::new(|| ContextManager::new().expect("Cannot setup context manager"));
impl ContextManager {
fn new() -> io::Result<Self> {
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<String> {
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<Context> {
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()
}
}