1use crate::detection::{OutputPreference, detected_preference};
7use std::str::FromStr;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum OutputMode {
12 Rich,
14
15 #[default]
17 Plain,
18
19 Minimal,
21}
22
23impl OutputMode {
24 #[must_use]
28 pub const fn as_str(&self) -> &'static str {
29 match self {
30 Self::Rich => "rich",
31 Self::Plain => "plain",
32 Self::Minimal => "minimal",
33 }
34 }
35
36 #[must_use]
51 pub const fn is_agent_friendly(&self) -> bool {
52 matches!(self, Self::Plain)
53 }
54
55 #[must_use]
57 pub fn auto() -> Self {
58 if let Ok(mode_str) = std::env::var("FASTAPI_OUTPUT_MODE") {
59 if let Ok(mode) = mode_str.parse::<OutputMode>() {
60 if matches!(mode, OutputMode::Rich) {
61 #[cfg(feature = "rich")]
62 {
63 return OutputMode::Rich;
64 }
65 #[cfg(not(feature = "rich"))]
66 {
67 return OutputMode::Plain;
68 }
69 }
70 return mode;
71 }
72 }
73
74 match detected_preference() {
75 OutputPreference::Plain => OutputMode::Plain,
76 OutputPreference::Rich => {
77 #[cfg(feature = "rich")]
78 {
79 OutputMode::Rich
80 }
81 #[cfg(not(feature = "rich"))]
82 {
83 OutputMode::Plain
84 }
85 }
86 }
87 }
88
89 #[must_use]
91 pub const fn uses_colors(&self) -> bool {
92 matches!(self, Self::Rich | Self::Minimal)
93 }
94
95 #[must_use]
97 pub const fn uses_boxes(&self) -> bool {
98 matches!(self, Self::Rich)
99 }
100
101 #[must_use]
103 pub const fn supports_tables(&self) -> bool {
104 matches!(self, Self::Rich)
105 }
106
107 #[must_use]
109 pub const fn success_indicator(&self) -> &'static str {
110 match self {
111 Self::Rich => "✓",
112 Self::Plain | Self::Minimal => "[OK]",
113 }
114 }
115
116 #[must_use]
118 pub const fn error_indicator(&self) -> &'static str {
119 match self {
120 Self::Rich => "✗",
121 Self::Plain | Self::Minimal => "[ERROR]",
122 }
123 }
124
125 #[must_use]
127 pub const fn warning_indicator(&self) -> &'static str {
128 match self {
129 Self::Rich => "⚠",
130 Self::Plain | Self::Minimal => "[WARN]",
131 }
132 }
133
134 #[must_use]
136 pub const fn info_indicator(&self) -> &'static str {
137 match self {
138 Self::Rich => "ℹ",
139 Self::Plain | Self::Minimal => "[INFO]",
140 }
141 }
142
143 #[must_use]
145 pub const fn uses_ansi(&self) -> bool {
146 self.uses_colors()
147 }
148
149 #[must_use]
151 pub const fn is_minimal(&self) -> bool {
152 matches!(self, Self::Minimal)
153 }
154}
155
156impl std::fmt::Display for OutputMode {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 Self::Rich => write!(f, "rich"),
160 Self::Plain => write!(f, "plain"),
161 Self::Minimal => write!(f, "minimal"),
162 }
163 }
164}
165
166impl FromStr for OutputMode {
167 type Err = OutputModeParseError;
168
169 fn from_str(s: &str) -> Result<Self, Self::Err> {
170 let normalized = s.trim().to_ascii_lowercase();
171 match normalized.as_str() {
172 "rich" => Ok(Self::Rich),
173 "plain" => Ok(Self::Plain),
174 "minimal" => Ok(Self::Minimal),
175 _ => Err(OutputModeParseError(s.to_string())),
176 }
177 }
178}
179
180#[must_use]
182pub const fn has_rich_support() -> bool {
183 cfg!(feature = "rich")
184}
185
186#[must_use]
188pub fn feature_info() -> &'static str {
189 if cfg!(feature = "full") {
190 "full (rich output with syntax highlighting)"
191 } else if cfg!(feature = "rich") {
192 "rich (styled output with tables and panels)"
193 } else {
194 "plain (text only, no dependencies)"
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct OutputModeParseError(String);
201
202impl std::fmt::Display for OutputModeParseError {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 write!(
205 f,
206 "invalid output mode '{}', expected: rich, plain, minimal",
207 self.0
208 )
209 }
210}
211
212impl std::error::Error for OutputModeParseError {}
213
214#[cfg(test)]
215#[allow(unsafe_code)]
216mod tests {
217 use super::*;
218 use serial_test::serial;
219 use std::env;
220
221 fn clean_env() {
222 unsafe {
224 env::remove_var("FASTAPI_OUTPUT_MODE");
225 env::remove_var("FASTAPI_AGENT_MODE");
226 env::remove_var("FASTAPI_HUMAN_MODE");
227 env::remove_var("CLAUDE_CODE");
228 env::remove_var("FORCE_COLOR");
229 env::remove_var("NO_COLOR");
230 env::remove_var("CI");
231 }
232 }
233
234 fn with_clean_env<F: FnOnce()>(f: F) {
235 clean_env();
236 f();
237 clean_env();
238 }
239
240 fn set_env(key: &str, value: &str) {
241 unsafe {
243 env::set_var(key, value);
244 }
245 }
246
247 #[test]
250 fn test_output_mode_default() {
251 let mode = OutputMode::default();
252 eprintln!("[TEST] Default OutputMode: {mode:?}");
253 assert_eq!(mode, OutputMode::Plain);
254 }
255
256 #[test]
257 fn test_output_mode_clone_copy() {
258 let mode = OutputMode::Rich;
259 let cloned = mode;
260 let copied = mode;
261 eprintln!(
262 "[TEST] Clone/Copy test: original={mode:?}, cloned={cloned:?}, copied={copied:?}"
263 );
264 assert_eq!(mode, cloned);
265 assert_eq!(mode, copied);
266 }
267
268 #[test]
269 fn test_output_mode_equality() {
270 assert_eq!(OutputMode::Rich, OutputMode::Rich);
271 assert_eq!(OutputMode::Plain, OutputMode::Plain);
272 assert_eq!(OutputMode::Minimal, OutputMode::Minimal);
273 assert_ne!(OutputMode::Rich, OutputMode::Plain);
274 assert_ne!(OutputMode::Plain, OutputMode::Minimal);
275 }
276
277 #[test]
280 fn test_display_rich() {
281 let s = OutputMode::Rich.to_string();
282 eprintln!("[TEST] Display Rich: {s}");
283 assert_eq!(s, "rich");
284 }
285
286 #[test]
287 fn test_display_plain() {
288 let s = OutputMode::Plain.to_string();
289 eprintln!("[TEST] Display Plain: {s}");
290 assert_eq!(s, "plain");
291 }
292
293 #[test]
294 fn test_display_minimal() {
295 let s = OutputMode::Minimal.to_string();
296 eprintln!("[TEST] Display Minimal: {s}");
297 assert_eq!(s, "minimal");
298 }
299
300 #[test]
303 fn test_parse_rich() {
304 let mode: OutputMode = "rich".parse().unwrap();
305 eprintln!("[TEST] Parse rich: {mode:?}");
306 assert_eq!(mode, OutputMode::Rich);
307 }
308
309 #[test]
310 fn test_parse_plain() {
311 let mode: OutputMode = "plain".parse().unwrap();
312 eprintln!("[TEST] Parse plain: {mode:?}");
313 assert_eq!(mode, OutputMode::Plain);
314 }
315
316 #[test]
317 fn test_parse_minimal() {
318 let mode: OutputMode = "minimal".parse().unwrap();
319 eprintln!("[TEST] Parse minimal: {mode:?}");
320 assert_eq!(mode, OutputMode::Minimal);
321 }
322
323 #[test]
324 fn test_parse_case_insensitive() {
325 assert_eq!("RICH".parse::<OutputMode>().unwrap(), OutputMode::Rich);
326 assert_eq!("Plain".parse::<OutputMode>().unwrap(), OutputMode::Plain);
327 assert_eq!(
328 "MINIMAL".parse::<OutputMode>().unwrap(),
329 OutputMode::Minimal
330 );
331 eprintln!("[TEST] Case insensitive parsing works");
332 }
333
334 #[test]
335 fn test_parse_invalid() {
336 let result = "invalid".parse::<OutputMode>();
337 eprintln!("[TEST] Parse invalid: {result:?}");
338 assert!(result.is_err());
339 let err = result.unwrap_err();
340 assert!(err.to_string().contains("invalid"));
341 }
342
343 #[test]
346 fn test_uses_colors() {
347 eprintln!(
348 "[TEST] uses_colors: Rich={}, Plain={}, Minimal={}",
349 OutputMode::Rich.uses_colors(),
350 OutputMode::Plain.uses_colors(),
351 OutputMode::Minimal.uses_colors()
352 );
353 assert!(OutputMode::Rich.uses_colors());
354 assert!(!OutputMode::Plain.uses_colors());
355 assert!(OutputMode::Minimal.uses_colors());
356 }
357
358 #[test]
359 fn test_uses_boxes() {
360 eprintln!(
361 "[TEST] uses_boxes: Rich={}, Plain={}, Minimal={}",
362 OutputMode::Rich.uses_boxes(),
363 OutputMode::Plain.uses_boxes(),
364 OutputMode::Minimal.uses_boxes()
365 );
366 assert!(OutputMode::Rich.uses_boxes());
367 assert!(!OutputMode::Plain.uses_boxes());
368 assert!(!OutputMode::Minimal.uses_boxes());
369 }
370
371 #[test]
372 fn test_supports_tables() {
373 eprintln!(
374 "[TEST] supports_tables: Rich={}, Plain={}, Minimal={}",
375 OutputMode::Rich.supports_tables(),
376 OutputMode::Plain.supports_tables(),
377 OutputMode::Minimal.supports_tables()
378 );
379 assert!(OutputMode::Rich.supports_tables());
380 assert!(!OutputMode::Plain.supports_tables());
381 assert!(!OutputMode::Minimal.supports_tables());
382 }
383
384 #[test]
385 fn test_feature_info_matches_flags() {
386 let info = feature_info();
387 eprintln!("[TEST] feature_info: {info}");
388 if cfg!(feature = "full") {
389 assert!(info.contains("full"));
390 } else if cfg!(feature = "rich") {
391 assert!(info.contains("rich"));
392 } else {
393 assert!(info.contains("plain"));
394 }
395 }
396
397 #[test]
398 fn test_has_rich_support_flag() {
399 let expected = cfg!(feature = "rich");
400 eprintln!(
401 "[TEST] has_rich_support: expected={}, actual={}",
402 expected,
403 has_rich_support()
404 );
405 assert_eq!(has_rich_support(), expected);
406 }
407
408 #[test]
411 fn test_success_indicators() {
412 eprintln!(
413 "[TEST] success_indicator: Rich={}, Plain={}, Minimal={}",
414 OutputMode::Rich.success_indicator(),
415 OutputMode::Plain.success_indicator(),
416 OutputMode::Minimal.success_indicator()
417 );
418 assert_eq!(OutputMode::Rich.success_indicator(), "✓");
419 assert_eq!(OutputMode::Plain.success_indicator(), "[OK]");
420 assert_eq!(OutputMode::Minimal.success_indicator(), "[OK]");
421 }
422
423 #[test]
424 fn test_error_indicators() {
425 eprintln!(
426 "[TEST] error_indicator: Rich={}, Plain={}, Minimal={}",
427 OutputMode::Rich.error_indicator(),
428 OutputMode::Plain.error_indicator(),
429 OutputMode::Minimal.error_indicator()
430 );
431 assert_eq!(OutputMode::Rich.error_indicator(), "✗");
432 assert_eq!(OutputMode::Plain.error_indicator(), "[ERROR]");
433 assert_eq!(OutputMode::Minimal.error_indicator(), "[ERROR]");
434 }
435
436 #[test]
437 fn test_warning_indicators() {
438 eprintln!(
439 "[TEST] warning_indicator: Rich={}, Plain={}, Minimal={}",
440 OutputMode::Rich.warning_indicator(),
441 OutputMode::Plain.warning_indicator(),
442 OutputMode::Minimal.warning_indicator()
443 );
444 assert_eq!(OutputMode::Rich.warning_indicator(), "⚠");
445 assert_eq!(OutputMode::Plain.warning_indicator(), "[WARN]");
446 assert_eq!(OutputMode::Minimal.warning_indicator(), "[WARN]");
447 }
448
449 #[test]
450 fn test_info_indicators() {
451 eprintln!(
452 "[TEST] info_indicator: Rich={}, Plain={}, Minimal={}",
453 OutputMode::Rich.info_indicator(),
454 OutputMode::Plain.info_indicator(),
455 OutputMode::Minimal.info_indicator()
456 );
457 assert_eq!(OutputMode::Rich.info_indicator(), "ℹ");
458 assert_eq!(OutputMode::Plain.info_indicator(), "[INFO]");
459 assert_eq!(OutputMode::Minimal.info_indicator(), "[INFO]");
460 }
461
462 #[test]
465 #[serial]
466 fn test_auto_explicit_plain_override() {
467 with_clean_env(|| {
468 set_env("FASTAPI_OUTPUT_MODE", "plain");
469 let mode = OutputMode::auto();
470 eprintln!("[TEST] Explicit plain override: {mode:?}");
471 assert_eq!(mode, OutputMode::Plain);
472 });
473 }
474
475 #[test]
476 #[serial]
477 fn test_auto_explicit_minimal_override() {
478 with_clean_env(|| {
479 set_env("FASTAPI_OUTPUT_MODE", "minimal");
480 let mode = OutputMode::auto();
481 eprintln!("[TEST] Explicit minimal override: {mode:?}");
482 assert_eq!(mode, OutputMode::Minimal);
483 });
484 }
485
486 #[test]
487 #[serial]
488 fn test_auto_agent_detected() {
489 with_clean_env(|| {
490 set_env("CLAUDE_CODE", "1");
491 let mode = OutputMode::auto();
492 eprintln!("[TEST] Agent detected mode: {mode:?}");
493 assert_eq!(mode, OutputMode::Plain);
494 });
495 }
496
497 #[test]
498 #[serial]
499 fn test_auto_ci_detected() {
500 with_clean_env(|| {
501 set_env("CI", "true");
502 let mode = OutputMode::auto();
503 eprintln!("[TEST] CI detected mode: {mode:?}");
504 assert_eq!(mode, OutputMode::Plain);
505 });
506 }
507
508 #[test]
509 #[serial]
510 fn test_auto_no_color_detected() {
511 with_clean_env(|| {
512 set_env("NO_COLOR", "1");
513 let mode = OutputMode::auto();
514 eprintln!("[TEST] NO_COLOR detected mode: {mode:?}");
515 assert_eq!(mode, OutputMode::Plain);
516 });
517 }
518
519 #[test]
520 #[serial]
521 fn test_explicit_override_beats_detection() {
522 with_clean_env(|| {
523 set_env("CLAUDE_CODE", "1");
524 set_env("FASTAPI_OUTPUT_MODE", "minimal");
525 let mode = OutputMode::auto();
526 eprintln!("[TEST] Override beats detection: {mode:?}");
527 assert_eq!(mode, OutputMode::Minimal);
528 });
529 }
530
531 #[test]
532 #[serial]
533 fn test_auto_deterministic() {
534 with_clean_env(|| {
535 set_env("CI", "true");
536 let mode1 = OutputMode::auto();
537 let mode2 = OutputMode::auto();
538 let mode3 = OutputMode::auto();
539 eprintln!("[TEST] Deterministic: {mode1:?} == {mode2:?} == {mode3:?}");
540 assert_eq!(mode1, mode2);
541 assert_eq!(mode2, mode3);
542 });
543 }
544
545 #[test]
548 fn test_parse_error_display() {
549 let err = OutputModeParseError("foobar".to_string());
550 let msg = err.to_string();
551 eprintln!("[TEST] Parse error display: {msg}");
552 assert!(msg.contains("foobar"));
553 assert!(msg.contains("rich"));
554 assert!(msg.contains("plain"));
555 assert!(msg.contains("minimal"));
556 }
557
558 #[test]
559 fn test_parse_error_is_error() {
560 let err = OutputModeParseError("x".to_string());
561 let _: &dyn std::error::Error = &err;
562 eprintln!("[TEST] OutputModeParseError implements Error trait");
563 }
564
565 #[test]
568 fn test_as_str_rich() {
569 assert_eq!(OutputMode::Rich.as_str(), "rich");
570 }
571
572 #[test]
573 fn test_as_str_plain() {
574 assert_eq!(OutputMode::Plain.as_str(), "plain");
575 }
576
577 #[test]
578 fn test_as_str_minimal() {
579 assert_eq!(OutputMode::Minimal.as_str(), "minimal");
580 }
581
582 #[test]
583 fn test_as_str_matches_display() {
584 assert_eq!(OutputMode::Rich.as_str(), OutputMode::Rich.to_string());
586 assert_eq!(OutputMode::Plain.as_str(), OutputMode::Plain.to_string());
587 assert_eq!(
588 OutputMode::Minimal.as_str(),
589 OutputMode::Minimal.to_string()
590 );
591 }
592
593 #[test]
596 fn test_is_agent_friendly_plain() {
597 assert!(OutputMode::Plain.is_agent_friendly());
598 }
599
600 #[test]
601 fn test_is_agent_friendly_rich() {
602 assert!(!OutputMode::Rich.is_agent_friendly());
603 }
604
605 #[test]
606 fn test_is_agent_friendly_minimal() {
607 assert!(!OutputMode::Minimal.is_agent_friendly());
608 }
609
610 #[test]
611 fn test_is_agent_friendly_consistency() {
612 let modes = [OutputMode::Rich, OutputMode::Plain, OutputMode::Minimal];
614 let agent_friendly_count = modes.iter().filter(|m| m.is_agent_friendly()).count();
615 assert_eq!(
616 agent_friendly_count, 1,
617 "Only Plain should be agent-friendly"
618 );
619 }
620}