1use std::io::IsTerminal;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use serde_json::json;
8
9use crate::error::{CliError, exit_code_for, kind_for};
10
11pub fn use_color() -> bool {
12 !NO_COLOR.load(Ordering::Relaxed)
13 && std::env::var_os("NO_COLOR").is_none()
14 && std::io::stdout().is_terminal()
15}
16
17static NO_COLOR: AtomicBool = AtomicBool::new(false);
18
19pub fn set_no_color(disabled: bool) {
20 NO_COLOR.store(disabled, Ordering::Relaxed);
21}
22
23pub fn terminal_width() -> usize {
24 terminal_size::terminal_size()
25 .map(|(w, _)| w.0 as usize)
26 .unwrap_or(80)
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
31pub enum OutputFormat {
32 Auto,
34 Text,
36 Json,
38}
39
40#[derive(Clone, Copy, Debug)]
41pub struct OutputConfig {
42 pub json: bool,
44 pub quiet: bool,
45}
46
47impl OutputConfig {
48 pub fn new(format: OutputFormat, quiet: bool) -> Self {
53 let json = match format {
54 OutputFormat::Json => true,
55 OutputFormat::Text => false,
56 OutputFormat::Auto => !std::io::stdout().is_terminal(),
57 };
58 Self { json, quiet }
59 }
60
61 pub fn print_data(&self, data: &str) {
63 println!("{data}");
64 }
65
66 pub fn print_message(&self, msg: &str) {
68 if !self.quiet {
69 eprintln!("{msg}");
70 }
71 }
72
73 pub fn print_required_prompt(&self, msg: &str) {
81 eprintln!("{msg}");
82 }
83
84 pub fn print_json(&self, value: &serde_json::Value) {
86 println!(
87 "{}",
88 serde_json::to_string_pretty(value).expect("serialize JSON")
89 );
90 }
91
92 pub fn render_error(&self, err: &CliError) -> i32 {
99 let exit = exit_code_for(err);
100 let kind = kind_for(err);
101 let envelope = json!({
102 "error": {
103 "kind": kind,
104 "message": err.to_string(),
105 "exit_code": exit,
106 }
107 });
108 if self.json {
109 eprintln!(
110 "{}",
111 serde_json::to_string(&envelope).expect("serialize error envelope")
112 );
113 } else {
114 eprintln!("error: {err}");
115 }
116 exit
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn json_forced_on_when_not_tty() {
126 let cfg = OutputConfig::new(OutputFormat::Auto, false);
128 assert!(cfg.json);
129 }
130
131 #[test]
132 fn explicit_text_wins_over_auto() {
133 let cfg = OutputConfig::new(OutputFormat::Text, false);
134 assert!(!cfg.json, "text format must not emit JSON even when piped");
135 }
136
137 #[test]
138 fn explicit_json_wins_over_auto() {
139 let cfg = OutputConfig::new(OutputFormat::Json, false);
140 assert!(cfg.json);
141 }
142
143 #[test]
144 fn quiet_flag_propagates() {
145 let cfg = OutputConfig::new(OutputFormat::Auto, true);
146 assert!(cfg.quiet);
147 }
148
149 #[test]
150 fn render_error_returns_input_exit_for_input_error() {
151 let cfg = OutputConfig {
152 json: true,
153 quiet: true,
154 };
155 let exit = cfg.render_error(&CliError::Input("bad ref".into()));
156 assert_eq!(exit, 2);
157 }
158
159 #[test]
160 fn render_error_returns_auth_exit_for_auth_error() {
161 let cfg = OutputConfig {
162 json: true,
163 quiet: true,
164 };
165 let exit = cfg.render_error(&CliError::Auth("expired".into()));
166 assert_eq!(exit, 3);
167 }
168
169 #[test]
170 fn use_color_respects_no_color_env() {
171 unsafe { std::env::set_var("NO_COLOR", "1") };
176 assert!(!use_color());
177 unsafe { std::env::remove_var("NO_COLOR") };
178 }
179}