chabeau 0.7.3

A full-screen terminal chat interface that connects to various AI APIs for real-time conversations
Documentation
use crate::core::config::data::Config;
use std::fs;
use std::path::PathBuf;
use std::sync::{LazyLock, Mutex};
use std::time::SystemTime;

#[derive(Default)]
pub(crate) struct ConfigCacheState {
    config: Option<Config>,
    modified: Option<SystemTime>,
}

pub(crate) struct ConfigOrchestrator {
    path: PathBuf,
    state: Mutex<ConfigCacheState>,
}

pub(crate) static CONFIG_ORCHESTRATOR: LazyLock<ConfigOrchestrator> =
    LazyLock::new(|| ConfigOrchestrator::new(Config::get_config_path()));

#[cfg(test)]
pub(crate) static TEST_ORCHESTRATOR: LazyLock<Mutex<Option<ConfigOrchestrator>>> =
    LazyLock::new(|| Mutex::new(None));

impl ConfigOrchestrator {
    pub(crate) fn new(path: PathBuf) -> Self {
        Self {
            path,
            state: Mutex::new(ConfigCacheState::default()),
        }
    }

    pub(crate) fn load_with_cache(&self) -> Result<Config, Box<dyn std::error::Error>> {
        let mut state = self.state.lock().unwrap();
        let disk_modified = Self::modified_time(&self.path);
        if state.config.is_none() || state.modified != disk_modified {
            let config = Config::load_from_path(&self.path)?;
            state.modified = disk_modified;
            state.config = Some(config);
        }
        Ok(state.config.clone().unwrap_or_default())
    }

    pub(crate) fn persist(&self, config: Config) -> Result<(), Box<dyn std::error::Error>> {
        self.write_config(&config)?;
        let mut state = self.state.lock().unwrap();
        state.modified = Self::modified_time(&self.path);
        state.config = Some(config);
        Ok(())
    }

    pub(crate) fn mutate<F, T>(&self, mutator: F) -> Result<T, Box<dyn std::error::Error>>
    where
        F: FnOnce(&mut Config) -> Result<T, Box<dyn std::error::Error>>,
    {
        let snapshot = {
            let mut state = self.state.lock().unwrap();
            let disk_modified = Self::modified_time(&self.path);
            if state.config.is_none() || state.modified != disk_modified {
                let config = Config::load_from_path(&self.path)?;
                state.modified = disk_modified;
                state.config = Some(config);
            }
            state.config.clone().unwrap_or_default()
        };

        let mut working = snapshot;
        let result = mutator(&mut working)?;
        self.persist(working)?;
        Ok(result)
    }

    fn write_config(&self, config: &Config) -> Result<(), Box<dyn std::error::Error>> {
        config.save_to_path(&self.path)
    }

    fn modified_time(path: &PathBuf) -> Option<SystemTime> {
        fs::metadata(path).ok()?.modified().ok()
    }
}

impl Config {
    pub fn load() -> Result<Config, Box<dyn std::error::Error>> {
        #[cfg(test)]
        {
            if let Some(orchestrator) = TEST_ORCHESTRATOR.lock().unwrap().as_ref() {
                return orchestrator.load_with_cache();
            }
        }
        CONFIG_ORCHESTRATOR.load_with_cache()
    }

    #[cfg(test)]
    pub fn load_test_safe() -> Result<Config, Box<dyn std::error::Error>> {
        Ok(Config::default())
    }

    #[cfg(not(test))]
    pub fn load_test_safe() -> Result<Config, Box<dyn std::error::Error>> {
        Self::load()
    }

    pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
        #[cfg(test)]
        {
            if let Some(orchestrator) = TEST_ORCHESTRATOR.lock().unwrap().as_ref() {
                return orchestrator.persist(self.clone());
            }
        }
        CONFIG_ORCHESTRATOR.persist(self.clone())
    }

    #[cfg(not(test))]
    pub fn mutate<F, T>(mutator: F) -> Result<T, Box<dyn std::error::Error>>
    where
        F: FnOnce(&mut Config) -> Result<T, Box<dyn std::error::Error>>,
    {
        CONFIG_ORCHESTRATOR.mutate(mutator)
    }

    #[cfg(test)]
    pub fn mutate<F, T>(mutator: F) -> Result<T, Box<dyn std::error::Error>>
    where
        F: FnOnce(&mut Config) -> Result<T, Box<dyn std::error::Error>>,
    {
        if let Some(orchestrator) = TEST_ORCHESTRATOR.lock().unwrap().as_ref() {
            orchestrator.mutate(mutator)
        } else {
            let mut config = Config::default();
            let result = mutator(&mut config)?;
            Ok(result)
        }
    }

    #[cfg(test)]
    pub(crate) fn set_test_config_path(path: PathBuf) {
        let mut guard = TEST_ORCHESTRATOR.lock().unwrap();
        *guard = Some(ConfigOrchestrator::new(path));
    }

    #[cfg(test)]
    pub(crate) fn clear_test_config_override() {
        let mut guard = TEST_ORCHESTRATOR.lock().unwrap();
        guard.take();
    }
}