1use std::path::PathBuf;
2
3use crate::model::Config;
4
5#[derive(Debug, thiserror::Error)]
6pub enum ConfigError {
7 #[error("TOML parse error: {0}")]
8 Parse(#[from] toml::de::Error),
9 #[error("IO error: {0}")]
10 Io(#[from] std::io::Error),
11 #[error("cannot determine config directory")]
12 NoConfigDir,
13}
14
15pub fn parse_config(s: &str) -> Result<Config, ConfigError> {
17 let config: Config = toml::from_str(s)?;
18 Ok(config)
19}
20
21pub fn default_config_path() -> Result<PathBuf, ConfigError> {
24 if let Ok(p) = std::env::var("RUNEX_CONFIG") {
25 return Ok(PathBuf::from(p));
26 }
27 let dir = dirs::config_dir().ok_or(ConfigError::NoConfigDir)?;
28 Ok(dir.join("runex").join("config.toml"))
29}
30
31pub fn load_config(path: &std::path::Path) -> Result<Config, ConfigError> {
33 let content = std::fs::read_to_string(path)?;
34 parse_config(&content)
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use crate::model::TriggerKey;
41
42 #[test]
43 fn parse_minimal_toml() {
44 let toml = r#"
45version = 1
46
47[[abbr]]
48key = "gcm"
49expand = "git commit -m"
50"#;
51 let config = parse_config(toml).unwrap();
52 assert_eq!(config.version, 1);
53 assert_eq!(config.abbr.len(), 1);
54 assert_eq!(config.abbr[0].key, "gcm");
55 assert_eq!(config.abbr[0].expand, "git commit -m");
56 }
57
58 #[test]
59 fn parse_with_when_command_exists() {
60 let toml = r#"
61version = 1
62
63[[abbr]]
64key = "ls"
65expand = "lsd"
66when_command_exists = ["lsd"]
67"#;
68 let config = parse_config(toml).unwrap();
69 assert_eq!(
70 config.abbr[0].when_command_exists,
71 Some(vec!["lsd".to_string()])
72 );
73 }
74
75 #[test]
76 fn parse_with_keybind() {
77 let toml = r#"
78version = 1
79
80[keybind]
81trigger = "space"
82bash = "alt-space"
83zsh = "space"
84pwsh = "tab"
85"#;
86 let config = parse_config(toml).unwrap();
87 assert_eq!(config.keybind.trigger, Some(TriggerKey::Space));
88 assert_eq!(config.keybind.bash, Some(TriggerKey::AltSpace));
89 assert_eq!(config.keybind.zsh, Some(TriggerKey::Space));
90 assert_eq!(config.keybind.pwsh, Some(TriggerKey::Tab));
91 assert_eq!(config.keybind.nu, None);
92 }
93
94 #[test]
95 fn parse_missing_version_is_err() {
96 let toml = r#"
97[[abbr]]
98key = "gcm"
99expand = "git commit -m"
100"#;
101 assert!(parse_config(toml).is_err());
102 }
103
104 #[test]
105 fn parse_empty_abbr_list() {
106 let toml = "version = 1\n";
107 let config = parse_config(toml).unwrap();
108 assert!(config.abbr.is_empty());
109 }
110
111 #[test]
112 fn load_config_from_file() {
113 let dir = std::env::temp_dir().join("runex_test_load");
114 std::fs::create_dir_all(&dir).unwrap();
115 let path = dir.join("config.toml");
116 std::fs::write(
117 &path,
118 r#"
119version = 1
120
121[[abbr]]
122key = "gcm"
123expand = "git commit -m"
124"#,
125 )
126 .unwrap();
127
128 let config = load_config(&path).unwrap();
129 assert_eq!(config.version, 1);
130 assert_eq!(config.abbr[0].key, "gcm");
131
132 std::fs::remove_dir_all(&dir).ok();
133 }
134
135 #[test]
136 fn default_config_path_env_override() {
137 std::env::set_var("RUNEX_CONFIG", "/tmp/custom.toml");
138 let path = default_config_path().unwrap();
139 assert_eq!(path, PathBuf::from("/tmp/custom.toml"));
140 std::env::remove_var("RUNEX_CONFIG");
141 }
142}