modcli/output/
messages.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::sync::Mutex;
4use std::sync::OnceLock;
5
6static CATALOG: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
7type InterceptorFn = dyn Fn(&str, &str) -> Cow<'static, str> + Send + Sync;
8static INTERCEPTOR: OnceLock<Mutex<Option<Box<InterceptorFn>>>> = OnceLock::new();
9
10fn catalog() -> &'static Mutex<HashMap<String, String>> {
11    CATALOG.get_or_init(|| Mutex::new(HashMap::new()))
12}
13
14fn interceptor() -> &'static Mutex<Option<Box<InterceptorFn>>> {
15    INTERCEPTOR.get_or_init(|| Mutex::new(None))
16}
17
18/// Set or override a message value for a given key.
19pub fn set_message<K: Into<String>, V: Into<String>>(key: K, value: V) {
20    if let Ok(mut map) = catalog().lock() {
21        map.insert(key.into(), value.into());
22    }
23}
24
25/// Remove a customized message for a key (falls back to default when used).
26pub fn reset_message(key: &str) {
27    if let Ok(mut map) = catalog().lock() {
28        map.remove(key);
29    }
30}
31
32/// Get a customized message for a key, if present.
33pub fn get_message(key: &str) -> Option<String> {
34    catalog().lock().ok().and_then(|m| m.get(key).cloned())
35}
36
37/// Return a customized message if present, otherwise the provided default.
38pub fn message_or_default<'a>(key: &str, default: &'a str) -> Cow<'a, str> {
39    if let Some(val) = get_message(key) {
40        Cow::Owned(val)
41    } else {
42        Cow::Borrowed(default)
43    }
44}
45
46/// Set a global output interceptor. The interceptor receives a category and text and
47/// returns the (possibly transformed) text to print.
48pub fn set_output_interceptor<F>(f: F)
49where
50    F: Fn(&str, &str) -> Cow<'static, str> + Send + Sync + 'static,
51{
52    if let Ok(mut slot) = interceptor().lock() {
53        *slot = Some(Box::new(f));
54    }
55}
56
57/// Clear the output interceptor.
58pub fn clear_output_interceptor() {
59    if let Ok(mut slot) = interceptor().lock() {
60        *slot = None;
61    }
62}
63
64/// Apply the interceptor to a given category/text if one is set.
65pub fn intercept<'a>(category: &str, text: &'a str) -> Cow<'a, str> {
66    if let Ok(slot) = interceptor().lock() {
67        if let Some(ref cb) = *slot {
68            // Promote to 'static by cloning into owned when changed
69            let owned: Cow<'static, str> = cb(category, text);
70            return Cow::Owned(owned.into_owned());
71        }
72    }
73    Cow::Borrowed(text)
74}
75
76/// Load messages from a JSON object file mapping string keys to string values.
77/// Example JSON: { "help.header": "Commands", "unknown": "..." }
78#[cfg(feature = "theme-config")]
79pub fn load_messages_from_json(path: &str) -> Result<(), crate::error::ModCliError> {
80    let data = std::fs::read_to_string(path)?;
81    let map: HashMap<String, String> = serde_json::from_str(&data)?;
82    if let Ok(mut cat) = catalog().lock() {
83        for (k, v) in map {
84            cat.insert(k, v);
85        }
86    }
87    Ok(())
88}
89
90#[cfg(not(feature = "theme-config"))]
91pub fn load_messages_from_json(_path: &str) -> Result<(), crate::error::ModCliError> {
92    Err(crate::error::ModCliError::InvalidUsage(
93        "messages JSON loader requires feature: theme-config".into(),
94    ))
95}