1use std::io::{self, 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::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]
93 pub fn exit_code(&self) -> ExitCode {
94 ExitCode::from(self.exit_code)
95 }
96}
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub struct View {
101 pub format: OutputFormat,
103 pub color: ColorMode,
105 pub quiet: bool,
107 width: Option<u16>,
108}
109
110impl View {
111 #[must_use]
113 pub const fn new(format: OutputFormat, color: ColorMode) -> Self {
114 Self {
115 format,
116 color,
117 quiet: false,
118 width: None,
119 }
120 }
121
122 #[must_use]
124 pub const fn quiet(mut self, quiet: bool) -> Self {
125 self.quiet = quiet;
126 self
127 }
128
129 #[must_use]
131 pub const fn width(mut self, width: u16) -> Self {
132 self.width = Some(width);
133 self
134 }
135
136 pub fn capture(self, value: &impl Present) -> io::Result<Captured> {
138 let kind = value.message_kind();
139 let exit_code = value.exit_code();
140 if self.format.is_json() {
141 let mut content = serde_json::to_string(value)?;
142 content.push('\n');
143 return Ok(Captured {
144 stream: Stream::Stdout,
145 content,
146 exit_code,
147 });
148 }
149 if self.quiet && kind == MessageKind::Success {
150 return Ok(Captured {
151 stream: Stream::None,
152 content: String::new(),
153 exit_code,
154 });
155 }
156 let options = self.width.map_or_else(
157 || RenderOptions::new(self.color),
158 |width| RenderOptions::new(self.color).width(width),
159 );
160 let content = value.present().render(options);
161 Ok(Captured {
162 stream: match kind {
163 MessageKind::Success => Stream::Stdout,
164 MessageKind::Error => Stream::Stderr,
165 },
166 content,
167 exit_code,
168 })
169 }
170
171 pub fn show(self, value: &impl Present) -> io::Result<ExitCode> {
173 let captured = self.capture(value)?;
174 match captured.stream() {
175 Stream::None => {}
176 Stream::Stdout => write_stdout(captured.bytes(), self.color)?,
177 Stream::Stderr => write_stderr(captured.bytes(), self.color)?,
178 }
179 Ok(captured.exit_code())
180 }
181
182 pub fn emit_err(self, bin: &str, message: &str) -> io::Result<ExitCode> {
184 if self.format.is_json() {
185 emit_json(&Envelope::<()>::err(ErrorBody::new(bin, message)))?;
186 return Ok(ExitCode::FAILURE);
187 }
188 let options = self.width.map_or_else(
189 || RenderOptions::new(self.color),
190 |width| RenderOptions::new(self.color).width(width),
191 );
192 let document =
193 Document::new().paragraph(Text::new().error(bin).then(": ").then(message.to_owned()));
194 write_stderr(document.render(options).as_bytes(), self.color)?;
195 Ok(ExitCode::FAILURE)
196 }
197}
198
199pub(crate) fn emit_json<T: Serialize>(value: &T) -> io::Result<()> {
201 let stdout = io::stdout();
202 let mut lock = stdout.lock();
203 serde_json::to_writer(&mut lock, value)?;
204 lock.write_all(b"\n")?;
205 lock.flush()
206}
207
208pub(crate) fn write_stdout(bytes: &[u8], color: ColorMode) -> io::Result<()> {
210 let mut stream = AutoStream::new(io::stdout().lock(), color.choice());
211 stream.write_all(bytes)?;
212 stream.flush()
213}
214
215pub(crate) fn write_stderr(bytes: &[u8], color: ColorMode) -> io::Result<()> {
217 let mut stream = AutoStream::new(io::stderr().lock(), color.choice());
218 stream.write_all(bytes)?;
219 stream.flush()
220}