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//! seeded from defaults and overridden by environment variables. The
6//! [`OutputFormat`], [`VerbosityLevel`], and [`ColorMode`] enums express the
7//! individual axes. A thread-local instance is published via
8//! [`set_global_config`] / [`get_global_config`] so command handlers can read
9//! the active settings without threading them through every call.
10
11use std::env;
12use std::io::IsTerminal;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum OutputFormat {
16    Table,
17    Json,
18    Yaml,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
22pub enum VerbosityLevel {
23    Quiet,
24    Normal,
25    Verbose,
26    Debug,
27}
28
29impl VerbosityLevel {
30    pub const fn as_tracing_filter(&self) -> Option<&'static str> {
31        match self {
32            Self::Quiet => Some("error"),
33            Self::Normal => None,
34            Self::Verbose => Some("debug"),
35            Self::Debug => Some("trace"),
36        }
37    }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ColorMode {
42    Auto,
43    Always,
44    Never,
45}
46
47#[derive(Debug, Clone)]
48pub struct CliConfig {
49    pub output_format: OutputFormat,
50    pub verbosity: VerbosityLevel,
51    pub color_mode: ColorMode,
52    pub interactive: bool,
53    pub profile_override: Option<String>,
54}
55
56impl Default for CliConfig {
57    fn default() -> Self {
58        Self {
59            output_format: OutputFormat::Table,
60            verbosity: VerbosityLevel::Normal,
61            color_mode: ColorMode::Auto,
62            interactive: true,
63            profile_override: None,
64        }
65    }
66}
67
68impl CliConfig {
69    pub fn new() -> Self {
70        let mut config = Self::default();
71        config.apply_environment_variables();
72        config
73    }
74
75    pub const fn with_output_format(mut self, format: OutputFormat) -> Self {
76        self.output_format = format;
77        self
78    }
79
80    pub const fn with_verbosity(mut self, level: VerbosityLevel) -> Self {
81        self.verbosity = level;
82        self
83    }
84
85    pub const fn with_color_mode(mut self, mode: ColorMode) -> Self {
86        self.color_mode = mode;
87        self
88    }
89
90    pub const fn with_interactive(mut self, interactive: bool) -> Self {
91        self.interactive = interactive;
92        self
93    }
94
95    pub fn with_profile_override(mut self, profile: Option<String>) -> Self {
96        self.profile_override = profile;
97        self
98    }
99
100    fn apply_environment_variables(&mut self) {
101        if let Ok(format) = env::var("SYSTEMPROMPT_OUTPUT_FORMAT") {
102            self.output_format = match format.to_lowercase().as_str() {
103                "json" => OutputFormat::Json,
104                "yaml" => OutputFormat::Yaml,
105                "table" => OutputFormat::Table,
106                _ => self.output_format,
107            };
108        }
109
110        if let Ok(level) = env::var("SYSTEMPROMPT_LOG_LEVEL") {
111            self.verbosity = match level.to_lowercase().as_str() {
112                "quiet" => VerbosityLevel::Quiet,
113                "normal" => VerbosityLevel::Normal,
114                "verbose" => VerbosityLevel::Verbose,
115                "debug" => VerbosityLevel::Debug,
116                _ => self.verbosity,
117            };
118        }
119
120        if env::var("SYSTEMPROMPT_NO_COLOR").is_ok() || env::var("NO_COLOR").is_ok() {
121            self.color_mode = ColorMode::Never;
122        }
123
124        if env::var("SYSTEMPROMPT_NON_INTERACTIVE").is_ok() {
125            self.interactive = false;
126        }
127    }
128
129    pub fn should_use_color(&self) -> bool {
130        match self.color_mode {
131            ColorMode::Always => true,
132            ColorMode::Never => false,
133            ColorMode::Auto => std::io::stdout().is_terminal(),
134        }
135    }
136
137    pub fn is_json_output(&self) -> bool {
138        self.output_format == OutputFormat::Json
139    }
140
141    pub fn should_show_verbose(&self) -> bool {
142        self.verbosity >= VerbosityLevel::Verbose
143    }
144
145    pub fn is_interactive(&self) -> bool {
146        self.interactive && std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
147    }
148
149    pub const fn output_format(&self) -> OutputFormat {
150        self.output_format
151    }
152}
153
154thread_local! {
155    static CLI_CONFIG: std::cell::RefCell<CliConfig> = std::cell::RefCell::new(CliConfig::new());
156}
157
158pub fn set_global_config(config: CliConfig) {
159    CLI_CONFIG.with(|c| {
160        *c.borrow_mut() = config;
161    });
162}
163
164pub fn get_global_config() -> CliConfig {
165    CLI_CONFIG.with(|c| c.borrow().clone())
166}