1use std::io::IsTerminal;
5
6use serde_json::json;
7
8use crate::error::{CliError, exit_code_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)]
21pub struct OutputConfig {
22 pub json: bool,
23 pub quiet: bool,
24}
25
26impl OutputConfig {
27 pub fn new(json_flag: bool, quiet: bool) -> Self {
29 let json = json_flag || !std::io::stdout().is_terminal();
30 Self { json, quiet }
31 }
32
33 pub fn print_data(&self, data: &str) {
35 println!("{data}");
36 }
37
38 pub fn print_message(&self, msg: &str) {
40 if !self.quiet {
41 eprintln!("{msg}");
42 }
43 }
44
45 pub fn print_required_prompt(&self, msg: &str) {
53 eprintln!("{msg}");
54 }
55
56 pub fn print_json(&self, value: &serde_json::Value) {
58 println!(
59 "{}",
60 serde_json::to_string_pretty(value).expect("serialize JSON")
61 );
62 }
63
64 pub fn render_error(&self, err: &CliError) -> i32 {
71 let exit = exit_code_for(err);
72 if self.json {
73 let code = match err {
74 CliError::Input(_) => "input",
75 CliError::Auth(_) => "auth",
76 CliError::ReadOnly(_) => "read_only",
77 CliError::NotFound(_) => "not_found",
78 CliError::Api { .. } => "api",
79 CliError::RateLimit => "rate_limit",
80 CliError::Http(_) => "http",
81 CliError::Other(_) => "other",
82 };
83 let value = json!({
84 "error": {
85 "code": code,
86 "message": err.to_string(),
87 "exit": exit,
88 }
89 });
90 self.print_json(&value);
91 } else {
92 eprintln!("error: {err}");
93 }
94 exit
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 #[test]
103 fn json_forced_on_when_not_tty() {
104 let cfg = OutputConfig::new(false, false);
106 assert!(cfg.json);
107 }
108
109 #[test]
110 fn quiet_flag_propagates() {
111 let cfg = OutputConfig::new(false, true);
112 assert!(cfg.quiet);
113 }
114
115 #[test]
116 fn render_error_returns_input_exit_for_input_error() {
117 let cfg = OutputConfig {
118 json: true,
119 quiet: true,
120 };
121 let exit = cfg.render_error(&CliError::Input("bad ref".into()));
122 assert_eq!(exit, 2);
123 }
124
125 #[test]
126 fn render_error_returns_auth_exit_for_auth_error() {
127 let cfg = OutputConfig {
128 json: true,
129 quiet: true,
130 };
131 let exit = cfg.render_error(&CliError::Auth("expired".into()));
132 assert_eq!(exit, 3);
133 }
134
135 #[test]
136 fn use_color_respects_no_color_env() {
137 unsafe { std::env::set_var("NO_COLOR", "1") };
142 assert!(!use_color());
143 unsafe { std::env::remove_var("NO_COLOR") };
144 }
145}