Skip to main content

osp_cli/core/
runtime.rs

1use std::collections::HashMap;
2
3use crate::core::output::{ColorMode, OutputFormat, UnicodeMode};
4
5pub const ENV_OSP_UI_VERBOSITY: &str = "OSP_UI_VERBOSITY";
6pub const ENV_OSP_DEBUG_LEVEL: &str = "OSP_DEBUG_LEVEL";
7pub const ENV_OSP_FORMAT: &str = "OSP_FORMAT";
8pub const ENV_OSP_COLOR: &str = "OSP_COLOR";
9pub const ENV_OSP_UNICODE: &str = "OSP_UNICODE";
10pub const ENV_OSP_PROFILE: &str = "OSP_PROFILE";
11pub const ENV_OSP_TERMINAL: &str = "OSP_TERMINAL";
12pub const ENV_OSP_TERMINAL_KIND: &str = "OSP_TERMINAL_KIND";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
15pub enum UiVerbosity {
16    Error,
17    Warning,
18    #[default]
19    Success,
20    Info,
21    Trace,
22}
23
24impl UiVerbosity {
25    pub fn as_str(self) -> &'static str {
26        match self {
27            UiVerbosity::Error => "error",
28            UiVerbosity::Warning => "warning",
29            UiVerbosity::Success => "success",
30            UiVerbosity::Info => "info",
31            UiVerbosity::Trace => "trace",
32        }
33    }
34
35    pub fn parse(value: &str) -> Option<Self> {
36        match value.trim().to_ascii_lowercase().as_str() {
37            "error" => Some(UiVerbosity::Error),
38            "warning" | "warn" => Some(UiVerbosity::Warning),
39            "success" => Some(UiVerbosity::Success),
40            "info" => Some(UiVerbosity::Info),
41            "trace" => Some(UiVerbosity::Trace),
42            _ => None,
43        }
44    }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum RuntimeTerminalKind {
49    Cli,
50    Repl,
51    #[default]
52    Unknown,
53}
54
55impl RuntimeTerminalKind {
56    pub fn as_str(self) -> &'static str {
57        match self {
58            RuntimeTerminalKind::Cli => "cli",
59            RuntimeTerminalKind::Repl => "repl",
60            RuntimeTerminalKind::Unknown => "unknown",
61        }
62    }
63
64    pub fn parse(value: &str) -> Option<Self> {
65        match value.trim().to_ascii_lowercase().as_str() {
66            "cli" => Some(RuntimeTerminalKind::Cli),
67            "repl" => Some(RuntimeTerminalKind::Repl),
68            "unknown" => Some(RuntimeTerminalKind::Unknown),
69            _ => None,
70        }
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct RuntimeHints {
76    pub ui_verbosity: UiVerbosity,
77    pub debug_level: u8,
78    pub format: OutputFormat,
79    pub color: ColorMode,
80    pub unicode: UnicodeMode,
81    pub profile: Option<String>,
82    pub terminal: Option<String>,
83    pub terminal_kind: RuntimeTerminalKind,
84}
85
86impl Default for RuntimeHints {
87    fn default() -> Self {
88        Self {
89            ui_verbosity: UiVerbosity::Success,
90            debug_level: 0,
91            format: OutputFormat::Auto,
92            color: ColorMode::Auto,
93            unicode: UnicodeMode::Auto,
94            profile: None,
95            terminal: None,
96            terminal_kind: RuntimeTerminalKind::Unknown,
97        }
98    }
99}
100
101impl RuntimeHints {
102    pub fn from_env() -> Self {
103        Self::from_env_iter(std::env::vars())
104    }
105
106    pub fn from_env_iter<I, K, V>(vars: I) -> Self
107    where
108        I: IntoIterator<Item = (K, V)>,
109        K: AsRef<str>,
110        V: AsRef<str>,
111    {
112        let values = vars
113            .into_iter()
114            .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
115            .collect::<HashMap<String, String>>();
116
117        let ui_verbosity = values
118            .get(ENV_OSP_UI_VERBOSITY)
119            .and_then(|value| UiVerbosity::parse(value))
120            .unwrap_or(UiVerbosity::Success);
121        let debug_level = values
122            .get(ENV_OSP_DEBUG_LEVEL)
123            .and_then(|value| value.trim().parse::<u8>().ok())
124            .unwrap_or(0)
125            .min(3);
126        let format = values
127            .get(ENV_OSP_FORMAT)
128            .and_then(|value| OutputFormat::parse(value))
129            .unwrap_or(OutputFormat::Auto);
130        let color = values
131            .get(ENV_OSP_COLOR)
132            .and_then(|value| ColorMode::parse(value))
133            .unwrap_or(ColorMode::Auto);
134        let unicode = values
135            .get(ENV_OSP_UNICODE)
136            .and_then(|value| UnicodeMode::parse(value))
137            .unwrap_or(UnicodeMode::Auto);
138        let profile = values
139            .get(ENV_OSP_PROFILE)
140            .map(String::as_str)
141            .map(str::trim)
142            .filter(|value| !value.is_empty())
143            .map(ToOwned::to_owned);
144        let terminal = values
145            .get(ENV_OSP_TERMINAL)
146            .map(String::as_str)
147            .map(str::trim)
148            .filter(|value| !value.is_empty())
149            .map(ToOwned::to_owned);
150        let terminal_kind = values
151            .get(ENV_OSP_TERMINAL_KIND)
152            .and_then(|value| RuntimeTerminalKind::parse(value))
153            .or_else(|| {
154                values
155                    .get(ENV_OSP_TERMINAL)
156                    .and_then(|value| RuntimeTerminalKind::parse(value))
157            })
158            .unwrap_or(RuntimeTerminalKind::Unknown);
159
160        Self {
161            ui_verbosity,
162            debug_level,
163            format,
164            color,
165            unicode,
166            profile,
167            terminal,
168            terminal_kind,
169        }
170    }
171
172    pub fn env_pairs(&self) -> Vec<(&'static str, String)> {
173        let mut out = vec![
174            (ENV_OSP_UI_VERBOSITY, self.ui_verbosity.as_str().to_string()),
175            (ENV_OSP_DEBUG_LEVEL, self.debug_level.min(3).to_string()),
176            (ENV_OSP_FORMAT, self.format.as_str().to_string()),
177            (ENV_OSP_COLOR, self.color.as_str().to_string()),
178            (ENV_OSP_UNICODE, self.unicode.as_str().to_string()),
179            (
180                ENV_OSP_TERMINAL_KIND,
181                self.terminal_kind.as_str().to_string(),
182            ),
183        ];
184
185        if let Some(profile) = &self.profile {
186            out.push((ENV_OSP_PROFILE, profile.clone()));
187        }
188        if let Some(terminal) = &self.terminal {
189            out.push((ENV_OSP_TERMINAL, terminal.clone()));
190        }
191
192        out
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::{
199        ENV_OSP_COLOR, ENV_OSP_DEBUG_LEVEL, ENV_OSP_FORMAT, ENV_OSP_PROFILE, ENV_OSP_TERMINAL,
200        ENV_OSP_UI_VERBOSITY, ENV_OSP_UNICODE, RuntimeHints, RuntimeTerminalKind, UiVerbosity,
201    };
202    use crate::core::output::{ColorMode, OutputFormat, UnicodeMode};
203
204    #[test]
205    fn env_roundtrip_keeps_runtime_hints() {
206        let hints = RuntimeHints {
207            ui_verbosity: UiVerbosity::Trace,
208            debug_level: 7,
209            format: OutputFormat::Json,
210            color: ColorMode::Never,
211            unicode: UnicodeMode::Always,
212            profile: Some("uio".to_string()),
213            terminal: Some("xterm-256color".to_string()),
214            terminal_kind: RuntimeTerminalKind::Repl,
215        };
216
217        let parsed = RuntimeHints::from_env_iter(hints.env_pairs());
218        assert_eq!(parsed.ui_verbosity, UiVerbosity::Trace);
219        assert_eq!(parsed.debug_level, 3);
220        assert_eq!(parsed.format, OutputFormat::Json);
221        assert_eq!(parsed.color, ColorMode::Never);
222        assert_eq!(parsed.unicode, UnicodeMode::Always);
223        assert_eq!(parsed.profile.as_deref(), Some("uio"));
224        assert_eq!(parsed.terminal.as_deref(), Some("xterm-256color"));
225        assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Repl);
226    }
227
228    #[test]
229    fn from_env_defaults_when_vars_missing_or_invalid() {
230        let parsed = RuntimeHints::from_env_iter(vec![
231            (ENV_OSP_UI_VERBOSITY, "loud"),
232            (ENV_OSP_DEBUG_LEVEL, "NaN"),
233            (ENV_OSP_FORMAT, "???"),
234            (ENV_OSP_COLOR, "blue"),
235            (ENV_OSP_UNICODE, "emoji"),
236        ]);
237
238        assert_eq!(parsed.ui_verbosity, UiVerbosity::Success);
239        assert_eq!(parsed.debug_level, 0);
240        assert_eq!(parsed.format, OutputFormat::Auto);
241        assert_eq!(parsed.color, ColorMode::Auto);
242        assert_eq!(parsed.unicode, UnicodeMode::Auto);
243        assert_eq!(parsed.profile, None);
244        assert_eq!(parsed.terminal, None);
245        assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Unknown);
246    }
247
248    #[test]
249    fn terminal_kind_falls_back_to_terminal_env() {
250        let parsed =
251            RuntimeHints::from_env_iter(vec![(ENV_OSP_TERMINAL, "repl"), (ENV_OSP_PROFILE, "tsd")]);
252
253        assert_eq!(parsed.profile.as_deref(), Some("tsd"));
254        assert_eq!(parsed.terminal.as_deref(), Some("repl"));
255        assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Repl);
256    }
257}