use std::io::{self, Write};
use std::process::ExitCode;
use sparcli::{
Alert, Color as UiColor, Renderable, Style as SpStyle, Theme, set_theme,
};
use crate::config::Config;
use crate::domain::error::Error;
use crate::theme::{Color, GlyphVariant};
use crate::util::app_info::APP_NAME;
pub const EXIT_OK: u8 = 0;
pub const EXIT_ERROR: u8 = 1;
pub const EXIT_USAGE: u8 = 2;
pub const EXIT_INTERRUPTED: u8 = 130;
pub struct Streams<'a> {
pub out: &'a mut dyn Write,
pub err: &'a mut dyn Write,
}
impl Streams<'_> {
pub fn line(&mut self, text: &str) -> io::Result<()> {
writeln!(self.out, "{text}")
}
pub fn message(&mut self, widget: &impl Renderable) -> CliResult {
widget
.print_to(&mut self.err)
.map_err(|error| CliError::runtime(error.to_string()))
}
pub fn payload(&mut self, widget: &impl Renderable) -> CliResult {
widget
.print_to(&mut self.out)
.map_err(|error| CliError::runtime(error.to_string()))
}
}
#[derive(Debug)]
pub struct CliError {
message: String,
code: u8,
}
impl CliError {
pub fn runtime(message: impl Into<String>) -> Self {
CliError {
message: message.into(),
code: EXIT_ERROR,
}
}
pub fn usage(message: impl Into<String>) -> Self {
CliError {
message: message.into(),
code: EXIT_USAGE,
}
}
pub fn code(&self) -> u8 {
self.code
}
}
impl From<Error> for CliError {
fn from(error: Error) -> Self {
CliError::runtime(error.to_string())
}
}
impl From<io::Error> for CliError {
fn from(error: io::Error) -> Self {
CliError::runtime(error.to_string())
}
}
impl std::fmt::Display for CliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
pub type CliResult = Result<(), CliError>;
pub fn report(error: &CliError, err: &mut dyn Write) -> ExitCode {
let _ = writeln!(err, "{APP_NAME}: error: {error}");
ExitCode::from(error.code)
}
pub fn apply_sparcli_theme(config: &Config) {
let palette = config.palette();
let mut theme = Theme {
accent: map_color(palette.accent),
unicode: matches!(config.appearance.glyphs, GlyphVariant::Unicode),
..Theme::default()
};
theme.success = SpStyle::new().fg(map_color(palette.success));
theme.error = SpStyle::new().fg(map_color(palette.error));
theme.warning = SpStyle::new().fg(map_color(palette.warning));
theme.info = SpStyle::new().fg(map_color(palette.info));
theme.secondary = SpStyle::new().fg(map_color(palette.foreground_dim));
set_theme(theme);
}
fn map_color(color: Color) -> UiColor {
match color.rgb() {
Some((red, green, blue)) => UiColor::Rgb(red, green, blue),
None => UiColor::Reset,
}
}
pub fn info(streams: &mut Streams, message: impl Into<String>) {
let _ = streams.message(&Alert::info(message.into()));
}
pub fn success(streams: &mut Streams, message: impl Into<String>) {
let _ = streams.message(&Alert::success(message.into()));
}
pub fn warning(streams: &mut Streams, message: impl Into<String>) {
let _ = streams.message(&Alert::warning(message.into()));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_error_is_reported_as_a_greppable_prefixed_line() {
let mut err: Vec<u8> = Vec::new();
let code = report(&CliError::runtime("no such slug 'zzz'"), &mut err);
assert_eq!(
String::from_utf8(err).unwrap(),
"hop: error: no such slug 'zzz'\n"
);
assert_eq!(code, ExitCode::from(EXIT_ERROR));
}
#[test]
fn a_usage_error_carries_exit_code_two() {
assert_eq!(CliError::usage("needs a terminal").code(), EXIT_USAGE);
assert_eq!(CliError::runtime("boom").code(), EXIT_ERROR);
}
#[test]
fn payload_and_messages_land_on_separate_streams() {
let mut out: Vec<u8> = Vec::new();
let mut err: Vec<u8> = Vec::new();
let mut streams = Streams {
out: &mut out,
err: &mut err,
};
streams.line("payload").unwrap();
info(&mut streams, "a hint");
assert_eq!(String::from_utf8(out).unwrap(), "payload\n");
assert!(String::from_utf8(err).unwrap().contains("a hint"));
}
}