#![doc(html_root_url = "https://docs.rs/murmur/")]
mod color_map;
mod icon_map;
pub use icon_map::IconKind;
use core::fmt::{Debug, Display};
use std::fmt;
use std::io::{self, BufWriter, Write};
#[derive(Debug)]
#[non_exhaustive]
pub enum WhisperError {
Lock,
Print,
Write,
Flush,
Utf8Conversion,
}
impl Display for WhisperError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Lock => write!(f, "Failed to acquire lock on ICON_MAP"),
Self::Print => write!(f, "Failed to print message"),
Self::Write => write!(f, "Error writing to buffer"),
Self::Flush => write!(f, "Error flushing buffer"),
Self::Utf8Conversion => write!(f, "Failed to convert bytes to UTF-8 string"),
}
}
}
impl std::error::Error for WhisperError {}
#[derive(Debug, Clone, Eq, PartialEq, Default)]
pub struct Whisper {
pub icon_kind: Option<IconKind>,
pub messages: Vec<String>,
}
impl Whisper {
#[must_use]
pub fn new() -> Self {
Self {
icon_kind: None,
messages: Vec::new(),
}
}
#[must_use]
pub fn icon(mut self, icon_kind: IconKind) -> Self {
self.icon_kind = Some(icon_kind);
self
}
#[must_use]
pub fn message<T: Display + Debug>(mut self, message: T) -> Self {
self.messages.push(message.to_string());
self
}
#[must_use]
pub fn messages<I, S>(mut self, messages: I) -> Self
where
I: IntoIterator<Item = S>,
S: Display + Debug + AsRef<str>,
{
for message in messages {
self.messages.push(message.as_ref().to_string());
}
self
}
pub fn whisper(&self) -> Result<(), WhisperError> {
let icon_map = icon_map::ICON_MAP.read().map_err(|_| WhisperError::Lock)?;
let (icon, color) = self.icon_kind.clone().map_or(("", ""), |icon_kind| {
icon_map.get(&icon_kind).map_or(("", ""), |value| *value)
});
self.print_messages(icon, color)
.map_err(|_| WhisperError::Print)?;
Ok(())
}
#[cfg(feature = "experimental")]
pub fn whisper_out(self, output: &std::process::Output) -> Result<(), WhisperError> {
let message =
std::str::from_utf8(&output.stdout).map_err(|_| WhisperError::Utf8Conversion)?;
let whisper = self.message(message);
whisper.whisper()?;
Ok(())
}
#[cfg(feature = "experimental")]
pub fn whisper_err(self, output: &std::process::Output) -> Result<(), WhisperError> {
let message =
std::str::from_utf8(&output.stderr).map_err(|_| WhisperError::Utf8Conversion)?;
let whisper = self.message(message);
whisper.whisper()?;
Ok(())
}
fn print_messages(&self, icon: &str, color: &str) -> Result<(), WhisperError> {
let messages = if self.messages.is_empty() {
vec![String::new()]
} else {
self.messages.clone()
};
for (index, message) in messages.iter().enumerate() {
let prefix = if index == 0 { icon } else { " " };
Self::print_message(color, prefix, message).map_err(|_| WhisperError::Print)?;
}
Ok(())
}
fn print_message(color: &str, prefix: &str, message: &str) -> Result<(), WhisperError> {
const BUFFER_SIZE: usize = 8192;
let stdout = io::stdout();
let mut writer = BufWriter::with_capacity(BUFFER_SIZE, stdout.lock());
if let Some(color_fn) = color_map::COLOR_MAP.get(color) {
writeln!(writer, "{}{}", color_fn(prefix), color_fn(message))
.map_err(|_| WhisperError::Write)?;
} else {
writeln!(writer, "{prefix}{message}").map_err(|_| WhisperError::Write)?;
}
writer.flush().map_err(|_| WhisperError::Flush)?;
Ok(())
}
}
#[cfg(test)]
#[cfg(feature = "experimental")]
mod whisper_experimental {
use super::*;
use std::error::Error;
use std::process::Command;
#[test]
fn execute_cargo_version() -> Result<(), Box<dyn Error>> {
let success = Whisper::new().icon(IconKind::NfFaCheck);
let failure = Whisper::new().icon(IconKind::NfFaTimes);
let output = Command::new("cargo").arg("version").output()?;
if output.status.success() {
success.whisper_out(&output)?;
} else {
failure.whisper_err(&output)?;
}
Ok(())
}
#[test]
#[allow(clippy::match_bool)]
fn execute_cargo_version_whisper_error() -> Result<(), WhisperError> {
let check = Whisper::new().icon(IconKind::NfFaCheck);
let times = Whisper::new().icon(IconKind::NfFaTimes);
let output = Command::new("cargo")
.arg("version")
.output()
.map_err(|_| WhisperError::Print)?;
match output.status.success() {
true => check.whisper_out(&output)?,
false => times.whisper_err(&output)?,
}
Ok(())
}
#[test]
#[allow(clippy::match_bool)]
fn execute_cargo_version_box_dyn() -> Result<(), Box<dyn Error>> {
let check = Whisper::new().icon(IconKind::NfFaCheck);
let times = Whisper::new().icon(IconKind::NfFaTimes);
let output = Command::new("cargo").arg("version").output()?;
match output.status.success() {
true => check.whisper_out(&output)?,
false => times.whisper_err(&output)?,
}
Ok(())
}
}
#[cfg(test)]
mod whisper_color_override_tests {
use super::*;
use owo_colors::OwoColorize;
#[test]
fn test_whisper_color_print() {
Whisper::new()
.message("test color".yellow())
.whisper()
.unwrap();
}
#[test]
fn test_whisper_with_icon_and_color() {
Whisper::new()
.icon(IconKind::NfFaCheck)
.message("each icon provides a default color but you can override it")
.message("this message is red".red())
.whisper()
.unwrap();
}
#[test]
fn test_add_custom_color() {
Whisper::new()
.icon(IconKind::NfFaBug)
.message("test color".magenta())
.message("owo_colors crate has a lot of colors".cyan())
.message("you can add your own colors".red())
.whisper()
.unwrap();
}
}
#[cfg(test)]
mod whisper_functionality_tests {
use super::*;
#[test]
fn test_whisper_messages() {
Whisper::new()
.messages(["1 message without icon", "2 message", "3 message"])
.whisper()
.ok();
Whisper::new()
.messages(vec!["1 message without icon", "2 message", "3 message"])
.whisper()
.ok();
Whisper::new()
.messages(["1 message without icon", "2 message", "3 message"].iter())
.whisper()
.ok();
}
#[test]
fn test_whisper_no_icon_no_messages() {
let whisper = Whisper::new();
let result = whisper.whisper();
assert!(result.is_ok()); assert_eq!(whisper.icon_kind, None);
assert_eq!(whisper.messages, Vec::<String>::new());
}
#[test]
fn test_whisper_no_icon_one_message() {
let whisper = Whisper::new().message("message without icon");
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, None);
assert_eq!(whisper.messages, vec!["message without icon"]);
}
#[test]
fn test_whisper_no_icon_multiple_messages() {
let whisper = Whisper::new()
.message("1 message without icon")
.message("2 message without icon")
.message("3 message without icon");
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, None);
assert_eq!(
whisper.messages.as_slice(),
&[
"1 message without icon",
"2 message without icon",
"3 message without icon"
]
);
}
#[test]
fn test_whisper_icon_no_message() {
let whisper = Whisper::new().icon(IconKind::NfFaBug);
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaBug));
assert_eq!(whisper.messages, Vec::<String>::new());
}
#[test]
fn test_whisper_icon_message() {
let whisper = Whisper::new()
.icon(IconKind::NfFaInfoCircle)
.message("message with icon");
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaInfoCircle));
assert_eq!(whisper.messages.as_slice(), &["message with icon"]);
}
#[test]
fn test_whisper_icon_multiple_messages() {
let whisper = Whisper::new()
.icon(IconKind::NfFaWarning)
.message("First message")
.message("Second message")
.message("Third message");
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaWarning));
assert_eq!(
whisper.messages.as_slice(),
&["First message", "Second message", "Third message"]
);
}
#[test]
fn test_whisper_icon_message_and_multiple_messages() {
let whisper = Whisper::new()
.icon(IconKind::NfFaCheck)
.message("First message")
.message("Second message")
.messages(vec!["Third message", "Fourth message"]);
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaCheck));
assert_eq!(
whisper.messages.as_slice(),
&[
"First message",
"Second message",
"Third message",
"Fourth message"
]
);
}
#[test]
fn test_icon_multiple_message_and_messages() {
let whisper = Whisper::new()
.icon(IconKind::NfFaWarning)
.messages(vec!["Line", "Another line"])
.messages(vec!["Another line"]);
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(
whisper.messages,
vec!["Line", "Another line", "Another line"]
);
}
#[test]
fn test_message_empty_messages() {
let whisper = Whisper::new().messages(Vec::<String>::new());
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, None);
assert_eq!(whisper.messages, Vec::<String>::new());
}
#[test]
fn test_message_multiple_messages() {
let whisper = Whisper::new()
.icon(IconKind::NfFaTimes)
.messages(vec!["Test message vec 1", "Test message vec 2"]);
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaTimes));
assert_eq!(
whisper.messages,
vec!["Test message vec 1", "Test message vec 2"]
);
}
#[test]
fn test_whisper_add_icon_random_order() {
let whisper = Whisper::new()
.message("Test adding icon in random place")
.icon(IconKind::NfFaBug)
.message("icon should be added to the first message");
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaBug));
assert_eq!(
whisper.messages,
vec![
"Test adding icon in random place",
"icon should be added to the first message"
]
);
}
#[test]
fn test_whisper_append_icon_message_to_instance() {
let mut whisper = Whisper::new().message("Test creating a Whisper instance with message");
assert_eq!(whisper.icon_kind, None);
assert_eq!(
whisper.messages,
vec!["Test creating a Whisper instance with message"]
);
whisper = whisper
.message("Append a message and icon after creation")
.icon(IconKind::NfFaInfoCircle);
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaInfoCircle));
assert_eq!(
whisper.messages,
vec![
"Test creating a Whisper instance with message",
"Append a message and icon after creation"
]
);
}
#[test]
fn test_whisper_default() {
let whisper = Whisper::default()
.icon(IconKind::NfFaRefresh)
.message("Test default");
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.icon_kind, Some(IconKind::NfFaRefresh));
}
#[test]
fn test_whisper_very_long_message() {
let long_message = "a".repeat(10000);
let whisper = Whisper::new()
.icon(IconKind::NfFaBug)
.message(long_message.clone());
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.messages, vec![long_message]);
}
#[test]
fn test_whisper_special_characters_in_message() {
let special_message = "!@#$%^&*()";
let whisper = Whisper::new()
.icon(IconKind::NfFaBug)
.message(special_message);
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.messages, vec![special_message]);
}
#[test]
fn test_whisper_large_number_of_messages() {
let messages = vec!["Test message".to_string(); 1000];
let whisper = Whisper::new()
.icon(IconKind::NfFaBug)
.messages(messages.clone());
let result = whisper.whisper();
assert!(result.is_ok());
assert_eq!(whisper.messages, messages);
}
}
#[cfg(test)]
mod whisper_error_tests {
use super::*;
#[test]
fn whisper_error_lock_error() {
let error = WhisperError::Lock;
assert_eq!(format!("{error}"), "Failed to acquire lock on ICON_MAP");
}
#[test]
fn whisper_error_print_error() {
let error = WhisperError::Print;
assert_eq!(format!("{error}"), "Failed to print message");
}
#[test]
fn whisper_error_write_error() {
let error = WhisperError::Write;
assert_eq!(format!("{error}"), "Error writing to buffer");
}
#[test]
fn whisper_error_flush_error() {
let error = WhisperError::Flush;
assert_eq!(format!("{error}"), "Error flushing buffer");
}
}