Skip to main content

pimalaya_cli/
printer.rs

1//! Command output rendering to stdout.
2//!
3//! The [`Printer`] trait abstracts how a command emits what it prints:
4//! events while it runs, then its result. The [`StdoutPrinter`]
5//! implementation writes plain text or JSON to stdout, the
6//! [`CapturePrinter`] keeps it as JSON values for a caller such as an MCP
7//! server.
8//!
9//! Every value a command prints is data: a named type whose `Display`
10//! writes the text form and whose `Serialize` writes the JSON one. There
11//! is no message wrapper, a sentence being the text form of data, never
12//! data itself. A record's text form is [`Fields`], a list's [`Columns`].
13//!
14//! [`Columns`]: crate::columns::Columns
15//! [`Fields`]: crate::fields::Fields
16
17use std::{
18    fmt,
19    io::{IsTerminal, Stdout, Write, stdout},
20};
21
22use anyhow::{Context, Result, bail};
23use serde::Serialize;
24use serde_json::Value;
25
26use crate::{clap::args::JsonFlag, output::Bytes};
27
28/// Sink a command writes its output to.
29pub trait Printer {
30    /// Writes the result of the command, as text or JSON.
31    fn out<T: fmt::Display + Serialize>(&mut self, data: T) -> Result<()>;
32
33    /// Writes an event, something the command observed while running,
34    /// before its result.
35    ///
36    /// An event serializes as an object whose root carries `event`, a
37    /// kebab-case name, which a result never carries.
38    fn event<T: fmt::Display + Serialize>(&mut self, event: T) -> Result<()>;
39
40    /// Writes bytes as the result of the command: verbatim in text mode,
41    /// as a [`Bytes`] object under JSON.
42    fn out_bytes(&mut self, bytes: Bytes) -> Result<()>;
43
44    /// Whether the printer emits JSON.
45    fn is_json(&self) -> bool {
46        false
47    }
48}
49
50/// A [`Printer`] writing plain text or JSON to stdout.
51pub struct StdoutPrinter {
52    stdout: Stdout,
53    json: bool,
54    streaming: bool,
55}
56
57impl StdoutPrinter {
58    /// Builds a stdout printer, emitting JSON when the flag is set.
59    pub fn new(json: &JsonFlag) -> Self {
60        Self {
61            stdout: stdout(),
62            json: json.enabled,
63            streaming: false,
64        }
65    }
66
67    /// Turns streaming on, for a long-lived command: text events are
68    /// written to stdout even when it is no terminal, so a status bar
69    /// piping the command reads each one as it comes.
70    pub fn streaming(mut self) -> Self {
71        self.streaming = true;
72        self
73    }
74
75    fn write_json_line<T: Serialize>(&mut self, data: &T) -> Result<()> {
76        serde_json::to_writer(&mut self.stdout, data).context("Print JSON to stdout error")?;
77        writeln!(self.stdout)?;
78        self.stdout.flush()?;
79        Ok(())
80    }
81}
82
83impl Printer for StdoutPrinter {
84    fn out<T: fmt::Display + Serialize>(&mut self, data: T) -> Result<()> {
85        if !self.json {
86            write!(self.stdout, "{data}")?;
87            return Ok(());
88        }
89
90        if self.stdout.is_terminal() {
91            serde_json::to_writer_pretty(&mut self.stdout, &data)
92                .context("Print pretty JSON to stdout error")?;
93            writeln!(self.stdout)?;
94            return Ok(());
95        }
96
97        // NOTE: one value per line, flushed, so a process printing
98        // several values (a REPL session, a watch) emits
99        // newline-delimited JSON.
100        self.write_json_line(&data)
101    }
102
103    fn event<T: fmt::Display + Serialize>(&mut self, event: T) -> Result<()> {
104        // NOTE: an event is one compact line even on a terminal, so a
105        // stream of them stays one value per line.
106        if self.json {
107            return self.write_json_line(&event);
108        }
109
110        // NOTE: text events go to a terminal only, so a redirected stdout
111        // captures the result alone, unless the command streams.
112        if self.streaming || self.stdout.is_terminal() {
113            write!(self.stdout, "{event}")?;
114            self.stdout.flush()?;
115        }
116
117        Ok(())
118    }
119
120    fn out_bytes(&mut self, bytes: Bytes) -> Result<()> {
121        if self.json {
122            return self.out(bytes);
123        }
124
125        self.stdout
126            .write_all(&bytes.data)
127            .context("Write bytes to stdout error")?;
128        self.stdout.flush().context("Flush stdout error")?;
129
130        Ok(())
131    }
132
133    fn is_json(&self) -> bool {
134        self.json
135    }
136}
137
138/// A [`Printer`] keeping each value and event a command prints as JSON,
139/// for a caller that hands them on rather than printing them.
140#[derive(Debug, Default)]
141pub struct CapturePrinter {
142    /// The values printed, in order.
143    pub values: Vec<Value>,
144    /// The events printed, in order.
145    pub events: Vec<Value>,
146}
147
148impl CapturePrinter {
149    /// The one value the command printed, failing when it printed none or
150    /// several.
151    pub fn into_single(mut self) -> Result<Value> {
152        if self.values.len() != 1 {
153            let n = self.values.len();
154            bail!("The command printed {n} values, one expected");
155        }
156
157        Ok(self.values.remove(0))
158    }
159}
160
161impl Printer for CapturePrinter {
162    fn out<T: fmt::Display + Serialize>(&mut self, data: T) -> Result<()> {
163        self.values.push(serde_json::to_value(&data)?);
164        Ok(())
165    }
166
167    fn event<T: fmt::Display + Serialize>(&mut self, event: T) -> Result<()> {
168        self.events.push(serde_json::to_value(&event)?);
169        Ok(())
170    }
171
172    fn out_bytes(&mut self, bytes: Bytes) -> Result<()> {
173        self.out(bytes)
174    }
175
176    fn is_json(&self) -> bool {
177        true
178    }
179}
180
181/// A [`Printer`] keeping the rendered output in memory, for tests.
182#[cfg(test)]
183#[derive(Default)]
184pub struct TestPrinter {
185    /// The text form of the result.
186    pub output: String,
187    /// The text form of each event, in order.
188    pub events: Vec<String>,
189}
190
191#[cfg(test)]
192impl Printer for TestPrinter {
193    fn out<T: fmt::Display + Serialize>(&mut self, data: T) -> Result<()> {
194        self.output = data.to_string();
195        Ok(())
196    }
197
198    fn event<T: fmt::Display + Serialize>(&mut self, event: T) -> Result<()> {
199        self.events.push(event.to_string());
200        Ok(())
201    }
202
203    fn out_bytes(&mut self, bytes: Bytes) -> Result<()> {
204        self.output = String::from_utf8_lossy(&bytes.data).into_owned();
205        Ok(())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use serde_json::json;
212
213    use crate::printer::{CapturePrinter, Printer};
214
215    #[test]
216    fn a_single_value_is_taken_and_events_kept_apart() {
217        let mut printer = CapturePrinter::default();
218        printer.event(json!({ "event": "a" })).unwrap();
219        printer.out(json!({ "id": 1 })).unwrap();
220
221        assert_eq!(printer.events, [json!({ "event": "a" })]);
222        assert_eq!(printer.into_single().unwrap(), json!({ "id": 1 }));
223    }
224
225    #[test]
226    fn the_count_of_values_is_the_one_printed() {
227        let none = CapturePrinter::default().into_single().unwrap_err();
228        assert_eq!(
229            none.to_string(),
230            "The command printed 0 values, one expected"
231        );
232
233        let mut printer = CapturePrinter::default();
234        printer.out(json!(1)).unwrap();
235        printer.out(json!(2)).unwrap();
236
237        let two = printer.into_single().unwrap_err();
238        assert_eq!(
239            two.to_string(),
240            "The command printed 2 values, one expected"
241        );
242    }
243}