romm_cli/
cli_presentation.rs1use std::io::{self, IsTerminal, Write};
4
5use indicatif::{MultiProgress, ProgressDrawTarget, ProgressStyle};
6use serde::Serialize;
7
8use crate::commands::OutputFormat;
9use crate::error::{user_message, RommError};
10
11#[derive(Clone, Copy, Debug)]
13pub struct CliPresentation {
14 pub format: OutputFormat,
15 pub verbose: bool,
16}
17
18impl CliPresentation {
19 pub fn from_cli(global_json: bool, local_json: bool, verbose: bool) -> Self {
20 Self {
21 format: OutputFormat::from_flags(global_json, local_json),
22 verbose,
23 }
24 }
25
26 pub fn is_json(&self) -> bool {
27 matches!(self.format, OutputFormat::Json)
28 }
29
30 pub fn is_text(&self) -> bool {
31 matches!(self.format, OutputFormat::Text)
32 }
33
34 pub fn supports_ansi_color(&self) -> bool {
36 if std::env::var_os("NO_COLOR").is_some() {
37 return false;
38 }
39 if std::env::var("CLICOLOR").as_deref() == Ok("0")
40 && std::env::var("CLICOLOR_FORCE").as_deref() != Ok("1")
41 {
42 return false;
43 }
44 io::stdout().is_terminal()
45 }
46
47 pub fn shows_progress(&self) -> bool {
49 self.is_text() && io::stdout().is_terminal()
50 }
51
52 pub fn progress_draw_target(&self) -> ProgressDrawTarget {
53 ProgressDrawTarget::stderr()
54 }
55
56 pub fn progress_style(&self, template_plain: &str, template_color: &str) -> ProgressStyle {
57 let template = if self.supports_ansi_color() {
58 template_color
59 } else {
60 template_plain
61 };
62 ProgressStyle::with_template(template)
63 .expect("hardcoded progress template")
64 .progress_chars("#>-")
65 }
66
67 pub fn multi_progress(&self) -> Option<MultiProgress> {
68 if !self.shows_progress() {
69 return None;
70 }
71 let mp = MultiProgress::with_draw_target(self.progress_draw_target());
72 Some(mp)
73 }
74
75 pub fn emit_status(&self, message: impl AsRef<str>) {
77 if self.is_text() {
78 let _ = writeln!(io::stderr(), "{}", message.as_ref());
79 }
80 }
81
82 pub fn emit_json<T: Serialize>(&self, value: &T) -> Result<(), RommError> {
84 if self.is_json() {
85 println!(
86 "{}",
87 serde_json::to_string_pretty(value).map_err(|e| {
88 RommError::Other(format!("failed to serialize JSON output: {e}"))
89 })?
90 );
91 }
92 Ok(())
93 }
94
95 pub fn emit_command_error(&self, err: &RommError) {
97 eprintln!("Error: {}", user_message(err));
98 if self.verbose {
99 eprintln!("Details: {err:#}");
100 }
101 }
102}
103
104pub fn format_command_error(err: &RommError) -> String {
106 user_message(err)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn clear_color_env() {
114 std::env::remove_var("NO_COLOR");
115 std::env::remove_var("CLICOLOR");
116 std::env::remove_var("CLICOLOR_FORCE");
117 }
118
119 #[test]
120 fn json_format_suppresses_progress() {
121 clear_color_env();
122 let p = CliPresentation::from_cli(true, false, false);
123 assert!(!p.shows_progress());
124 assert!(p.is_json());
125 }
126
127 #[test]
128 fn no_color_disables_ansi() {
129 clear_color_env();
130 std::env::set_var("NO_COLOR", "1");
131 let p = CliPresentation::from_cli(false, false, false);
132 assert!(!p.supports_ansi_color());
133 std::env::remove_var("NO_COLOR");
134 }
135
136 #[test]
137 fn clicolor_zero_disables_ansi() {
138 clear_color_env();
139 std::env::set_var("CLICOLOR", "0");
140 let p = CliPresentation::from_cli(false, false, false);
141 assert!(!p.supports_ansi_color());
142 std::env::remove_var("CLICOLOR");
143 }
144
145 #[test]
146 fn clicolor_force_overrides_clicolor_zero() {
147 clear_color_env();
148 std::env::set_var("CLICOLOR", "0");
149 std::env::set_var("CLICOLOR_FORCE", "1");
150 let p = CliPresentation::from_cli(false, false, false);
151 let _ = p.supports_ansi_color();
153 std::env::remove_var("CLICOLOR");
154 std::env::remove_var("CLICOLOR_FORCE");
155 }
156}