1use std::io::IsTerminal;
5
6use serde_json::json;
7
8use crate::error::{CliError, exit_code_for, kind_for};
9
10pub fn use_color() -> bool {
11 std::env::var_os("NO_COLOR").is_none() && std::io::stdout().is_terminal()
12}
13
14pub fn terminal_width() -> usize {
15 terminal_size::terminal_size()
16 .map(|(w, _)| w.0 as usize)
17 .unwrap_or(80)
18}
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
22pub enum OutputFormat {
23 Auto,
25 Text,
27 Json,
29}
30
31#[derive(Clone, Copy, Debug)]
32pub struct OutputConfig {
33 pub json: bool,
35 pub quiet: bool,
36}
37
38impl OutputConfig {
39 pub fn new(format: OutputFormat, quiet: bool) -> Self {
44 let json = match format {
45 OutputFormat::Json => true,
46 OutputFormat::Text => false,
47 OutputFormat::Auto => !std::io::stdout().is_terminal(),
48 };
49 Self { json, quiet }
50 }
51
52 pub fn print_data(&self, data: &str) {
54 println!("{data}");
55 }
56
57 pub fn print_message(&self, msg: &str) {
59 if !self.quiet {
60 eprintln!("{msg}");
61 }
62 }
63
64 pub fn print_required_prompt(&self, msg: &str) {
72 eprintln!("{msg}");
73 }
74
75 pub fn print_json(&self, value: &serde_json::Value) {
77 println!(
78 "{}",
79 serde_json::to_string_pretty(value).expect("serialize JSON")
80 );
81 }
82
83 pub fn render_error(&self, err: &CliError) -> i32 {
92 let exit = exit_code_for(err);
93 let kind = kind_for(err);
94 let envelope = json!({
95 "error": {
96 "kind": kind,
97 "message": err.to_string(),
98 "exit_code": exit,
99 }
100 });
101 if !self.json {
103 eprintln!("error: {err}");
104 }
105 eprintln!(
106 "{}",
107 serde_json::to_string(&envelope).expect("serialize error envelope")
108 );
109 exit
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn json_forced_on_when_not_tty() {
119 let cfg = OutputConfig::new(OutputFormat::Auto, false);
121 assert!(cfg.json);
122 }
123
124 #[test]
125 fn explicit_text_wins_over_auto() {
126 let cfg = OutputConfig::new(OutputFormat::Text, false);
127 assert!(!cfg.json, "text format must not emit JSON even when piped");
128 }
129
130 #[test]
131 fn explicit_json_wins_over_auto() {
132 let cfg = OutputConfig::new(OutputFormat::Json, false);
133 assert!(cfg.json);
134 }
135
136 #[test]
137 fn quiet_flag_propagates() {
138 let cfg = OutputConfig::new(OutputFormat::Auto, true);
139 assert!(cfg.quiet);
140 }
141
142 #[test]
143 fn render_error_returns_input_exit_for_input_error() {
144 let cfg = OutputConfig {
145 json: true,
146 quiet: true,
147 };
148 let exit = cfg.render_error(&CliError::Input("bad ref".into()));
149 assert_eq!(exit, 2);
150 }
151
152 #[test]
153 fn render_error_returns_auth_exit_for_auth_error() {
154 let cfg = OutputConfig {
155 json: true,
156 quiet: true,
157 };
158 let exit = cfg.render_error(&CliError::Auth("expired".into()));
159 assert_eq!(exit, 3);
160 }
161
162 #[test]
163 fn use_color_respects_no_color_env() {
164 unsafe { std::env::set_var("NO_COLOR", "1") };
169 assert!(!use_color());
170 unsafe { std::env::remove_var("NO_COLOR") };
171 }
172}