Skip to main content

ctl_core/
view.rs

1//! Multi-view emit: one serializable model, pretty or JSON, color or not.
2
3use std::io::{self, Write};
4
5use anstream::AutoStream;
6use serde::Serialize;
7
8use crate::color::ColorMode;
9use crate::format::OutputFormat;
10use crate::formatdoc;
11use crate::model::{Envelope, ErrorBody};
12
13/// Pretty text for a model. Prefer [`Pretty`] + a Jinja template.
14pub trait Render {
15    /// Human view. Must not depend on [`View::format`].
16    fn render_pretty(&self) -> String;
17}
18
19/// A serializable model whose pretty view is a Jinja template.
20///
21/// Prepare the data. The template owns `{% if %}`, loops, and alignment.
22pub trait Pretty: Serialize {
23    /// `include_str!` of a `.jinja` file next to the binary crate.
24    const TEMPLATE: &'static str;
25}
26
27/// Render `value` through a Jinja template. Used by [`View::show_pretty`].
28pub fn render_template(source: &str, value: &impl Serialize) -> Result<String, minijinja::Error> {
29    let mut env = minijinja::Environment::new();
30    env.add_template("pretty", source)?;
31    env.get_template("pretty")?.render(value)
32}
33
34/// How to present a model. JSON never contains ANSI.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub struct View {
37    /// Pretty or JSON.
38    pub format: OutputFormat,
39    /// ANSI policy for pretty output.
40    pub color: ColorMode,
41    /// Suppress successful pretty output.
42    pub quiet: bool,
43}
44
45impl View {
46    #[must_use]
47    /// Build a view that prints successes.
48    pub fn new(format: OutputFormat, color: ColorMode) -> Self {
49        Self {
50            format,
51            color,
52            quiet: false,
53        }
54    }
55
56    #[must_use]
57    /// Suppress successful pretty output when `quiet` is set.
58    pub fn quiet(mut self, quiet: bool) -> Self {
59        self.quiet = quiet;
60        self
61    }
62
63    /// JSON writes the model. Pretty writes [`Render::render_pretty`].
64    pub fn show(self, value: &(impl Serialize + Render)) -> io::Result<()> {
65        self.emit(value, &value.render_pretty())
66    }
67
68    /// JSON writes the model. Pretty renders [`Pretty::TEMPLATE`] against it.
69    pub fn show_pretty<T: Pretty>(self, value: &T) -> io::Result<()> {
70        self.show_template(value, T::TEMPLATE)
71    }
72
73    /// JSON writes the model. Pretty renders `template` against it.
74    pub fn show_template(self, value: &impl Serialize, template: &str) -> io::Result<()> {
75        if self.quiet {
76            return Ok(());
77        }
78        if self.format.is_json() {
79            return emit_json(value);
80        }
81        let pretty = render_template(template, value).map_err(io::Error::other)?;
82        write_stdout(pretty.as_bytes(), self.color)
83    }
84
85    /// JSON writes `value`. Pretty writes `pretty` (already styled or plain).
86    pub fn emit(self, value: &impl Serialize, pretty: &str) -> io::Result<()> {
87        if self.quiet {
88            return Ok(());
89        }
90        if self.format.is_json() {
91            return emit_json(value);
92        }
93        write_stdout(pretty.as_bytes(), self.color)
94    }
95
96    /// Emit a success [`Envelope`].
97    pub fn emit_ok<T: Serialize>(self, data: &T, pretty: &str) -> io::Result<()> {
98        self.emit(&Envelope::ok(data), pretty)
99    }
100
101    /// Emit `{bin}: {message}` or a JSON error envelope.
102    pub fn emit_err(self, bin: &str, message: &str) -> io::Result<()> {
103        let error = ErrorBody::new(bin, message);
104        if self.format.is_json() {
105            return emit_json(&Envelope::<()>::err(error));
106        }
107        write_stderr(formatdoc!("{bin}: {message}\n").as_bytes(), self.color)
108    }
109}
110
111/// Write `value` as one JSON line to stdout. No ANSI.
112pub fn emit_json<T: Serialize>(value: &T) -> io::Result<()> {
113    let stdout = io::stdout();
114    let mut lock = stdout.lock();
115    serde_json::to_writer(&mut lock, value)?;
116    lock.write_all(b"\n")?;
117    lock.flush()
118}
119
120/// Write raw bytes to stdout with `color`.
121pub fn write_stdout(bytes: &[u8], color: ColorMode) -> io::Result<()> {
122    let mut stream = AutoStream::new(io::stdout().lock(), color.choice());
123    stream.write_all(bytes)?;
124    stream.flush()
125}
126
127/// Write raw bytes to stderr with `color`.
128pub fn write_stderr(bytes: &[u8], color: ColorMode) -> io::Result<()> {
129    let mut stream = AutoStream::new(io::stderr().lock(), color.choice());
130    stream.write_all(bytes)?;
131    stream.flush()
132}