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}