ferrite_config/
config.rs

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