1use colored::{Color, Colorize};
12use serde_json::json;
13use std::io::{self, Write};
14use std::sync::{Arc, Mutex};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum OutputMode {
19 Human,
21 Json,
23 Quiet,
25}
26
27#[derive(Clone)]
29pub struct Output {
30 mode: OutputMode,
31 writer: Arc<Mutex<Box<dyn Write + Send>>>,
32 is_tty: bool,
33}
34
35impl Output {
36 pub fn new(mode: OutputMode) -> Self {
38 let is_tty = atty::is(atty::Stream::Stdout);
39 Self {
40 mode,
41 writer: Arc::new(Mutex::new(Box::new(io::stdout()))),
42 is_tty,
43 }
44 }
45
46 pub fn with_writer(mode: OutputMode, writer: Box<dyn Write + Send>) -> Self {
48 Self {
49 mode,
50 writer: Arc::new(Mutex::new(writer)),
51 is_tty: false, }
53 }
54
55 pub fn step(&self, msg: &str) {
57 match self.mode {
58 OutputMode::Human => {
59 let prefix = if self.is_tty {
60 "→".cyan().to_string()
61 } else {
62 "→".to_string()
63 };
64 self.write_line(&format!("{} {}", prefix, msg));
65 }
66 OutputMode::Json => {
67 self.write_json("step", msg, None);
68 }
69 OutputMode::Quiet => {}
70 }
71 }
72
73 pub fn success(&self, msg: &str) {
75 match self.mode {
76 OutputMode::Human => {
77 let prefix = if self.is_tty {
78 "✓".green().to_string()
79 } else {
80 "✓".to_string()
81 };
82 self.write_line(&format!("{} {}", prefix, msg));
83 }
84 OutputMode::Json => {
85 self.write_json("success", msg, None);
86 }
87 OutputMode::Quiet => {}
88 }
89 }
90
91 pub fn warn(&self, msg: &str) {
93 match self.mode {
94 OutputMode::Human => {
95 let prefix = if self.is_tty {
96 "⚠".yellow().to_string()
97 } else {
98 "⚠".to_string()
99 };
100 self.write_line(&format!("{} {}", prefix, msg));
101 }
102 OutputMode::Json => {
103 self.write_json("warning", msg, None);
104 }
105 OutputMode::Quiet => {}
106 }
107 }
108
109 pub fn error(&self, msg: &str) {
111 match self.mode {
112 OutputMode::Human => {
113 let prefix = if self.is_tty {
114 "✗".red().to_string()
115 } else {
116 "✗".to_string()
117 };
118 self.write_line(&format!("{} {}", prefix, msg));
119 }
120 OutputMode::Json => {
121 self.write_json("error", msg, None);
122 }
123 OutputMode::Quiet => {
124 self.write_line(&format!("✗ {}", msg));
126 }
127 }
128 }
129
130 pub fn info(&self, msg: &str) {
132 match self.mode {
133 OutputMode::Human => {
134 self.write_line(msg);
135 }
136 OutputMode::Json => {
137 self.write_json("info", msg, None);
138 }
139 OutputMode::Quiet => {}
140 }
141 }
142
143 pub fn detail(&self, msg: &str) {
145 match self.mode {
146 OutputMode::Human => {
147 self.write_line(&format!(" {}", msg));
148 }
149 OutputMode::Json => {
150 self.write_json("detail", msg, None);
151 }
152 OutputMode::Quiet => {}
153 }
154 }
155
156 pub fn colored(&self, prefix: &str, msg: &str, color: Color) {
158 match self.mode {
159 OutputMode::Human => {
160 let formatted_prefix = if self.is_tty {
161 prefix.color(color).to_string()
162 } else {
163 prefix.to_string()
164 };
165 self.write_line(&format!("{} {}", formatted_prefix, msg));
166 }
167 OutputMode::Json => {
168 self.write_json("message", msg, Some(("prefix", prefix)));
169 }
170 OutputMode::Quiet => {}
171 }
172 }
173
174 pub fn json(&self, value: &serde_json::Value) {
176 if let Ok(mut writer) = self.writer.lock() {
177 let _ = writeln!(writer, "{}", value);
178 }
179 }
180
181 fn write_line(&self, line: &str) {
183 if let Ok(mut writer) = self.writer.lock() {
184 let _ = writeln!(writer, "{}", line);
185 }
186 }
187
188 fn write_json(&self, level: &str, msg: &str, extra: Option<(&str, &str)>) {
190 if let Ok(mut writer) = self.writer.lock() {
191 let mut obj = json!({
192 "level": level,
193 "msg": msg,
194 });
195
196 if let Some((key, value)) = extra {
197 obj[key] = json!(value);
198 }
199
200 let _ = writeln!(writer, "{}", obj);
201 }
202 }
203
204 pub fn mode(&self) -> OutputMode {
206 self.mode
207 }
208
209 pub fn is_tty(&self) -> bool {
211 self.is_tty
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use std::sync::{Arc, Mutex};
219
220 struct TestWriter {
222 buffer: Arc<Mutex<Vec<u8>>>,
223 }
224
225 impl TestWriter {
226 fn new() -> (Self, Arc<Mutex<Vec<u8>>>) {
227 let buffer = Arc::new(Mutex::new(Vec::new()));
228 (
229 Self {
230 buffer: buffer.clone(),
231 },
232 buffer,
233 )
234 }
235 }
236
237 impl Write for TestWriter {
238 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
239 self.buffer.lock().unwrap().write(buf)
240 }
241
242 fn flush(&mut self) -> io::Result<()> {
243 self.buffer.lock().unwrap().flush()
244 }
245 }
246
247 #[test]
248 fn test_human_mode_output() {
249 let (writer, buffer) = TestWriter::new();
250 let output = Output::with_writer(OutputMode::Human, Box::new(writer));
251
252 output.step("Starting");
253 output.success("Done");
254 output.warn("Warning");
255 output.error("Error");
256 output.info("Info");
257 output.detail("Detail");
258
259 let data = buffer.lock().unwrap();
260 let result = String::from_utf8(data.clone()).unwrap();
261 assert!(result.contains("→ Starting"));
262 assert!(result.contains("✓ Done"));
263 assert!(result.contains("⚠ Warning"));
264 assert!(result.contains("✗ Error"));
265 assert!(result.contains("Info"));
266 assert!(result.contains(" Detail"));
267 }
268
269 #[test]
270 fn test_json_mode_output() {
271 let (writer, buffer) = TestWriter::new();
272 let output = Output::with_writer(OutputMode::Json, Box::new(writer));
273
274 output.step("Starting");
275 output.success("Done");
276
277 let data = buffer.lock().unwrap();
278 let result = String::from_utf8(data.clone()).unwrap();
279 assert!(result.contains(r#""level":"step""#));
280 assert!(result.contains(r#""msg":"Starting""#));
281 assert!(result.contains(r#""level":"success""#));
282 assert!(result.contains(r#""msg":"Done""#));
283 }
284
285 #[test]
286 fn test_quiet_mode_only_errors() {
287 let (writer, buffer) = TestWriter::new();
288 let output = Output::with_writer(OutputMode::Quiet, Box::new(writer));
289
290 output.step("Starting");
291 output.success("Done");
292 output.warn("Warning");
293 output.error("Error");
294 output.info("Info");
295
296 let data = buffer.lock().unwrap();
297 let result = String::from_utf8(data.clone()).unwrap();
298 assert!(result.contains("✗ Error"));
300 assert!(!result.contains("Starting"));
301 assert!(!result.contains("Done"));
302 assert!(!result.contains("Warning"));
303 assert!(!result.contains("Info"));
304 }
305
306 #[test]
307 fn test_mode_getter() {
308 let output = Output::new(OutputMode::Json);
309 assert_eq!(output.mode(), OutputMode::Json);
310 }
311}