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