1use std::collections::HashMap;
32
33use crate::core::output::{ColorMode, OutputFormat, UnicodeMode};
34
35pub const ENV_OSP_UI_VERBOSITY: &str = "OSP_UI_VERBOSITY";
37pub const ENV_OSP_DEBUG_LEVEL: &str = "OSP_DEBUG_LEVEL";
39pub const ENV_OSP_FORMAT: &str = "OSP_FORMAT";
41pub const ENV_OSP_COLOR: &str = "OSP_COLOR";
43pub const ENV_OSP_UNICODE: &str = "OSP_UNICODE";
45pub const ENV_OSP_PROFILE: &str = "OSP_PROFILE";
47pub const ENV_OSP_TERMINAL: &str = "OSP_TERMINAL";
49pub const ENV_OSP_TERMINAL_KIND: &str = "OSP_TERMINAL_KIND";
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
54pub enum UiVerbosity {
55 Error,
57 Warning,
59 #[default]
61 Success,
62 Info,
64 Trace,
66}
67
68impl UiVerbosity {
69 pub fn as_str(self) -> &'static str {
71 match self {
72 UiVerbosity::Error => "error",
73 UiVerbosity::Warning => "warning",
74 UiVerbosity::Success => "success",
75 UiVerbosity::Info => "info",
76 UiVerbosity::Trace => "trace",
77 }
78 }
79
80 pub fn parse(value: &str) -> Option<Self> {
92 match value.trim().to_ascii_lowercase().as_str() {
93 "error" => Some(UiVerbosity::Error),
94 "warning" | "warn" => Some(UiVerbosity::Warning),
95 "success" => Some(UiVerbosity::Success),
96 "info" => Some(UiVerbosity::Info),
97 "trace" => Some(UiVerbosity::Trace),
98 _ => None,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum RuntimeTerminalKind {
106 Cli,
108 Repl,
110 #[default]
112 Unknown,
113}
114
115impl RuntimeTerminalKind {
116 pub fn as_str(self) -> &'static str {
118 match self {
119 RuntimeTerminalKind::Cli => "cli",
120 RuntimeTerminalKind::Repl => "repl",
121 RuntimeTerminalKind::Unknown => "unknown",
122 }
123 }
124
125 pub fn parse(value: &str) -> Option<Self> {
137 match value.trim().to_ascii_lowercase().as_str() {
138 "cli" => Some(RuntimeTerminalKind::Cli),
139 "repl" => Some(RuntimeTerminalKind::Repl),
140 "unknown" => Some(RuntimeTerminalKind::Unknown),
141 _ => None,
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148#[must_use]
149pub struct RuntimeHints {
150 pub ui_verbosity: UiVerbosity,
152 pub debug_level: u8,
154 pub format: OutputFormat,
156 pub color: ColorMode,
158 pub unicode: UnicodeMode,
160 pub profile: Option<String>,
162 pub terminal: Option<String>,
164 pub terminal_kind: RuntimeTerminalKind,
166}
167
168impl Default for RuntimeHints {
169 fn default() -> Self {
170 Self {
171 ui_verbosity: UiVerbosity::Success,
172 debug_level: 0,
173 format: OutputFormat::Auto,
174 color: ColorMode::Auto,
175 unicode: UnicodeMode::Auto,
176 profile: None,
177 terminal: None,
178 terminal_kind: RuntimeTerminalKind::Unknown,
179 }
180 }
181}
182
183impl RuntimeHints {
184 pub fn new(
208 ui_verbosity: UiVerbosity,
209 debug_level: u8,
210 format: OutputFormat,
211 color: ColorMode,
212 unicode: UnicodeMode,
213 ) -> Self {
214 Self {
215 ui_verbosity,
216 debug_level: debug_level.min(3),
217 format,
218 color,
219 unicode,
220 profile: None,
221 terminal: None,
222 terminal_kind: RuntimeTerminalKind::Unknown,
223 }
224 }
225
226 pub fn from_env() -> Self {
228 Self::from_env_iter(std::env::vars())
229 }
230
231 pub fn with_profile(mut self, profile: Option<String>) -> Self {
233 self.profile = profile
234 .map(|value| value.trim().to_string())
235 .filter(|value| !value.is_empty());
236 self
237 }
238
239 pub fn with_terminal(mut self, terminal: Option<String>) -> Self {
241 self.terminal = terminal
242 .map(|value| value.trim().to_string())
243 .filter(|value| !value.is_empty());
244 self
245 }
246
247 pub fn with_terminal_kind(mut self, terminal_kind: RuntimeTerminalKind) -> Self {
249 self.terminal_kind = terminal_kind;
250 self
251 }
252
253 pub fn from_env_iter<I, K, V>(vars: I) -> Self
283 where
284 I: IntoIterator<Item = (K, V)>,
285 K: AsRef<str>,
286 V: AsRef<str>,
287 {
288 let values = vars
289 .into_iter()
290 .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
291 .collect::<HashMap<String, String>>();
292
293 let ui_verbosity = values
294 .get(ENV_OSP_UI_VERBOSITY)
295 .and_then(|value| UiVerbosity::parse(value))
296 .unwrap_or(UiVerbosity::Success);
297 let debug_level = values
298 .get(ENV_OSP_DEBUG_LEVEL)
299 .and_then(|value| value.trim().parse::<u8>().ok())
300 .unwrap_or(0)
301 .min(3);
302 let format = values
303 .get(ENV_OSP_FORMAT)
304 .and_then(|value| OutputFormat::parse(value))
305 .unwrap_or(OutputFormat::Auto);
306 let color = values
307 .get(ENV_OSP_COLOR)
308 .and_then(|value| ColorMode::parse(value))
309 .unwrap_or(ColorMode::Auto);
310 let unicode = values
311 .get(ENV_OSP_UNICODE)
312 .and_then(|value| UnicodeMode::parse(value))
313 .unwrap_or(UnicodeMode::Auto);
314 let profile = values
315 .get(ENV_OSP_PROFILE)
316 .map(String::as_str)
317 .map(str::trim)
318 .filter(|value| !value.is_empty())
319 .map(ToOwned::to_owned);
320 let terminal = values
321 .get(ENV_OSP_TERMINAL)
322 .map(String::as_str)
323 .map(str::trim)
324 .filter(|value| !value.is_empty())
325 .map(ToOwned::to_owned);
326 let terminal_kind = values
327 .get(ENV_OSP_TERMINAL_KIND)
328 .and_then(|value| RuntimeTerminalKind::parse(value))
329 .or_else(|| {
330 values
331 .get(ENV_OSP_TERMINAL)
332 .and_then(|value| RuntimeTerminalKind::parse(value))
333 })
334 .unwrap_or(RuntimeTerminalKind::Unknown);
335
336 Self::new(ui_verbosity, debug_level, format, color, unicode)
337 .with_profile(profile)
338 .with_terminal(terminal)
339 .with_terminal_kind(terminal_kind)
340 }
341
342 pub fn env_pairs(&self) -> Vec<(&'static str, String)> {
366 let mut out = vec![
367 (ENV_OSP_UI_VERBOSITY, self.ui_verbosity.as_str().to_string()),
368 (ENV_OSP_DEBUG_LEVEL, self.debug_level.min(3).to_string()),
369 (ENV_OSP_FORMAT, self.format.as_str().to_string()),
370 (ENV_OSP_COLOR, self.color.as_str().to_string()),
371 (ENV_OSP_UNICODE, self.unicode.as_str().to_string()),
372 (
373 ENV_OSP_TERMINAL_KIND,
374 self.terminal_kind.as_str().to_string(),
375 ),
376 ];
377
378 if let Some(profile) = &self.profile {
379 out.push((ENV_OSP_PROFILE, profile.clone()));
380 }
381 if let Some(terminal) = &self.terminal {
382 out.push((ENV_OSP_TERMINAL, terminal.clone()));
383 }
384
385 out
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::{
392 ENV_OSP_COLOR, ENV_OSP_DEBUG_LEVEL, ENV_OSP_FORMAT, ENV_OSP_PROFILE, ENV_OSP_TERMINAL,
393 ENV_OSP_UI_VERBOSITY, ENV_OSP_UNICODE, RuntimeHints, RuntimeTerminalKind, UiVerbosity,
394 };
395 use crate::core::output::{ColorMode, OutputFormat, UnicodeMode};
396
397 #[test]
398 fn env_roundtrip_keeps_runtime_hints() {
399 let hints = RuntimeHints::new(
400 UiVerbosity::Trace,
401 7,
402 OutputFormat::Json,
403 ColorMode::Never,
404 UnicodeMode::Always,
405 )
406 .with_profile(Some("uio".to_string()))
407 .with_terminal(Some("xterm-256color".to_string()))
408 .with_terminal_kind(RuntimeTerminalKind::Repl);
409
410 let parsed = RuntimeHints::from_env_iter(hints.env_pairs());
411 assert_eq!(parsed.ui_verbosity, UiVerbosity::Trace);
412 assert_eq!(parsed.debug_level, 3);
413 assert_eq!(parsed.format, OutputFormat::Json);
414 assert_eq!(parsed.color, ColorMode::Never);
415 assert_eq!(parsed.unicode, UnicodeMode::Always);
416 assert_eq!(parsed.profile.as_deref(), Some("uio"));
417 assert_eq!(parsed.terminal.as_deref(), Some("xterm-256color"));
418 assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Repl);
419 }
420
421 #[test]
422 fn new_and_with_helpers_build_runtime_hints_unit() {
423 let hints = RuntimeHints::new(
424 UiVerbosity::Info,
425 9,
426 OutputFormat::Table,
427 ColorMode::Always,
428 UnicodeMode::Never,
429 )
430 .with_profile(Some(" dev ".to_string()))
431 .with_terminal(Some(" xterm-256color ".to_string()))
432 .with_terminal_kind(RuntimeTerminalKind::Cli);
433
434 assert_eq!(hints.ui_verbosity, UiVerbosity::Info);
435 assert_eq!(hints.debug_level, 3);
436 assert_eq!(hints.profile.as_deref(), Some("dev"));
437 assert_eq!(hints.terminal.as_deref(), Some("xterm-256color"));
438 assert_eq!(hints.terminal_kind, RuntimeTerminalKind::Cli);
439 }
440
441 #[test]
442 fn from_env_defaults_when_vars_missing_or_invalid() {
443 let parsed = RuntimeHints::from_env_iter(vec![
444 (ENV_OSP_UI_VERBOSITY, "loud"),
445 (ENV_OSP_DEBUG_LEVEL, "NaN"),
446 (ENV_OSP_FORMAT, "???"),
447 (ENV_OSP_COLOR, "blue"),
448 (ENV_OSP_UNICODE, "emoji"),
449 ]);
450
451 assert_eq!(parsed.ui_verbosity, UiVerbosity::Success);
452 assert_eq!(parsed.debug_level, 0);
453 assert_eq!(parsed.format, OutputFormat::Auto);
454 assert_eq!(parsed.color, ColorMode::Auto);
455 assert_eq!(parsed.unicode, UnicodeMode::Auto);
456 assert_eq!(parsed.profile, None);
457 assert_eq!(parsed.terminal, None);
458 assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Unknown);
459 }
460
461 #[test]
462 fn terminal_kind_falls_back_to_terminal_env() {
463 let parsed =
464 RuntimeHints::from_env_iter(vec![(ENV_OSP_TERMINAL, "repl"), (ENV_OSP_PROFILE, "tsd")]);
465
466 assert_eq!(parsed.profile.as_deref(), Some("tsd"));
467 assert_eq!(parsed.terminal.as_deref(), Some("repl"));
468 assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Repl);
469 }
470}