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 pub fn capture(self, value: &impl Present) -> io::Result<Captured> {
254 let kind = value.message_kind();
255 let exit_code = value.exit_code();
256 if self.format.is_json() {
257 let mut content = if self.json_layout.pretty() {
258 serde_json::to_string_pretty(value)?
259 } else {
260 serde_json::to_string(value)?
261 };
262 content.push('\n');
263 return Ok(Captured {
264 stream: Stream::Stdout,
265 content,
266 exit_code,
267 });
268 }
269 if self.quiet && kind == MessageKind::Success {
270 return Ok(Captured {
271 stream: Stream::None,
272 content: String::new(),
273 exit_code,
274 });
275 }
276 let content = value.present().render(self.render_options());
277 Ok(Captured {
278 stream: match kind {
279 MessageKind::Success => Stream::Stdout,
280 MessageKind::Error => Stream::Stderr,
281 },
282 content,
283 exit_code,
284 })
285 }
286
287 pub fn show(self, value: &impl Present) -> io::Result<ExitCode> {
289 let captured = self.capture(value)?;
290 match captured.stream() {
291 Stream::None => {}
292 Stream::Stdout => write_stdout(captured.bytes(), self.color)?,
293 Stream::Stderr => write_stderr(captured.bytes(), self.color)?,
294 }
295 Ok(captured.exit_code())
296 }
297
298 pub fn emit_err(self, bin: &str, message: &str) -> io::Result<ExitCode> {
300 if self.format.is_json() {
301 emit_json(&Envelope::<()>::err(ErrorBody::new(bin, message)))?;
302 return Ok(ExitCode::FAILURE);
303 }
304 let document =
305 Document::new().paragraph(Text::new().error(bin).then(": ").then(message.to_owned()));
306 write_stderr(
307 document.render(self.render_options()).as_bytes(),
308 self.color,
309 )?;
310 Ok(ExitCode::FAILURE)
311 }
312}
313
314pub(crate) fn emit_json<T: Serialize>(value: &T) -> io::Result<()> {
316 let stdout = io::stdout();
317 let mut lock = stdout.lock();
318 serde_json::to_writer(&mut lock, value)?;
319 lock.write_all(b"\n")?;
320 lock.flush()
321}
322
323pub(crate) fn write_stdout(bytes: &[u8], color: ColorMode) -> io::Result<()> {
325 let mut stream = AutoStream::new(io::stdout().lock(), color.choice());
326 stream.write_all(bytes)?;
327 stream.flush()
328}
329
330pub(crate) fn write_stderr(bytes: &[u8], color: ColorMode) -> io::Result<()> {
332 let mut stream = AutoStream::new(io::stderr().lock(), color.choice());
333 stream.write_all(bytes)?;
334 stream.flush()
335}