Skip to main content

systemprompt_cli/
cli_settings.rs

1//! CLI-wide runtime settings: output format, verbosity, colour, and
2//! interactivity.
3//!
4//! [`CliConfig`] holds the resolved presentation options for a CLI invocation:
5//! defaults, overridden by the [`crate::env_overrides::EnvOverrides`] snapshot
6//! ([`CliConfig::resolve`]), overridden by command-line flags. The
7//! [`OutputFormat`], [`VerbosityLevel`], and [`ColorMode`] enums express the
8//! individual axes. The resolved config travels explicitly on
9//! [`crate::context::CommandContext`]; there is no process-global instance.
10
11use std::io::IsTerminal;
12
13use crate::env_overrides::EnvOverrides;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum OutputFormat {
17    Table,
18    Json,
19    Yaml,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub enum VerbosityLevel {
24    Quiet,
25    Normal,
26    Verbose,
27    Debug,
28}
29
30impl VerbosityLevel {
31    pub const fn as_tracing_filter(&self) -> Option<&'static str> {
32        match self {
33            Self::Quiet => Some("error"),
34            Self::Normal => None,
35            Self::Verbose => Some("debug"),
36            Self::Debug => Some("trace"),
37        }
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ColorMode {
43    Auto,
44    Always,
45    Never,
46}
47
48#[derive(Debug, Clone)]
49pub struct CliConfig {
50    pub output_format: OutputFormat,
51    pub verbosity: VerbosityLevel,
52    pub color_mode: ColorMode,
53    pub interactive: bool,
54    pub assume_terminal: bool,
55    pub profile_override: Option<String>,
56}
57
58impl Default for CliConfig {
59    fn default() -> Self {
60        Self {
61            output_format: OutputFormat::Table,
62            verbosity: VerbosityLevel::Normal,
63            color_mode: ColorMode::Auto,
64            interactive: true,
65            assume_terminal: false,
66            profile_override: None,
67        }
68    }
69}
70
71impl CliConfig {
72    #[must_use]
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    #[must_use]
78    pub fn resolve(env: &EnvOverrides) -> Self {
79        let mut config = Self::default();
80        config.apply_env(env);
81        config
82    }
83
84    pub const fn with_output_format(mut self, format: OutputFormat) -> Self {
85        self.output_format = format;
86        self
87    }
88
89    pub const fn with_verbosity(mut self, level: VerbosityLevel) -> Self {
90        self.verbosity = level;
91        self
92    }
93
94    pub const fn with_color_mode(mut self, mode: ColorMode) -> Self {
95        self.color_mode = mode;
96        self
97    }
98
99    pub const fn with_interactive(mut self, interactive: bool) -> Self {
100        self.interactive = interactive;
101        self
102    }
103
104    /// Treat the session as terminal-attached even when stdio is piped.
105    ///
106    /// Interactive flows are driven through the
107    /// [`crate::interactive::Prompter`] seam in tests, where no TTY exists;
108    /// this bypasses the terminal probe so a scripted prompter can reach
109    /// them.
110    pub const fn with_assume_terminal(mut self, assume: bool) -> Self {
111        self.assume_terminal = assume;
112        self
113    }
114
115    pub fn with_profile_override(mut self, profile: Option<String>) -> Self {
116        self.profile_override = profile;
117        self
118    }
119
120    fn apply_env(&mut self, env: &EnvOverrides) {
121        if let Some(format) = &env.output_format {
122            self.output_format = match format.to_lowercase().as_str() {
123                "json" => OutputFormat::Json,
124                "yaml" => OutputFormat::Yaml,
125                "table" => OutputFormat::Table,
126                _ => self.output_format,
127            };
128        }
129
130        if let Some(level) = &env.log_level {
131            self.verbosity = match level.to_lowercase().as_str() {
132                "quiet" => VerbosityLevel::Quiet,
133                "normal" => VerbosityLevel::Normal,
134                "verbose" => VerbosityLevel::Verbose,
135                "debug" => VerbosityLevel::Debug,
136                _ => self.verbosity,
137            };
138        }
139
140        if env.no_color {
141            self.color_mode = ColorMode::Never;
142        }
143
144        if env.non_interactive {
145            self.interactive = false;
146        }
147    }
148
149    pub fn should_use_color(&self) -> bool {
150        match self.color_mode {
151            ColorMode::Always => true,
152            ColorMode::Never => false,
153            ColorMode::Auto => std::io::stdout().is_terminal(),
154        }
155    }
156
157    pub fn is_json_output(&self) -> bool {
158        self.output_format == OutputFormat::Json
159    }
160
161    pub fn should_show_verbose(&self) -> bool {
162        self.verbosity >= VerbosityLevel::Verbose
163    }
164
165    pub fn is_interactive(&self) -> bool {
166        self.interactive
167            && (self.assume_terminal
168                || (std::io::stdin().is_terminal() && std::io::stdout().is_terminal()))
169    }
170
171    pub const fn output_format(&self) -> OutputFormat {
172        self.output_format
173    }
174}