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::{DEFAULT_COLUMN_BUFFER_ENVS, DEFAULT_MINIMUM_AUTOMATIC_WIDTH, RenderOptions};
14
15pub trait Present: Serialize {
17 fn present(&self) -> Document;
19
20 fn message_kind(&self) -> MessageKind {
22 MessageKind::Success
23 }
24
25 fn exit_code(&self) -> u8 {
28 self.message_kind().default_exit_code()
29 }
30}
31
32#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
34pub enum MessageKind {
35 #[default]
37 Success,
38 Error,
40}
41
42impl MessageKind {
43 #[must_use]
45 pub const fn default_exit_code(self) -> u8 {
46 match self {
47 Self::Success => 0,
48 Self::Error => 1,
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum Stream {
56 None,
58 Stdout,
60 Stderr,
62}
63
64#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct Captured {
67 stream: Stream,
68 content: String,
69 exit_code: u8,
70}
71
72impl Captured {
73 #[must_use]
75 pub const fn stream(&self) -> Stream {
76 self.stream
77 }
78
79 #[must_use]
81 pub fn bytes(&self) -> &[u8] {
82 self.content.as_bytes()
83 }
84
85 #[must_use]
87 pub fn text(&self) -> &str {
88 &self.content
89 }
90
91 #[must_use = "return it from main, or the command exits 0"]
93 pub fn exit_code(&self) -> ExitCode {
94 ExitCode::from(self.exit_code)
95 }
96}
97
98#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
100pub enum JsonLayout {
101 #[default]
103 Compact,
104 Pretty,
106 PrettyOnTerminal,
108}
109
110impl JsonLayout {
111 fn pretty(self) -> bool {
112 match self {
113 Self::Compact => false,
114 Self::Pretty => true,
115 Self::PrettyOnTerminal => io::stdout().is_terminal(),
116 }
117 }
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
122pub struct View {
123 pub format: OutputFormat,
125 pub color: ColorMode,
127 pub quiet: bool,
129 width: Option<u16>,
130 automatic_width_buffer: Option<u16>,
131 automatic_width_buffer_envs: &'static [&'static str],
132 minimum_automatic_width: u16,
133 json_layout: JsonLayout,
134 styles: RenderOptions,
135}
136
137impl View {
138 #[must_use]
140 pub const fn new(format: OutputFormat, color: ColorMode) -> Self {
141 Self {
142 format,
143 color,
144 quiet: false,
145 width: None,
146 automatic_width_buffer: None,
147 automatic_width_buffer_envs: DEFAULT_COLUMN_BUFFER_ENVS,
148 minimum_automatic_width: DEFAULT_MINIMUM_AUTOMATIC_WIDTH,
149 json_layout: JsonLayout::Compact,
150 styles: RenderOptions::new(color),
151 }
152 }
153
154 #[must_use]
156 pub const fn json_layout(mut self, layout: JsonLayout) -> Self {
157 self.json_layout = layout;
158 self
159 }
160
161 #[must_use]
164 pub const fn styles(mut self, styles: RenderOptions) -> Self {
165 self.styles = styles;
166 self
167 }
168
169 #[must_use]
171 pub const fn quiet(mut self, quiet: bool) -> Self {
172 self.quiet = quiet;
173 self
174 }
175
176 #[must_use]
178 pub const fn width(mut self, width: u16) -> Self {
179 self.width = Some(width);
180 self
181 }
182
183 #[must_use]
186 pub const fn automatic_width_buffer(mut self, columns: u16) -> Self {
187 self.automatic_width_buffer = Some(columns);
188 self
189 }
190
191 #[must_use]
193 pub const fn automatic_width_buffer_envs(mut self, names: &'static [&'static str]) -> Self {
194 self.automatic_width_buffer_envs = names;
195 self
196 }
197
198 #[must_use]
200 pub const fn minimum_automatic_width(mut self, columns: u16) -> Self {
201 self.minimum_automatic_width = columns;
202 self
203 }
204
205 #[must_use]
207 pub const fn explicit_automatic_width_buffer(self) -> Option<u16> {
208 self.automatic_width_buffer
209 }
210
211 #[must_use]
213 pub const fn automatic_width_buffer_env_names(self) -> &'static [&'static str] {
214 self.automatic_width_buffer_envs
215 }
216
217 #[must_use]
219 pub const fn automatic_width_minimum(self) -> u16 {
220 self.minimum_automatic_width
221 }
222
223 pub(crate) fn render_options(self) -> RenderOptions {
224 let mut options = RenderOptions::new(self.color)
225 .automatic_width_buffer_envs(self.automatic_width_buffer_envs)
226 .minimum_automatic_width(self.minimum_automatic_width)
227 .record_style(self.styles.record())
228 .list_style(self.styles.list())
229 .row_separation(self.styles.separation());
230 if let Some(width) = self.width {
231 options = options.width(width);
232 }
233 if let Some(buffer) = self.automatic_width_buffer {
234 options = options.automatic_width_buffer(buffer);
235 }
236 options
237 }
238
239 pub fn capture(self, value: &impl Present) -> io::Result<Captured> {
241 let kind = value.message_kind();
242 let exit_code = value.exit_code();
243 if self.format.is_json() {
244 let mut content = if self.json_layout.pretty() {
245 serde_json::to_string_pretty(value)?
246 } else {
247 serde_json::to_string(value)?
248 };
249 content.push('\n');
250 return Ok(Captured {
251 stream: Stream::Stdout,
252 content,
253 exit_code,
254 });
255 }
256 if self.quiet && kind == MessageKind::Success {
257 return Ok(Captured {
258 stream: Stream::None,
259 content: String::new(),
260 exit_code,
261 });
262 }
263 let content = value.present().render(self.render_options());
264 Ok(Captured {
265 stream: match kind {
266 MessageKind::Success => Stream::Stdout,
267 MessageKind::Error => Stream::Stderr,
268 },
269 content,
270 exit_code,
271 })
272 }
273
274 pub fn show(self, value: &impl Present) -> io::Result<ExitCode> {
276 let captured = self.capture(value)?;
277 match captured.stream() {
278 Stream::None => {}
279 Stream::Stdout => write_stdout(captured.bytes(), self.color)?,
280 Stream::Stderr => write_stderr(captured.bytes(), self.color)?,
281 }
282 Ok(captured.exit_code())
283 }
284
285 pub fn emit_err(self, bin: &str, message: &str) -> io::Result<ExitCode> {
287 if self.format.is_json() {
288 emit_json(&Envelope::<()>::err(ErrorBody::new(bin, message)))?;
289 return Ok(ExitCode::FAILURE);
290 }
291 let document =
292 Document::new().paragraph(Text::new().error(bin).then(": ").then(message.to_owned()));
293 write_stderr(
294 document.render(self.render_options()).as_bytes(),
295 self.color,
296 )?;
297 Ok(ExitCode::FAILURE)
298 }
299}
300
301pub(crate) fn emit_json<T: Serialize>(value: &T) -> io::Result<()> {
303 let stdout = io::stdout();
304 let mut lock = stdout.lock();
305 serde_json::to_writer(&mut lock, value)?;
306 lock.write_all(b"\n")?;
307 lock.flush()
308}
309
310pub(crate) fn write_stdout(bytes: &[u8], color: ColorMode) -> io::Result<()> {
312 let mut stream = AutoStream::new(io::stdout().lock(), color.choice());
313 stream.write_all(bytes)?;
314 stream.flush()
315}
316
317pub(crate) fn write_stderr(bytes: &[u8], color: ColorMode) -> io::Result<()> {
319 let mut stream = AutoStream::new(io::stderr().lock(), color.choice());
320 stream.write_all(bytes)?;
321 stream.flush()
322}