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 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"); log_dir.join("ferrite.log")
38 });
39
40 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")) });
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, }
56 }
57}
58
59impl FerriteConfig {
60 pub fn resolve_config_path() -> Result<PathBuf> {
64 if let Ok(env_path) = env::var("FERRITE_CONF") {
66 let path = PathBuf::from(env_path);
67
68 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 Self::get_default_path()
83 }
84
85 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 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 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 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}