1use std::io::{self, IsTerminal, Write};
4use std::process::ExitCode;
5
6use anstream::AutoStream;
7use serde::Serialize;
8
9use crate::color::ColorMode;
10use crate::document::{Document, Text};
11use crate::format::OutputFormat;
12use crate::model::{Envelope, ErrorBody};
13use crate::render::{
14 DEFAULT_COLUMN_BUFFER_ENVS, DEFAULT_FALLBACK_WIDTH, DEFAULT_MINIMUM_AUTOMATIC_WIDTH,
15 RenderOptions,
16};
17
18pub trait Present: Serialize {
20 fn present(&self) -> Document;
22
23 fn message_kind(&self) -> MessageKind {
25 MessageKind::Success
26 }
27
28 fn exit_code(&self) -> u8 {
31 self.message_kind().default_exit_code()
32 }
33}
34
35#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
37pub enum MessageKind {
38 #[default]
40 Success,
41 Error,
43}
44
45impl MessageKind {
46 #[must_use]
48 pub const fn default_exit_code(self) -> u8 {
49 match self {
50 Self::Success => 0,
51 Self::Error => 1,
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum Stream {
59 None,
61 Stdout,
63 Stderr,
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct Captured {
70 stream: Stream,
71 content: String,
72 exit_code: u8,
73}
74
75impl Captured {
76 #[must_use]
78 pub const fn stream(&self) -> Stream {
79 self.stream
80 }
81
82 #[must_use]
84 pub fn bytes(&self) -> &[u8] {
85 self.content.as_bytes()
86 }
87
88 #[must_use]
90 pub fn text(&self) -> &str {
91 &self.content
92 }
93
94 #[must_use = "return it from main, or the command exits 0"]
96 pub fn exit_code(&self) -> ExitCode {
97 ExitCode::from(self.exit_code)
98 }
99}
100
101#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
103pub enum JsonLayout {
104 Compact,
106 #[default]
108 Pretty,
109 PrettyOnTerminal,
111}
112
113impl JsonLayout {
114 fn pretty(self) -> bool {
115 match self {
116 Self::Compact => false,
117 Self::Pretty => true,
118 Self::PrettyOnTerminal => io::stdout().is_terminal(),
119 }
120 }
121}
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125pub struct View {
126 pub format: OutputFormat,
128 pub color: ColorMode,
130 pub quiet: bool,
132 width: Option<u16>,
133 automatic_width_buffer: Option<u16>,
134 automatic_width_buffer_envs: &'static [&'static str],
135 minimum_automatic_width: u16,
136 fallback_width: Option<u16>,
137 json_layout: JsonLayout,
138 styles: RenderOptions,
139}
140
141impl View {
142 #[must_use]
144 pub const fn new(format: OutputFormat, color: ColorMode) -> Self {
145 Self {
146 format,
147 color,
148 quiet: false,
149 width: None,
150 automatic_width_buffer: None,
151 automatic_width_buffer_envs: DEFAULT_COLUMN_BUFFER_ENVS,
152 minimum_automatic_width: DEFAULT_MINIMUM_AUTOMATIC_WIDTH,
153 fallback_width: Some(DEFAULT_FALLBACK_WIDTH),
154 json_layout: JsonLayout::Pretty,
155 styles: RenderOptions::new(color),
156 }
157 }
158
159 #[must_use]
161 pub const fn json_layout(mut self, layout: JsonLayout) -> Self {
162 self.json_layout = layout;
163 self
164 }
165
166 #[must_use]
169 pub const fn styles(mut self, styles: RenderOptions) -> Self {
170 self.styles = styles;
171 self
172 }
173
174 #[must_use]
176 pub const fn quiet(mut self, quiet: bool) -> Self {
177 self.quiet = quiet;
178 self
179 }
180
181 #[must_use]
183 pub const fn width(mut self, width: u16) -> Self {
184 self.width = Some(width);
185 self
186 }
187
188 #[must_use]
191 pub const fn automatic_width_buffer(mut self, columns: u16) -> Self {
192 self.automatic_width_buffer = Some(columns);
193 self
194 }
195
196 #[must_use]
198 pub const fn automatic_width_buffer_envs(mut self, names: &'static [&'static str]) -> Self {
199 self.automatic_width_buffer_envs = names;
200 self
201 }
202
203 #[must_use]
205 pub const fn minimum_automatic_width(mut self, columns: u16) -> Self {
206 self.minimum_automatic_width = columns;
207 self
208 }
209
210 #[must_use]
212 pub const fn fallback_width(mut self, width: Option<u16>) -> Self {
213 self.fallback_width = width;
214 self
215 }
216
217 #[must_use]
219 pub const fn explicit_automatic_width_buffer(self) -> Option<u16> {
220 self.automatic_width_buffer
221 }
222
223 #[must_use]
225 pub const fn automatic_width_buffer_env_names(self) -> &'static [&'static str] {
226 self.automatic_width_buffer_envs
227 }
228
229 #[must_use]
231 pub const fn automatic_width_minimum(self) -> u16 {
232 self.minimum_automatic_width
233 }
234
235 pub(crate) fn render_options(self) -> RenderOptions {
236 let mut options = RenderOptions::new(self.color)
237 .automatic_width_buffer_envs(self.automatic_width_buffer_envs)
238 .minimum_automatic_width(self.minimum_automatic_width)
239 .fallback_width(self.fallback_width)
240 .record_style(self.styles.record())
241 .list_style(self.styles.list())
242 .row_separation(self.styles.separation());
243 if let Some(width) = self.width {
244 options = options.width(width);
245 }
246 if let Some(buffer) = self.automatic_width_buffer {
247 options = options.automatic_width_buffer(buffer);
248 }
249 options
250 }
251
252 fn json(self, value: &impl Serialize) -> io::Result<String> {
255 let mut content = if self.json_layout.pretty() {
256 serde_json::to_string_pretty(value)?
257 } else {
258 serde_json::to_string(value)?
259 };
260 content.push('\n');
261 Ok(content)
262 }
263
264 pub fn capture(self, value: &impl Present) -> io::Result<Captured> {
266 let kind = value.message_kind();
267 let exit_code = value.exit_code();
268 if self.format.is_json() {
269 return Ok(Captured {
270 stream: Stream::Stdout,
271 content: self.json(value)?,
272 exit_code,
273 });
274 }
275 if self.quiet && kind == MessageKind::Success {
276 return Ok(Captured {
277 stream: Stream::None,
278 content: String::new(),
279 exit_code,
280 });
281 }
282 let content = value.present().render(self.render_options());
283 Ok(Captured {
284 stream: match kind {
285 MessageKind::Success => Stream::Stdout,
286 MessageKind::Error => Stream::Stderr,
287 },
288 content,
289 exit_code,
290 })
291 }
292
293 pub fn show(self, value: &impl Present) -> io::Result<ExitCode> {
295 let captured = self.capture(value)?;
296 match captured.stream() {
297 Stream::None => {}
298 Stream::Stdout => write_stdout(captured.bytes(), self.color)?,
299 Stream::Stderr => write_stderr(captured.bytes(), self.color)?,
300 }
301 Ok(captured.exit_code())
302 }
303
304 pub fn emit_err(self, bin: &str, message: &str) -> io::Result<ExitCode> {
306 if self.format.is_json() {
307 let envelope = Envelope::<()>::err(ErrorBody::new(bin, message));
308 write_stdout(self.json(&envelope)?.as_bytes(), ColorMode::Never)?;
309 return Ok(ExitCode::FAILURE);
310 }
311 let document =
312 Document::new().paragraph(Text::new().error(bin).then(": ").then(message.to_owned()));
313 write_stderr(
314 document.render(self.render_options()).as_bytes(),
315 self.color,
316 )?;
317 Ok(ExitCode::FAILURE)
318 }
319}
320
321pub(crate) fn write_stdout(bytes: &[u8], color: ColorMode) -> io::Result<()> {
323 let mut stream = AutoStream::new(io::stdout().lock(), color.choice());
324 stream.write_all(bytes)?;
325 stream.flush()
326}
327
328pub(crate) fn write_stderr(bytes: &[u8], color: ColorMode) -> io::Result<()> {
330 let mut stream = AutoStream::new(io::stderr().lock(), color.choice());
331 stream.write_all(bytes)?;
332 stream.flush()
333}
334
335#[cfg(test)]
336mod tests {
337 use super::View;
338 use crate::color::ColorMode;
339 use crate::format::OutputFormat;
340 use crate::model::{Envelope, ErrorBody};
341 use crate::view::JsonLayout;
342
343 #[test]
344 fn error_envelopes_follow_the_json_layout() {
345 let envelope = Envelope::<()>::err(ErrorBody::new("toy", "failed"));
346 let view = View::new(OutputFormat::Json, ColorMode::Never);
347 assert!(view.json(&envelope).unwrap().contains("\n "));
348 let compact = view
349 .json_layout(JsonLayout::Compact)
350 .json(&envelope)
351 .unwrap();
352 assert_eq!(compact.lines().count(), 1, "{compact}");
353 }
354}