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