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)]
148pub struct RuntimeHints {
149 pub ui_verbosity: UiVerbosity,
151 pub debug_level: u8,
153 pub format: OutputFormat,
155 pub color: ColorMode,
157 pub unicode: UnicodeMode,
159 pub profile: Option<String>,
161 pub terminal: Option<String>,
163 pub terminal_kind: RuntimeTerminalKind,
165}
166
167impl Default for RuntimeHints {
168 fn default() -> Self {
169 Self {
170 ui_verbosity: UiVerbosity::Success,
171 debug_level: 0,
172 format: OutputFormat::Auto,
173 color: ColorMode::Auto,
174 unicode: UnicodeMode::Auto,
175 profile: None,
176 terminal: None,
177 terminal_kind: RuntimeTerminalKind::Unknown,
178 }
179 }
180}
181
182impl RuntimeHints {
183 pub fn new(
207 ui_verbosity: UiVerbosity,
208 debug_level: u8,
209 format: OutputFormat,
210 color: ColorMode,
211 unicode: UnicodeMode,
212 ) -> Self {
213 Self {
214 ui_verbosity,
215 debug_level: debug_level.min(3),
216 format,
217 color,
218 unicode,
219 profile: None,
220 terminal: None,
221 terminal_kind: RuntimeTerminalKind::Unknown,
222 }
223 }
224
225 pub fn from_env() -> Self {
227 Self::from_env_iter(std::env::vars())
228 }
229
230 pub fn with_profile(mut self, profile: Option<String>) -> Self {
232 self.profile = profile
233 .map(|value| value.trim().to_string())
234 .filter(|value| !value.is_empty());
235 self
236 }
237
238 pub fn with_terminal(mut self, terminal: Option<String>) -> Self {
240 self.terminal = terminal
241 .map(|value| value.trim().to_string())
242 .filter(|value| !value.is_empty());
243 self
244 }
245
246 pub fn with_terminal_kind(mut self, terminal_kind: RuntimeTerminalKind) -> Self {
248 self.terminal_kind = terminal_kind;
249 self
250 }
251
252 pub fn from_env_iter<I, K, V>(vars: I) -> Self
282 where
283 I: IntoIterator<Item = (K, V)>,
284 K: AsRef<str>,
285 V: AsRef<str>,
286 {
287 let values = vars
288 .into_iter()
289 .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
290 .collect::<HashMap<String, String>>();
291
292 let ui_verbosity = values
293 .get(ENV_OSP_UI_VERBOSITY)
294 .and_then(|value| UiVerbosity::parse(value))
295 .unwrap_or(UiVerbosity::Success);
296 let debug_level = values
297 .get(ENV_OSP_DEBUG_LEVEL)
298 .and_then(|value| value.trim().parse::<u8>().ok())
299 .unwrap_or(0)
300 .min(3);
301 let format = values
302 .get(ENV_OSP_FORMAT)
303 .and_then(|value| OutputFormat::parse(value))
304 .unwrap_or(OutputFormat::Auto);
305 let color = values
306 .get(ENV_OSP_COLOR)
307 .and_then(|value| ColorMode::parse(value))
308 .unwrap_or(ColorMode::Auto);
309 let unicode = values
310 .get(ENV_OSP_UNICODE)
311 .and_then(|value| UnicodeMode::parse(value))
312 .unwrap_or(UnicodeMode::Auto);
313 let profile = values
314 .get(ENV_OSP_PROFILE)
315 .map(String::as_str)
316 .map(str::trim)
317 .filter(|value| !value.is_empty())
318 .map(ToOwned::to_owned);
319 let terminal = values
320 .get(ENV_OSP_TERMINAL)
321 .map(String::as_str)
322 .map(str::trim)
323 .filter(|value| !value.is_empty())
324 .map(ToOwned::to_owned);
325 let terminal_kind = values
326 .get(ENV_OSP_TERMINAL_KIND)
327 .and_then(|value| RuntimeTerminalKind::parse(value))
328 .or_else(|| {
329 values
330 .get(ENV_OSP_TERMINAL)
331 .and_then(|value| RuntimeTerminalKind::parse(value))
332 })
333 .unwrap_or(RuntimeTerminalKind::Unknown);
334
335 Self::new(ui_verbosity, debug_level, format, color, unicode)
336 .with_profile(profile)
337 .with_terminal(terminal)
338 .with_terminal_kind(terminal_kind)
339 }
340
341 pub fn env_pairs(&self) -> Vec<(&'static str, String)> {
365 let mut out = vec![
366 (ENV_OSP_UI_VERBOSITY, self.ui_verbosity.as_str().to_string()),
367 (ENV_OSP_DEBUG_LEVEL, self.debug_level.min(3).to_string()),
368 (ENV_OSP_FORMAT, self.format.as_str().to_string()),
369 (ENV_OSP_COLOR, self.color.as_str().to_string()),
370 (ENV_OSP_UNICODE, self.unicode.as_str().to_string()),
371 (
372 ENV_OSP_TERMINAL_KIND,
373 self.terminal_kind.as_str().to_string(),
374 ),
375 ];
376
377 if let Some(profile) = &self.profile {
378 out.push((ENV_OSP_PROFILE, profile.clone()));
379 }
380 if let Some(terminal) = &self.terminal {
381 out.push((ENV_OSP_TERMINAL, terminal.clone()));
382 }
383
384 out
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::{
391 ENV_OSP_COLOR, ENV_OSP_DEBUG_LEVEL, ENV_OSP_FORMAT, ENV_OSP_PROFILE, ENV_OSP_TERMINAL,
392 ENV_OSP_UI_VERBOSITY, ENV_OSP_UNICODE, RuntimeHints, RuntimeTerminalKind, UiVerbosity,
393 };
394 use crate::core::output::{ColorMode, OutputFormat, UnicodeMode};
395
396 #[test]
397 fn env_roundtrip_keeps_runtime_hints() {
398 let hints = RuntimeHints::new(
399 UiVerbosity::Trace,
400 7,
401 OutputFormat::Json,
402 ColorMode::Never,
403 UnicodeMode::Always,
404 )
405 .with_profile(Some("uio".to_string()))
406 .with_terminal(Some("xterm-256color".to_string()))
407 .with_terminal_kind(RuntimeTerminalKind::Repl);
408
409 let parsed = RuntimeHints::from_env_iter(hints.env_pairs());
410 assert_eq!(parsed.ui_verbosity, UiVerbosity::Trace);
411 assert_eq!(parsed.debug_level, 3);
412 assert_eq!(parsed.format, OutputFormat::Json);
413 assert_eq!(parsed.color, ColorMode::Never);
414 assert_eq!(parsed.unicode, UnicodeMode::Always);
415 assert_eq!(parsed.profile.as_deref(), Some("uio"));
416 assert_eq!(parsed.terminal.as_deref(), Some("xterm-256color"));
417 assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Repl);
418 }
419
420 #[test]
421 fn new_and_with_helpers_build_runtime_hints_unit() {
422 let hints = RuntimeHints::new(
423 UiVerbosity::Info,
424 9,
425 OutputFormat::Table,
426 ColorMode::Always,
427 UnicodeMode::Never,
428 )
429 .with_profile(Some(" dev ".to_string()))
430 .with_terminal(Some(" xterm-256color ".to_string()))
431 .with_terminal_kind(RuntimeTerminalKind::Cli);
432
433 assert_eq!(hints.ui_verbosity, UiVerbosity::Info);
434 assert_eq!(hints.debug_level, 3);
435 assert_eq!(hints.profile.as_deref(), Some("dev"));
436 assert_eq!(hints.terminal.as_deref(), Some("xterm-256color"));
437 assert_eq!(hints.terminal_kind, RuntimeTerminalKind::Cli);
438 }
439
440 #[test]
441 fn from_env_defaults_when_vars_missing_or_invalid() {
442 let parsed = RuntimeHints::from_env_iter(vec![
443 (ENV_OSP_UI_VERBOSITY, "loud"),
444 (ENV_OSP_DEBUG_LEVEL, "NaN"),
445 (ENV_OSP_FORMAT, "???"),
446 (ENV_OSP_COLOR, "blue"),
447 (ENV_OSP_UNICODE, "emoji"),
448 ]);
449
450 assert_eq!(parsed.ui_verbosity, UiVerbosity::Success);
451 assert_eq!(parsed.debug_level, 0);
452 assert_eq!(parsed.format, OutputFormat::Auto);
453 assert_eq!(parsed.color, ColorMode::Auto);
454 assert_eq!(parsed.unicode, UnicodeMode::Auto);
455 assert_eq!(parsed.profile, None);
456 assert_eq!(parsed.terminal, None);
457 assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Unknown);
458 }
459
460 #[test]
461 fn terminal_kind_falls_back_to_terminal_env() {
462 let parsed =
463 RuntimeHints::from_env_iter(vec![(ENV_OSP_TERMINAL, "repl"), (ENV_OSP_PROFILE, "tsd")]);
464
465 assert_eq!(parsed.profile.as_deref(), Some("tsd"));
466 assert_eq!(parsed.terminal.as_deref(), Some("repl"));
467 assert_eq!(parsed.terminal_kind, RuntimeTerminalKind::Repl);
468 }
469}