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_json(&self, value: &serde_json::Value) {
47 println!(
48 "{}",
49 serde_json::to_string_pretty(value).expect("serialize JSON")
50 );
51 }
52
53 pub fn render_error(&self, err: &CliError) -> i32 {
60 let exit = exit_code_for(err);
61 if self.json {
62 let code = match err {
63 CliError::Input(_) => "input",
64 CliError::Auth(_) => "auth",
65 CliError::ReadOnly(_) => "read_only",
66 CliError::NotFound(_) => "not_found",
67 CliError::Api { .. } => "api",
68 CliError::RateLimit => "rate_limit",
69 CliError::Http(_) => "http",
70 CliError::Other(_) => "other",
71 };
72 let value = json!({
73 "error": {
74 "code": code,
75 "message": err.to_string(),
76 "exit": exit,
77 }
78 });
79 self.print_json(&value);
80 } else {
81 eprintln!("error: {err}");
82 }
83 exit
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn json_forced_on_when_not_tty() {
93 let cfg = OutputConfig::new(false, false);
95 assert!(cfg.json);
96 }
97
98 #[test]
99 fn quiet_flag_propagates() {
100 let cfg = OutputConfig::new(false, true);
101 assert!(cfg.quiet);
102 }
103
104 #[test]
105 fn render_error_returns_input_exit_for_input_error() {
106 let cfg = OutputConfig {
107 json: true,
108 quiet: true,
109 };
110 let exit = cfg.render_error(&CliError::Input("bad ref".into()));
111 assert_eq!(exit, 2);
112 }
113
114 #[test]
115 fn render_error_returns_auth_exit_for_auth_error() {
116 let cfg = OutputConfig {
117 json: true,
118 quiet: true,
119 };
120 let exit = cfg.render_error(&CliError::Auth("expired".into()));
121 assert_eq!(exit, 3);
122 }
123
124 #[test]
125 fn use_color_respects_no_color_env() {
126 unsafe { std::env::set_var("NO_COLOR", "1") };
131 assert!(!use_color());
132 unsafe { std::env::remove_var("NO_COLOR") };
133 }
134}