1use aimcal_core::{Config as CoreConfig, Priority};
6use std::{
7 error::Error,
8 path::{Path, PathBuf},
9 str::FromStr,
10};
11
12pub const APP_NAME: &str = "aim";
13
14#[derive(Debug, serde::Deserialize)]
15struct ConfigRaw {
16 calendar_path: PathBuf,
17 state_dir: Option<PathBuf>,
18 default_priority: Option<Priority>,
19}
20
21#[derive(Debug)]
23pub struct Config {
24 pub core: CoreConfig,
26
27 pub state_dir: Option<PathBuf>,
29
30 pub default_priority: Priority,
32}
33
34impl TryFrom<ConfigRaw> for Config {
35 type Error = Box<dyn Error>;
36
37 fn try_from(raw: ConfigRaw) -> Result<Self, Self::Error> {
38 Ok(Self {
39 core: CoreConfig {
40 calendar_path: expand_path(&raw.calendar_path)?,
41 },
42 state_dir: match raw.state_dir {
43 Some(a) => Some(expand_path(&a)?),
44 None => get_state_dir().ok().map(|a| a.join(APP_NAME)),
45 },
46 default_priority: raw.default_priority.unwrap_or(Priority::None),
47 })
48 }
49}
50
51impl FromStr for Config {
52 type Err = Box<dyn Error>;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 toml::from_str::<ConfigRaw>(s)?.try_into()
56 }
57}
58
59impl Config {
60 pub async fn parse(path: Option<PathBuf>) -> Result<Config, Box<dyn Error>> {
62 let path = match path {
63 Some(path) => expand_path(&path)?,
64 None => {
65 let config = get_config_dir()?.join(format!("{APP_NAME}/config.toml"));
68 if !config.exists() {
69 return Err(format!("No config found at: {}", config.display()).into());
70 }
71 config
72 }
73 };
74
75 tokio::fs::read_to_string(path)
76 .await
77 .map_err(|e| format!("Failed to read config file: {e}"))?
78 .parse()
79 }
80}
81
82fn expand_path(path: &Path) -> Result<PathBuf, Box<dyn Error>> {
84 if path.is_absolute() {
85 return Ok(path.to_owned());
86 }
87
88 let path = path.to_str().ok_or("Invalid path")?;
89
90 let home_prefixes: &[&str] = if cfg!(unix) {
92 &["~/", "$HOME/", "${HOME}/"]
93 } else {
94 &[r"~\", "~/", r"%UserProfile%\", r"%UserProfile%/"]
95 };
96 for prefix in home_prefixes {
97 if let Some(stripped) = path.strip_prefix(prefix) {
98 return Ok(get_home_dir()?.join(stripped));
99 }
100 }
101
102 let config_prefixes: &[&str] = if cfg!(unix) {
104 &["$XDG_CONFIG_HOME/", "${XDG_CONFIG_HOME}/"]
105 } else {
106 &[r"%LOCALAPPDATA%\", "%LOCALAPPDATA%/"]
107 };
108 for prefix in config_prefixes {
109 if let Some(stripped) = path.strip_prefix(prefix) {
110 return Ok(get_config_dir()?.join(stripped));
111 }
112 }
113
114 Ok(path.into())
115}
116
117fn get_home_dir() -> Result<PathBuf, Box<dyn Error>> {
118 dirs::home_dir().ok_or("User-specific home directory not found".into())
119}
120
121fn get_config_dir() -> Result<PathBuf, Box<dyn Error>> {
122 #[cfg(unix)]
123 let config_dir = xdg::BaseDirectories::new().get_config_home();
124 #[cfg(windows)]
125 let config_dir = dirs::config_dir();
126 config_dir.ok_or("User-specific home directory not found".into())
127}
128
129fn get_state_dir() -> Result<PathBuf, Box<dyn Error>> {
130 #[cfg(unix)]
131 let state_dir = xdg::BaseDirectories::new().get_state_home();
132 #[cfg(windows)]
133 let state_dir = dirs::data_dir();
134 state_dir.ok_or("User-specific state directory not found".into())
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn test_expand_path_home_env() {
143 let home = get_home_dir().unwrap();
144 let home_prefixes: &[&str] = if cfg!(unix) {
145 &["~", "$HOME", "${HOME}"]
146 } else {
147 &[r"~", r"%UserProfile%"]
148 };
149 for prefix in home_prefixes {
150 let result = expand_path(&PathBuf::from(format!("{prefix}/Documents"))).unwrap();
151 assert_eq!(result, home.join("Documents"));
152 assert!(result.is_absolute());
153 }
154 }
155
156 #[test]
157 fn test_expand_path_config() {
158 let config_dir = get_config_dir().unwrap();
159 let config_prefixes: &[&str] = if cfg!(unix) {
160 &["$XDG_CONFIG_HOME", "${XDG_CONFIG_HOME}"]
161 } else {
162 &[r"%LOCALAPPDATA%"]
163 };
164 for prefix in config_prefixes {
165 let result = expand_path(&PathBuf::from(format!("{prefix}/config.toml"))).unwrap();
166 assert_eq!(result, config_dir.join("config.toml"));
167 assert!(result.is_absolute());
168 }
169 }
170
171 #[test]
172 fn test_expand_path_absolute() {
173 let absolute_path = PathBuf::from("/etc/passwd");
174 let result = expand_path(&absolute_path).unwrap();
175 assert_eq!(result, absolute_path);
176 }
177
178 #[test]
179 fn test_expand_path_relative() {
180 let relative_path = PathBuf::from("relative/path/to/file");
181 let result = expand_path(&relative_path).unwrap();
182 assert_eq!(result, relative_path);
183 }
184}