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 pub fn resolve_config_path() -> Result<PathBuf> {
50 if let Ok(env_path) = env::var("FERRITE_CONF") {
52 let path = PathBuf::from(env_path);
53
54 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 Self::get_default_path()
69 }
70
71 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 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 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 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}