use crate::lines;
use std::fmt;
use std::io;
use std::io::Write;
use std::result;
use termion::color::{Color, Fg, Reset};
use thiserror::Error;
pub(crate) type Result = result::Result<(), Error>;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("{0}")]
BrokenPipe(io::Error),
#[error("{0}")]
Other(io::Error),
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
match err.kind() {
io::ErrorKind::BrokenPipe => Self::BrokenPipe(err),
_ => Self::Other(err),
}
}
}
pub trait Printer {
fn print<S: fmt::Display>(&self, msg: S) -> Result;
fn colored_print<S: fmt::Display, C: Color>(&self, color: Fg<C>, msg: S) -> Result {
let msg_string = msg.to_string();
let colored_msg: String = lines::line_split(&msg_string)
.map(|(component, joining_newline)| {
if component.is_empty() {
return joining_newline.unwrap_or_default().to_string();
}
format!(
"{color}{component}{reset}{joining_newline}",
color = color,
reset = Fg(Reset),
component = component,
joining_newline = joining_newline.unwrap_or_default()
)
})
.collect();
self.print(colored_msg)
}
}
pub struct StdoutPrinter;
impl StdoutPrinter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for StdoutPrinter {
fn default() -> Self {
Self {}
}
}
impl Printer for StdoutPrinter {
fn print<S: fmt::Display>(&self, msg: S) -> Result {
let mut stdout = io::stdout();
Ok(write!(stdout, "{}", msg)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil;
use crate::testutil::mock_print::BarebonesMockPrinter;
use termion::color::Magenta;
use test_case::test_case;
#[test_case(
io::Error::new(io::ErrorKind::BrokenPipe, "broken pipe"),
&|err| matches!(err, Error::BrokenPipe(_));
"BrokenPipe results in BrokenPipe variant"
)]
#[test_case(
io::Error::new(io::ErrorKind::Interrupted, "can't print, we're busy"),
&|err| matches!(err, Error::Other(_));
"non-BrokenPipe produces Other variant"
)]
fn test_error_from_io_err(from: io::Error, matches: &dyn Fn(&Error) -> bool) {
let produced_err = Error::from(from);
assert!(
matches(&produced_err),
"enum did not match: got {:?}",
&produced_err
);
}
#[test_case(
"hello world".to_string(),
format!("{0}hello world{1}", Fg(Magenta), Fg(Reset));
"no-newline case ends with reset"
)]
#[test_case(
"foo\nbar\n".to_string(),
format!("{0}foo{1}\n{0}bar{1}\n", Fg(Magenta), Fg(Reset));
"puts reset char before newlines"
)]
#[test_case(
"hello\n\n\nworld".to_string(),
format!("{0}hello{1}\n\n\n{0}world{1}", Fg(Magenta), Fg(Reset));
"empty strings don't need colorization"
)]
fn test_resets_colors_properly(message: String, expected: String) {
let printer = BarebonesMockPrinter::default();
let res = printer.colored_print(Fg(Magenta), message);
assert!(res.is_ok(), "{}", res.unwrap_err());
testutil::assert_slices_eq!(&[expected], &printer.messages.borrow());
}
}