use crate::errors::{PocketError, PocketResult, IntoPocketError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use once_cell::sync::OnceCell;
use log::{info, debug, error};
static CONFIG: OnceCell<Arc<Mutex<ConfigManager>>> = OnceCell::new();
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
#[serde(default = "default_config_version")]
pub version: String,
#[serde(default)]
pub editor: Option<String>,
#[serde(default = "default_content_type")]
pub default_content_type: String,
#[serde(default = "default_log_level")]
pub log_level: String,
#[serde(default)]
pub hooks_dir: Option<PathBuf>,
#[serde(default)]
pub bin_dir: Option<PathBuf>,
#[serde(default = "default_max_search_results")]
pub max_search_results: usize,
#[serde(default = "default_search_algorithm")]
pub search_algorithm: String,
#[serde(default)]
pub cards: HashMap<String, serde_json::Value>,
}
fn default_config_version() -> String {
"1.0".to_string()
}
fn default_content_type() -> String {
"Code".to_string()
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_max_search_results() -> usize {
10
}
fn default_search_algorithm() -> String {
"fuzzy".to_string()
}
impl Default for Config {
fn default() -> Self {
Self {
version: default_config_version(),
editor: None,
default_content_type: default_content_type(),
log_level: default_log_level(),
hooks_dir: None,
bin_dir: None,
max_search_results: default_max_search_results(),
search_algorithm: default_search_algorithm(),
cards: HashMap::new(),
}
}
}
#[derive(Debug)]
pub struct ConfigManager {
config: Config,
config_path: PathBuf,
data_dir: PathBuf,
dirty: bool,
}
impl ConfigManager {
pub fn new(data_dir: impl AsRef<Path>) -> PocketResult<Self> {
let data_dir = data_dir.as_ref().to_path_buf();
let config_path = data_dir.join("config.toml");
let config = if config_path.exists() {
let config_str = fs::read_to_string(&config_path)
.config_err(&format!("Failed to read config file: {}", config_path.display()))?;
match toml::from_str::<Config>(&config_str) {
Ok(config) => {
debug!("Loaded config from {}", config_path.display());
config
}
Err(e) => {
error!("Failed to parse config file: {}", e);
error!("Using default config");
Config::default()
}
}
} else {
info!("No config file found, creating default config");
Config::default()
};
let config_exists = config_path.exists();
let manager = Self {
config,
config_path,
data_dir,
dirty: false,
};
if !config_exists {
manager.save()?;
}
Ok(manager)
}
pub fn get_config(&self) -> Config {
self.config.clone()
}
pub fn update_config(&mut self, config: Config) -> PocketResult<()> {
self.config = config;
self.dirty = true;
Ok(())
}
pub fn get_card_config(&self, card_name: &str) -> Option<serde_json::Value> {
self.config.cards.get(card_name).cloned()
}
pub fn set_card_config(&mut self, card_name: &str, config: serde_json::Value) -> PocketResult<()> {
self.config.cards.insert(card_name.to_string(), config);
self.dirty = true;
Ok(())
}
pub fn save(&self) -> PocketResult<()> {
let config_str = toml::to_string_pretty(&self.config)
.config_err("Failed to serialize config")?;
if let Some(parent) = self.config_path.parent() {
fs::create_dir_all(parent)
.config_err(&format!("Failed to create config directory: {}", parent.display()))?;
}
fs::write(&self.config_path, config_str)
.config_err(&format!("Failed to write config to {}", self.config_path.display()))?;
debug!("Saved config to {}", self.config_path.display());
Ok(())
}
pub fn get_data_dir(&self) -> PathBuf {
self.data_dir.clone()
}
pub fn get_hooks_dir(&self) -> PathBuf {
match &self.config.hooks_dir {
Some(dir) => dir.clone(),
None => self.data_dir.join("hooks"),
}
}
pub fn get_bin_dir(&self) -> PathBuf {
match &self.config.bin_dir {
Some(dir) => dir.clone(),
None => self.data_dir.join("bin"),
}
}
}
impl Drop for ConfigManager {
fn drop(&mut self) {
if self.dirty {
match self.save() {
Ok(_) => {}
Err(e) => error!("Failed to save config on drop: {}", e),
}
}
}
}
pub fn init(data_dir: impl AsRef<Path>) -> PocketResult<()> {
let config_manager = ConfigManager::new(data_dir)?;
let _ = CONFIG.set(Arc::new(Mutex::new(config_manager)));
Ok(())
}
pub fn get() -> PocketResult<Arc<Mutex<ConfigManager>>> {
match CONFIG.get() {
Some(config) => Ok(config.clone()),
None => Err(PocketError::Config("Configuration not initialized".to_string())),
}
}
pub fn get_config() -> PocketResult<Config> {
let config = get()?;
let config_guard = config.lock()
.map_err(|_| PocketError::Config("Failed to lock config".to_string()))?;
Ok(config_guard.get_config())
}
pub fn save() -> PocketResult<()> {
let config = get()?;
let config_guard = config.lock()
.map_err(|_| PocketError::Config("Failed to lock config".to_string()))?;
config_guard.save()
}