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, and
6//! [`PrintTable`] renders a value as a table.
7//!
8//! Every value a command prints is data: a named type whose `Display`
9//! writes the text form and whose `Serialize` writes the JSON one. There
10//! is no message wrapper, a sentence being the text form of data, never
11//! data itself.
12
13use std::{
14    fmt,
15    io::{IsTerminal, Stdout, Write, stdout},
16};
17
18use anyhow::{Context, Result};
19use serde::Serialize;
20
21use crate::{clap::args::JsonFlag, output::Bytes};
22
23/// A value that can render itself as a table.
24pub trait PrintTable {
25    /// Writes the table to the given writer, capped at an optional
26    /// maximum width.
27    fn print(&self, writer: &mut dyn Write, table_max_width: Option<u16>) -> Result<()>;
28}
29
30/// Sink a command writes its output to.
31pub trait Printer {
32    /// Writes the result of the command, as text or JSON.
33    fn out<T: fmt::Display + Serialize>(&mut self, data: T) -> Result<()>;
34
35    /// Writes an event, something the command observed while running,
36    /// before its result.
37    ///
38    /// An event serializes as an object whose root carries `event`, a
39    /// kebab-case name, which a result never carries.
40    fn event<T: fmt::Display + Serialize>(&mut self, event: T) -> Result<()>;
41
42    /// Writes bytes as the result of the command: verbatim in text mode,
43    /// as a [`Bytes`] object under JSON.
44    fn out_bytes(&mut self, bytes: Bytes) -> Result<()>;
45
46    /// Whether the printer emits JSON.
47    fn is_json(&self) -> bool {
48        false
49    }
50}
51
52/// A [`Printer`] writing plain text or JSON to stdout.
53pub struct StdoutPrinter {
54    stdout: Stdout,
55    json: bool,
56}
57
58impl StdoutPrinter {
59    /// Builds a stdout printer, emitting JSON when the flag is set.
60    pub fn new(json: &JsonFlag) -> Self {
61        Self {
62            stdout: stdout(),
63            json: json.enabled,
64        }
65    }
66
67    fn write_json_line<T: Serialize>(&mut self, data: &T) -> Result<()> {
68        serde_json::to_writer(&mut self.stdout, data).context("Print JSON to stdout error")?;
69        writeln!(self.stdout)?;
70        self.stdout.flush()?;
71        Ok(())
72    }
73}
74
75impl Printer for StdoutPrinter {
76    fn out<T: fmt::Display + Serialize>(&mut self, data: T) -> Result<()> {
77        if !self.json {
78            write!(self.stdout, "{data}")?;
79            return Ok(());
80        }
81
82        if self.stdout.is_terminal() {
83            serde_json::to_writer_pretty(&mut self.stdout, &data)
84                .context("Print pretty JSON to stdout error")?;
85            writeln!(self.stdout)?;
86            return Ok(());
87        }
88
89        // NOTE: one value per line, flushed, so a process printing
90        // several values (a REPL session, a watch) emits
91        // newline-delimited JSON.
92        self.write_json_line(&data)
93    }
94
95    fn event<T: fmt::Display + Serialize>(&mut self, event: T) -> Result<()> {
96        // NOTE: an event is one compact line even on a terminal, so a
97        // stream of them stays one value per line.
98        if self.json {
99            return self.write_json_line(&event);
100        }
101
102        // NOTE: text events go to a terminal only, so a redirected stdout
103        // captures the result alone.
104        if self.stdout.is_terminal() {
105            write!(self.stdout, "{event}")?;
106            self.stdout.flush()?;
107        }
108
109        Ok(())
110    }
111
112    fn out_bytes(&mut self, bytes: Bytes) -> Result<()> {
113        if self.json {
114            return self.out(bytes);
115        }
116
117        self.stdout
118            .write_all(&bytes.data)
119            .context("Write bytes to stdout error")?;
120        self.stdout.flush().context("Flush stdout error")?;
121
122        Ok(())
123    }
124
125    fn is_json(&self) -> bool {
126        self.json
127    }
128}
129
130/// A [`Printer`] keeping the rendered output in memory, for tests.
131#[cfg(test)]
132#[derive(Default)]
133pub struct TestPrinter {
134    /// The text form of the result.
135    pub output: String,
136    /// The text form of each event, in order.
137    pub events: Vec<String>,
138}
139
140#[cfg(test)]
141impl Printer for TestPrinter {
142    fn out<T: fmt::Display + Serialize>(&mut self, data: T) -> Result<()> {
143        self.output = data.to_string();
144        Ok(())
145    }
146
147    fn event<T: fmt::Display + Serialize>(&mut self, event: T) -> Result<()> {
148        self.events.push(event.to_string());
149        Ok(())
150    }
151
152    fn out_bytes(&mut self, bytes: Bytes) -> Result<()> {
153        self.output = String::from_utf8_lossy(&bytes.data).into_owned();
154        Ok(())
155    }
156}