1use crate::detection::DisplayContext;
8use std::env;
9
10#[derive(Debug, Clone)]
12pub struct ConsoleConfig {
13 pub context: Option<DisplayContext>,
16 pub force_color: Option<bool>,
18 pub force_plain: bool,
20
21 pub custom_colors: Option<CustomColors>,
24
25 pub show_banner: bool,
28 pub show_capabilities: bool,
30 pub banner_style: BannerStyle,
32
33 pub log_level: Option<log::Level>,
36 pub log_timestamps: bool,
38 pub log_targets: bool,
40 pub log_file_line: bool,
42
43 pub show_stats_periodic: bool,
46 pub stats_interval_secs: u64,
48 pub show_request_traffic: bool,
50 pub traffic_verbosity: TrafficVerbosity,
52
53 pub show_suggestions: bool,
56 pub show_error_codes: bool,
58 pub show_backtrace: bool,
60
61 pub max_table_rows: usize,
64 pub max_json_depth: usize,
66 pub truncate_at: usize,
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub enum BannerStyle {
73 #[default]
75 Full,
76 Compact,
78 Minimal,
80 None,
82}
83
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
86pub enum TrafficVerbosity {
87 #[default]
89 None,
90 Summary,
92 Headers,
94 Full,
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct CustomColors {
101 pub primary: Option<String>,
103 pub secondary: Option<String>,
105 pub success: Option<String>,
107 pub warning: Option<String>,
109 pub error: Option<String>,
111}
112
113impl Default for ConsoleConfig {
114 fn default() -> Self {
115 Self {
116 context: None,
117 force_color: None,
118 force_plain: false,
119 custom_colors: None,
120 show_banner: true,
121 show_capabilities: true,
122 banner_style: BannerStyle::Full,
123 log_level: None,
124 log_timestamps: true,
125 log_targets: true,
126 log_file_line: false,
127 show_stats_periodic: false,
128 stats_interval_secs: 60,
129 show_request_traffic: false,
130 traffic_verbosity: TrafficVerbosity::None,
131 show_suggestions: true,
132 show_error_codes: true,
133 show_backtrace: false,
134 max_table_rows: 100,
135 max_json_depth: 5,
136 truncate_at: 200,
137 }
138 }
139}
140
141impl ConsoleConfig {
142 #[must_use]
144 pub fn new() -> Self {
145 Self::default()
146 }
147
148 #[must_use]
163 pub fn from_env() -> Self {
164 let mut config = Self::default();
165
166 if env::var("FASTMCP_FORCE_COLOR").is_ok() {
168 config.force_color = Some(true);
169 }
170 if env::var("FASTMCP_PLAIN").is_ok() || env::var("NO_COLOR").is_ok() {
171 config.force_plain = true;
172 }
173
174 if let Ok(val) = env::var("FASTMCP_BANNER") {
176 config.banner_style = match val.to_lowercase().as_str() {
177 "compact" => BannerStyle::Compact,
178 "minimal" => BannerStyle::Minimal,
179 "none" | "0" | "false" => BannerStyle::None,
180 _ => BannerStyle::Full,
182 };
183 config.show_banner = !matches!(config.banner_style, BannerStyle::None);
184 }
185
186 if let Ok(level) = env::var("FASTMCP_LOG") {
188 config.log_level = match level.to_lowercase().as_str() {
189 "trace" => Some(log::Level::Trace),
190 "debug" => Some(log::Level::Debug),
191 "info" => Some(log::Level::Info),
192 "warn" | "warning" => Some(log::Level::Warn),
193 "error" => Some(log::Level::Error),
194 _ => None,
195 };
196 }
197 if env::var("FASTMCP_LOG_TIMESTAMPS")
198 .map(|v| v == "0" || v.to_lowercase() == "false")
199 .unwrap_or(false)
200 {
201 config.log_timestamps = false;
202 }
203
204 if let Ok(val) = env::var("FASTMCP_TRAFFIC") {
206 config.traffic_verbosity = match val.to_lowercase().as_str() {
207 "summary" | "1" => TrafficVerbosity::Summary,
208 "headers" | "2" => TrafficVerbosity::Headers,
209 "full" | "3" => TrafficVerbosity::Full,
210 _ => TrafficVerbosity::None,
212 };
213 config.show_request_traffic =
214 !matches!(config.traffic_verbosity, TrafficVerbosity::None);
215 }
216
217 if env::var("RUST_BACKTRACE").is_ok() {
219 config.show_backtrace = true;
220 }
221
222 config
223 }
224
225 #[must_use]
231 pub fn force_color(mut self, force: bool) -> Self {
232 self.force_color = Some(force);
233 self
234 }
235
236 #[must_use]
238 pub fn plain_mode(mut self) -> Self {
239 self.force_plain = true;
240 self
241 }
242
243 #[must_use]
245 pub fn with_banner(mut self, style: BannerStyle) -> Self {
246 self.banner_style = style;
247 self.show_banner = !matches!(style, BannerStyle::None);
248 self
249 }
250
251 #[must_use]
253 pub fn without_banner(mut self) -> Self {
254 self.show_banner = false;
255 self.banner_style = BannerStyle::None;
256 self
257 }
258
259 #[must_use]
261 pub fn with_log_level(mut self, level: log::Level) -> Self {
262 self.log_level = Some(level);
263 self
264 }
265
266 #[must_use]
268 pub fn with_traffic(mut self, verbosity: TrafficVerbosity) -> Self {
269 self.traffic_verbosity = verbosity;
270 self.show_request_traffic = !matches!(verbosity, TrafficVerbosity::None);
271 self
272 }
273
274 #[must_use]
276 pub fn with_periodic_stats(mut self, interval_secs: u64) -> Self {
277 self.show_stats_periodic = true;
278 self.stats_interval_secs = interval_secs;
279 self
280 }
281
282 #[must_use]
284 pub fn without_suggestions(mut self) -> Self {
285 self.show_suggestions = false;
286 self
287 }
288
289 #[must_use]
291 pub fn with_custom_colors(mut self, colors: CustomColors) -> Self {
292 self.custom_colors = Some(colors);
293 self
294 }
295
296 #[must_use]
298 pub fn with_context(mut self, context: DisplayContext) -> Self {
299 self.context = Some(context);
300 self
301 }
302
303 #[must_use]
305 pub fn with_max_table_rows(mut self, max: usize) -> Self {
306 self.max_table_rows = max;
307 self
308 }
309
310 #[must_use]
312 pub fn with_max_json_depth(mut self, max: usize) -> Self {
313 self.max_json_depth = max;
314 self
315 }
316
317 #[must_use]
319 pub fn with_truncate_at(mut self, len: usize) -> Self {
320 self.truncate_at = len;
321 self
322 }
323
324 #[must_use]
330 pub fn theme(&self) -> &'static crate::theme::FastMcpTheme {
331 crate::theme::theme()
332 }
333
334 #[must_use]
340 pub fn resolve_context(&self) -> DisplayContext {
341 if self.force_plain {
342 return DisplayContext::new_agent();
343 }
344 if let Some(true) = self.force_color {
345 return DisplayContext::new_human();
346 }
347 self.context.unwrap_or_else(DisplayContext::detect)
348 }
349
350 #[must_use]
352 pub fn should_use_rich(&self) -> bool {
353 self.resolve_context().is_human()
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_default_config() {
363 let config = ConsoleConfig::new();
364 assert!(config.show_banner);
365 assert!(config.show_capabilities);
366 assert_eq!(config.banner_style, BannerStyle::Full);
367 assert!(config.log_timestamps);
368 assert!(!config.force_plain);
369 assert_eq!(config.max_table_rows, 100);
370 }
371
372 #[test]
373 fn test_builder_pattern() {
374 let config = ConsoleConfig::new()
375 .with_banner(BannerStyle::Compact)
376 .with_log_level(log::Level::Debug)
377 .with_traffic(TrafficVerbosity::Summary)
378 .with_periodic_stats(30);
379
380 assert_eq!(config.banner_style, BannerStyle::Compact);
381 assert_eq!(config.log_level, Some(log::Level::Debug));
382 assert_eq!(config.traffic_verbosity, TrafficVerbosity::Summary);
383 assert!(config.show_stats_periodic);
384 assert_eq!(config.stats_interval_secs, 30);
385 }
386
387 #[test]
388 fn test_plain_mode() {
389 let config = ConsoleConfig::new().plain_mode();
390 assert!(config.force_plain);
391 assert_eq!(config.resolve_context(), DisplayContext::Agent);
392 }
393
394 #[test]
395 fn test_force_color() {
396 let config = ConsoleConfig::new().force_color(true);
397 assert_eq!(config.force_color, Some(true));
398 assert_eq!(config.resolve_context(), DisplayContext::Human);
399 }
400
401 #[test]
402 fn test_without_banner() {
403 let config = ConsoleConfig::new().without_banner();
404 assert!(!config.show_banner);
405 assert_eq!(config.banner_style, BannerStyle::None);
406 }
407}