use std::io::{self, Write};
use std::process::ExitCode;
use anstream::AutoStream;
use serde::Serialize;
use crate::color::ColorMode;
use crate::document::{Document, Text};
use crate::format::OutputFormat;
use crate::model::{Envelope, ErrorBody};
use crate::render::RenderOptions;
pub trait Present: Serialize {
fn present(&self) -> Document;
fn message_kind(&self) -> MessageKind {
MessageKind::Success
}
fn exit_code(&self) -> u8 {
self.message_kind().default_exit_code()
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum MessageKind {
#[default]
Success,
Error,
}
impl MessageKind {
#[must_use]
pub const fn default_exit_code(self) -> u8 {
match self {
Self::Success => 0,
Self::Error => 1,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Stream {
None,
Stdout,
Stderr,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Captured {
stream: Stream,
content: String,
exit_code: u8,
}
impl Captured {
#[must_use]
pub const fn stream(&self) -> Stream {
self.stream
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.content.as_bytes()
}
#[must_use]
pub fn text(&self) -> &str {
&self.content
}
#[must_use]
pub fn exit_code(&self) -> ExitCode {
ExitCode::from(self.exit_code)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct View {
pub format: OutputFormat,
pub color: ColorMode,
pub quiet: bool,
width: Option<u16>,
}
impl View {
#[must_use]
pub const fn new(format: OutputFormat, color: ColorMode) -> Self {
Self {
format,
color,
quiet: false,
width: None,
}
}
#[must_use]
pub const fn quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
#[must_use]
pub const fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
pub fn capture(self, value: &impl Present) -> io::Result<Captured> {
let kind = value.message_kind();
let exit_code = value.exit_code();
if self.format.is_json() {
let mut content = serde_json::to_string(value)?;
content.push('\n');
return Ok(Captured {
stream: Stream::Stdout,
content,
exit_code,
});
}
if self.quiet && kind == MessageKind::Success {
return Ok(Captured {
stream: Stream::None,
content: String::new(),
exit_code,
});
}
let options = self.width.map_or_else(
|| RenderOptions::new(self.color),
|width| RenderOptions::new(self.color).width(width),
);
let content = value.present().render(options);
Ok(Captured {
stream: match kind {
MessageKind::Success => Stream::Stdout,
MessageKind::Error => Stream::Stderr,
},
content,
exit_code,
})
}
pub fn show(self, value: &impl Present) -> io::Result<ExitCode> {
let captured = self.capture(value)?;
match captured.stream() {
Stream::None => {}
Stream::Stdout => write_stdout(captured.bytes(), self.color)?,
Stream::Stderr => write_stderr(captured.bytes(), self.color)?,
}
Ok(captured.exit_code())
}
pub fn emit_err(self, bin: &str, message: &str) -> io::Result<ExitCode> {
if self.format.is_json() {
emit_json(&Envelope::<()>::err(ErrorBody::new(bin, message)))?;
return Ok(ExitCode::FAILURE);
}
let options = self.width.map_or_else(
|| RenderOptions::new(self.color),
|width| RenderOptions::new(self.color).width(width),
);
let document =
Document::new().paragraph(Text::new().error(bin).then(": ").then(message.to_owned()));
write_stderr(document.render(options).as_bytes(), self.color)?;
Ok(ExitCode::FAILURE)
}
}
pub(crate) fn emit_json<T: Serialize>(value: &T) -> io::Result<()> {
let stdout = io::stdout();
let mut lock = stdout.lock();
serde_json::to_writer(&mut lock, value)?;
lock.write_all(b"\n")?;
lock.flush()
}
pub(crate) fn write_stdout(bytes: &[u8], color: ColorMode) -> io::Result<()> {
let mut stream = AutoStream::new(io::stdout().lock(), color.choice());
stream.write_all(bytes)?;
stream.flush()
}
pub(crate) fn write_stderr(bytes: &[u8], color: ColorMode) -> io::Result<()> {
let mut stream = AutoStream::new(io::stderr().lock(), color.choice());
stream.write_all(bytes)?;
stream.flush()
}