Skip to main content

runex_core/
config.rs

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
15/// Parse a TOML string into Config.
16pub fn parse_config(s: &str) -> Result<Config, ConfigError> {
17    let config: Config = toml::from_str(s)?;
18    Ok(config)
19}
20
21/// Default config file path: `~/.config/runex/config.toml` (or platform equivalent).
22/// Overridden by `RUNEX_CONFIG` env var.
23pub 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
31/// Load config from a file path.
32pub 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}