ferrite_config/
config.rs

1use directories::ProjectDirs;
2use serde::{Deserialize, Serialize};
3use std::{fs, path::PathBuf};
4use tracing::{debug, info};
5
6use crate::{
7    error::{ConfigError, Result},
8    input::ControlsConfig,
9    window::WindowConfig,
10    zoom::ZoomConfig,
11    CacheConfig,
12    HelpMenuConfig,
13    IndicatorConfig,
14    CONFIG_VERSION,
15};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct FerriteConfig {
19    version:       String,
20    pub window:    WindowConfig,
21    pub zoom:      ZoomConfig,
22    pub controls:  ControlsConfig,
23    pub indicator: IndicatorConfig,
24    pub help_menu: HelpMenuConfig,
25    pub cache:     CacheConfig,
26}
27
28impl Default for FerriteConfig {
29    fn default() -> Self {
30        info!("Creating default configuration");
31        Self {
32            version:   CONFIG_VERSION.to_string(),
33            window:    WindowConfig::default(),
34            zoom:      ZoomConfig::default(),
35            controls:  ControlsConfig::default(),
36            indicator: IndicatorConfig::default(),
37            help_menu: HelpMenuConfig::default(),
38            cache:     CacheConfig::default(),
39        }
40    }
41}
42
43use std::env;
44
45impl FerriteConfig {
46    /// Determines the configuration file path by checking:
47    /// 1. FERRITE_CONF environment variable
48    /// 2. Default XDG config path
49    pub fn resolve_config_path() -> Result<PathBuf> {
50        // First check environment variable
51        if let Ok(env_path) = env::var("FERRITE_CONF") {
52            let path = PathBuf::from(env_path);
53
54            // Validate the path from environment variable
55            if let Some(parent) = path.parent() {
56                if !parent.exists() {
57                    return Err(ConfigError::InvalidPath(format!(
58                        "Directory {} from FERRITE_CONF does not exist",
59                        parent.display()
60                    )));
61                }
62            }
63
64            return Ok(path);
65        }
66
67        // Fall back to default XDG config path
68        Self::get_default_path()
69    }
70
71    /// Loads configuration using environment-aware path resolution
72    pub fn load() -> Result<Self> {
73        let config_path = Self::resolve_config_path()?;
74
75        if !config_path.exists() {
76            info!(
77                "No configuration file found at {:?}, using defaults",
78                config_path
79            );
80            return Ok(Self::default());
81        }
82
83        info!("Loading configuration from {:?}", config_path);
84        Self::load_from_path(&config_path)
85    }
86
87    pub fn load_from_path(path: &PathBuf) -> Result<Self> {
88        if !path.exists() {
89            debug!("No config file found at {:?}, using defaults", path);
90            return Ok(Self::default());
91        }
92
93        info!("Loading configuration from {:?}", path);
94        let content = fs::read_to_string(path)?;
95        let config: Self = toml::from_str(&content)?;
96
97        if config.version != CONFIG_VERSION {
98            return Err(ConfigError::VersionError {
99                found:     config.version.clone(),
100                supported: CONFIG_VERSION.to_string(),
101            });
102        }
103
104        config.validate()?;
105        Ok(config)
106    }
107
108    pub fn save_to_path(&self, path: &PathBuf) -> Result<()> {
109        if let Some(parent) = path.parent() {
110            fs::create_dir_all(parent)?;
111        }
112
113        self.validate()?;
114        let content = toml::to_string_pretty(self)?;
115        fs::write(path, content)?;
116
117        info!("Saved configuration to {:?}", path);
118        Ok(())
119    }
120
121    // Default paths handling
122    pub fn get_default_path() -> Result<PathBuf> {
123        ProjectDirs::from("com", "ferrite", "ferrite")
124            .map(|proj_dirs| proj_dirs.config_dir().join("config.toml"))
125            .ok_or_else(|| ConfigError::DirectoryError(PathBuf::from(".")))
126    }
127
128    // Configuration validation
129    pub fn validate(&self) -> Result<()> {
130        self.window.validate()?;
131        self.zoom.validate()?;
132        self.controls.validate()?;
133        self.indicator.validate()?;
134        Ok(())
135    }
136
137    // Utility method for creating new configurations
138    pub fn with_modifications<F>(&self, modify_fn: F) -> Result<Self>
139    where
140        F: FnOnce(&mut Self),
141    {
142        let mut new_config = self.clone();
143        modify_fn(&mut new_config);
144        new_config.validate()?;
145        Ok(new_config)
146    }
147}