modcli/
config.rs

1use serde::Deserialize;
2use std::fs;
3use std::sync::OnceLock;
4
5static CONFIG: OnceLock<CliConfig> = OnceLock::new();
6static CONFIG_PATH: OnceLock<String> = OnceLock::new();
7static RAW_CONFIG: &str = include_str!("../examples/config.json");
8
9#[derive(Debug, Deserialize)]
10pub struct CliConfig {
11    pub modcli: ModCliSection,
12}
13
14#[derive(Debug, Deserialize)]
15pub struct ModCliSection {
16    pub name: Option<String>,
17    pub prefix: Option<String>,
18    pub banner: Option<String>,
19    pub theme: Option<String>,
20    pub delay: Option<u64>,
21    pub strict: Option<bool>,
22    pub force_shell: Option<bool>,
23    pub shell: Option<ShellConfig>,
24    pub messages: Option<MessageConfig>,
25}
26
27#[derive(Debug, Deserialize)]
28pub struct ShellConfig {
29    pub prompt: Option<String>,
30    pub welcome: Option<Vec<String>>,
31    pub goodbye: Option<Vec<String>>,
32}
33
34#[derive(Debug, Deserialize)]
35pub struct MessageConfig {
36    pub no_command: Option<String>,
37    pub not_found: Option<String>,
38}
39
40impl Default for MessageConfig {
41    fn default() -> Self {
42        MessageConfig {
43            no_command: Some("⚠️ No command given. Try `help`.".to_string()),
44            not_found: Some("⚠️ Command not found.".to_string()),
45        }
46    }
47}
48
49impl CliConfig {
50    /// Loads config from: custom path > project root > examples/ > embedded
51    pub fn load(_unused: Option<&str>) -> &'static CliConfig {
52        CONFIG.get_or_init(|| {
53            // 👇 Custom override path if set
54            if let Some(p) = CONFIG_PATH.get() {
55                if let Ok(data) = fs::read_to_string(p) {
56                    return parse(&data);
57                }
58            }
59
60            // Fallbacks...
61            if let Ok(data) = fs::read_to_string("config.json") {
62                return parse(&data);
63            }
64
65            if let Ok(data) = fs::read_to_string("examples/config.json") {
66                return parse(&data);
67            }
68
69            parse(RAW_CONFIG)
70        })
71    }
72
73    /// Owned config loader (non-global). Prefer this in library code for better testability.
74    pub fn load_owned(path: Option<&str>) -> CliConfig {
75        if let Some(p) = path {
76            if let Ok(data) = fs::read_to_string(p) {
77                return parse(&data);
78            }
79        }
80
81        if let Ok(data) = fs::read_to_string("config.json") {
82            return parse(&data);
83        }
84
85        if let Ok(data) = fs::read_to_string("examples/config.json") {
86            return parse(&data);
87        }
88
89        parse(RAW_CONFIG)
90    }
91}
92
93fn parse(data: &str) -> CliConfig {
94    serde_json::from_str(data).expect("Failed to parse CLI config JSON")
95}
96
97pub fn set_path(path: &str) {
98    let _ = CONFIG_PATH.set(path.to_string()); // only sets once
99}