pub(super) mod kind;
#[expect(unused_imports, reason = "For docs.")]
use crate::MsgKind;
#[cfg(feature = "progress")] use crate::BeforeAfter;
use fyi_ansi::{
ansi,
csi,
dim,
};
use kind::IntoMsgPrefix;
use std::{
borrow::{
Borrow,
Cow,
},
cmp::Ordering,
fmt,
hash,
io,
num::NonZeroUsize,
ops::Range,
};
#[cfg(feature = "timestamps")]
macro_rules! toc {
($p_end:expr, $m_end:expr) => (
$crate::msg::Toc([
0, 0, 0, $p_end, $m_end, $m_end, $m_end, ])
);
($p_end:expr, $m_end:expr, true) => (
$crate::msg::Toc([
0, 0, 0, $p_end, $m_end, $m_end, $m_end + 1, ])
);
}
#[cfg(not(feature = "timestamps"))]
macro_rules! toc {
($p_end:expr, $m_end:expr) => (
$crate::msg::Toc([
0, 0, $p_end, $m_end, $m_end, $m_end, ])
);
($p_end:expr, $m_end:expr, true) => (
$crate::msg::Toc([
0, 0, $p_end, $m_end, $m_end, $m_end + 1, ])
);
}
use toc;
#[derive(Debug, Default, Clone)]
pub struct Msg {
inner: String,
toc: Toc,
}
impl AsRef<[u8]> for Msg {
#[inline]
fn as_ref(&self) -> &[u8] { self.as_bytes() }
}
impl AsRef<str> for Msg {
#[inline]
fn as_ref(&self) -> &str { self.as_str() }
}
impl Borrow<str> for Msg {
#[inline]
fn borrow(&self) -> &str { self.as_str() }
}
impl fmt::Display for Msg {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<str as fmt::Display>::fmt(self.as_str(), f)
}
}
impl Eq for Msg {}
macro_rules! from_stringlike {
($ty:ty, $op:ident) => (
impl From<$ty> for Msg {
#[inline]
fn from(src: $ty) -> Self { Self::from(src.$op()) }
}
);
}
from_stringlike!(&str, to_owned);
from_stringlike!(&String, clone);
from_stringlike!(Cow<'_, str>, into_owned);
impl From<String> for Msg {
#[inline]
fn from(src: String) -> Self {
let m_end = src.len();
Self {
inner: src,
toc: toc!(0, m_end),
}
}
}
impl From<Msg> for String {
#[inline]
fn from(src: Msg) -> Self { src.into_string() }
}
impl hash::Hash for Msg {
#[inline]
fn hash<H: hash::Hasher>(&self, state: &mut H) { self.inner.hash(state); }
}
impl PartialEq for Msg {
#[inline]
fn eq(&self, other: &Self) -> bool { self.inner == other.inner }
}
impl PartialEq<str> for Msg {
#[inline]
fn eq(&self, other: &str) -> bool { self.as_str() == other }
}
impl PartialEq<Msg> for str {
#[inline]
fn eq(&self, other: &Msg) -> bool { <Msg as PartialEq<Self>>::eq(other, self) }
}
impl PartialEq<[u8]> for Msg {
#[inline]
fn eq(&self, other: &[u8]) -> bool { self.as_bytes() == other }
}
impl PartialEq<Msg> for [u8] {
#[inline]
fn eq(&self, other: &Msg) -> bool { <Msg as PartialEq<Self>>::eq(other, self) }
}
macro_rules! eq {
($parent:ty: $($ty:ty),+) => ($(
impl PartialEq<$ty> for Msg {
#[inline]
fn eq(&self, other: &$ty) -> bool { <Self as PartialEq<$parent>>::eq(self, other) }
}
impl PartialEq<Msg> for $ty {
#[inline]
fn eq(&self, other: &Msg) -> bool { <Msg as PartialEq<$parent>>::eq(other, self) }
}
)+);
}
eq!(str: &str, &String, String, &Cow<'_, str>, Cow<'_, str>, &Box<str>, Box<str>);
eq!([u8]: &[u8], &Vec<u8>, Vec<u8>, &Cow<'_, [u8]>, Cow<'_, [u8]>, &Box<[u8]>, Box<[u8]>);
impl Ord for Msg {
#[inline]
fn cmp(&self, other: &Self) -> Ordering { self.inner.cmp(&other.inner) }
}
impl PartialOrd for Msg {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl Msg {
#[must_use]
#[expect(clippy::needless_pass_by_value, reason = "Trait covers owned and referenced types.")]
pub fn new<P, S>(prefix: P, msg: S) -> Self
where
P: IntoMsgPrefix,
S: AsRef<str>,
{
let msg = msg.as_ref();
let p_end = prefix.prefix_len();
let mut inner = String::with_capacity(p_end + msg.len());
prefix.prefix_push(&mut inner);
inner.push_str(msg);
let m_end = inner.len();
Self {
inner,
toc: toc!(p_end, m_end),
}
}
}
impl Msg {
#[inline]
#[must_use]
pub const fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() }
#[inline]
#[must_use]
pub const fn as_str(&self) -> &str { self.inner.as_str() }
#[cfg(feature = "fitted")]
#[cfg_attr(docsrs, doc(cfg(feature = "fitted")))]
#[must_use]
#[inline]
pub fn fitted(&self, width: usize) -> Cow<'_, str> {
crate::fit_to_width(self.as_str(), width)
}
#[inline]
#[must_use]
pub fn into_bytes(self) -> Vec<u8> { self.inner.into_bytes() }
#[inline]
#[must_use]
pub fn into_string(self) -> String { self.inner }
#[inline]
#[must_use]
pub const fn is_empty(&self) -> bool { self.inner.is_empty() }
#[inline]
#[must_use]
pub const fn len(&self) -> usize { self.inner.len() }
}
impl Msg {
pub fn set_indent(&mut self, tabs: u8) {
static SPACES: &str = " ";
self.replace_part(
TocId::Indent,
SPACES.get(..usize::from(tabs) * 4).unwrap_or(SPACES),
);
}
pub fn set_msg<S: AsRef<str>>(&mut self, msg: S) {
self.replace_part(TocId::Message, msg.as_ref());
}
pub fn set_newline(&mut self, enabled: bool) {
let out = if enabled { "\n" } else { "" };
self.replace_part(TocId::Newline, out);
}
#[expect(clippy::needless_pass_by_value, reason = "Impl is on referenced and owned types.")]
pub fn set_prefix<P: IntoMsgPrefix>(&mut self, prefix: P) {
self.replace_part(TocId::Prefix, &prefix.prefix_str());
}
pub fn set_suffix<S: AsRef<str>>(&mut self, suffix: S) {
self.replace_part(TocId::Suffix, suffix.as_ref());
}
#[cfg(feature = "timestamps")]
#[cfg_attr(docsrs, doc(cfg(feature = "timestamps")))]
pub fn set_timestamp(&mut self, enabled: bool) {
if enabled {
let now = utc2k::Local2k::now().formatted();
let mut out = String::with_capacity(43);
out.push_str(concat!(csi!(dim), "[", csi!(reset, blue)));
out.push_str(now.as_str());
out.push_str(concat!(ansi!((reset, dim) "]"), " "));
self.replace_part(TocId::Timestamp, &out);
}
else { self.replace_part(TocId::Timestamp, ""); }
}
pub fn strip_ansi(&mut self) -> bool {
let mut changed = false;
for id in TocId::ANSI_PARTS {
let old_rng = self.toc.part_rng(id);
let old_len = old_rng.len();
let mut removed = 0;
let mut start = old_rng.start;
let mut stop = old_rng.end;
while let Some(mut ansi_rng) = self.inner.get(start..stop).and_then(crate::ansi::next_ansi) {
ansi_rng.start += start;
ansi_rng.end += start;
let ansi_len = ansi_rng.len();
removed += ansi_len; start = ansi_rng.start; stop -= ansi_len; self.inner.replace_range(ansi_rng, "");
}
if removed != 0 {
self.toc.resize_part(id, old_len - removed);
changed = true;
}
}
changed
}
}
impl Msg {
#[inline]
#[must_use]
pub fn with_indent(mut self, tabs: u8) -> Self {
self.set_indent(tabs);
self
}
#[inline]
#[must_use]
pub fn with_msg<S: AsRef<str>>(mut self, msg: S) -> Self {
self.set_msg(msg);
self
}
#[inline]
#[must_use]
pub fn with_newline(mut self, enabled: bool) -> Self {
self.set_newline(enabled);
self
}
#[inline]
#[must_use]
pub fn with_prefix<P: IntoMsgPrefix>(mut self, prefix: P) -> Self {
self.set_prefix(prefix);
self
}
#[inline]
#[must_use]
pub fn with_suffix<S: AsRef<str>>(mut self, suffix: S) -> Self {
self.set_suffix(suffix);
self
}
#[cfg(feature = "timestamps")]
#[cfg_attr(docsrs, doc(cfg(feature = "timestamps")))]
#[inline]
#[must_use]
pub fn with_timestamp(mut self, enabled: bool) -> Self {
self.set_timestamp(enabled);
self
}
#[must_use]
pub fn without_ansi(mut self) -> Self {
self.strip_ansi();
self
}
}
impl Msg {
#[inline]
pub fn print(&self) {
use io::Write;
let mut handle = io::stdout().lock();
let _res = handle.write_all(self.as_bytes()).and_then(|()| handle.flush());
}
#[inline]
pub fn eprint(&self) {
use io::Write;
let mut handle = io::stderr().lock();
let _res = handle.write_all(self.as_bytes()).and_then(|()| handle.flush());
}
#[must_use]
#[inline]
pub fn prompt(&self) -> bool { self.prompt_with_default(false) }
#[must_use]
#[inline]
pub fn prompt_with_default(&self, default: bool) -> bool {
self.prompt__(default, false)
}
#[must_use]
#[inline]
pub fn eprompt(&self) -> bool { self.eprompt_with_default(false) }
#[must_use]
#[inline]
pub fn eprompt_with_default(&self, default: bool) -> bool {
self.prompt__(default, true)
}
fn prompt__(&self, default: bool, stderr: bool) -> bool {
let q = self.clone()
.with_suffix(
if default { concat!(" ", dim!("[", csi!(underline), "Y", csi!(!underline), "/n]"), " ") }
else { concat!(" ", dim!("[y/", csi!(underline), "N", csi!(!underline), "]"), " ") },
)
.with_newline(false);
let mut result = String::new();
loop {
if stderr { q.eprint(); }
else { q.print(); }
if io::stdin().read_line(&mut result).is_ok() {
result.make_ascii_lowercase();
match result.trim() {
"" => return default,
"n" | "no" => return false,
"y" | "yes" => return true,
_ => {}
}
}
result.truncate(0);
let err = Self::error(concat!(
"Invalid input; enter ",
ansi!((light_red) "N"),
" or ",
ansi!((light_green) "Y"),
".",
));
if stderr { err.eprint(); }
else { err.print(); }
}
}
}
#[cfg(feature = "progress")]
impl Msg {
#[cfg_attr(docsrs, doc(cfg(feature = "progress")))]
#[must_use]
pub fn with_bytes_saved(mut self, state: BeforeAfter) -> Self {
use dactyl::{NicePercent, NiceU64};
use fyi_ansi::csi;
if let Some(saved) = state.less() {
let saved = NiceU64::from(saved);
let buf = state.less_percent().map_or_else(
|| {
let mut buf = String::with_capacity(24 + saved.len());
buf.push_str(concat!(" ", csi!(dim), "(Saved "));
buf.push_str(saved.as_str());
buf.push_str(concat!(" bytes.)", csi!()));
buf
},
|per| {
let per = NicePercent::from(per);
let mut buf = String::with_capacity(26 + saved.len() + per.len());
buf.push_str(concat!(" ", csi!(dim), "(Saved "));
buf.push_str(saved.as_str());
buf.push_str(" bytes, ");
buf.push_str(per.as_str());
buf.push_str(concat!(".)", csi!()));
buf
}
);
self.replace_part(TocId::Suffix, &buf);
}
else {
self.replace_part(
TocId::Suffix,
concat!(" ", dim!("(No savings.)")),
);
}
self
}
}
impl Msg {
fn replace_part(&mut self, id: TocId, content: &str) {
let rng = self.toc.part_rng(id);
self.inner.replace_range(rng, content);
self.toc.resize_part(id, content.len());
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Toc([usize; Self::SIZE]);
impl Toc {
const SIZE: usize = TocId::Newline as usize + 2;
const fn part_len(&self, id: TocId) -> Option<NonZeroUsize> {
let start = self.0[id as usize]; let end = self.0[id as usize + 1]; if let Some(len) = end.checked_sub(start) { NonZeroUsize::new(len) }
else { None }
}
const fn part_rng(&self, id: TocId) -> Range<usize> {
let start = self.0[id as usize];
let end = self.0[id as usize + 1];
start..end
}
fn resize_part(&mut self, id: TocId, new_len: usize) {
let old_len = self.part_len(id).map_or(0, NonZeroUsize::get);
match old_len.cmp(&new_len) {
Ordering::Less => {
let diff = new_len - old_len;
for v in self.0.iter_mut().skip(id as usize + 1) { *v += diff; }
},
Ordering::Greater => {
let diff = old_len - new_len;
for v in self.0.iter_mut().skip(id as usize + 1) { *v -= diff; }
},
Ordering::Equal => {},
}
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum TocId {
Indent = 0_u8,
#[cfg(feature = "timestamps")]
Timestamp = 1_u8,
#[cfg(feature = "timestamps")]
Prefix = 2_u8,
#[cfg(feature = "timestamps")]
Message = 3_u8,
#[cfg(feature = "timestamps")]
Suffix = 4_u8,
#[cfg(feature = "timestamps")]
Newline = 5_u8,
#[cfg(not(feature = "timestamps"))]
Prefix = 1_u8,
#[cfg(not(feature = "timestamps"))]
Message = 2_u8,
#[cfg(not(feature = "timestamps"))]
Suffix = 3_u8,
#[cfg(not(feature = "timestamps"))]
Newline = 4_u8,
}
impl TocId {
#[cfg(feature = "timestamps")]
const ANSI_PARTS: [Self; 4] = [
Self::Timestamp, Self::Prefix, Self::Message, Self::Suffix,
];
#[cfg(not(feature = "timestamps"))]
const ANSI_PARTS: [Self; 3] = [Self::Prefix, Self::Message, Self::Suffix];
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn t_strip_ansi() {
let mut msg = Msg::error("Oops: \x1b[2mDaisy\x1b[0m.")
.with_suffix(" \x1b[1mYikes!\x1b[0m");
assert_eq!(
msg,
"\x1b[1;91mError:\x1b[0m Oops: \x1b[2mDaisy\x1b[0m. \x1b[1mYikes!\x1b[0m\n",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Prefix)],
"\x1b[1;91mError:\x1b[0m ",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Message)],
"Oops: \x1b[2mDaisy\x1b[0m.",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Suffix)],
" \x1b[1mYikes!\x1b[0m",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Newline)],
"\n",
);
msg.strip_ansi();
assert_eq!(
msg,
"Error: Oops: Daisy. Yikes!\n",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Prefix)],
"Error: ",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Message)],
"Oops: Daisy.",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Suffix)],
" Yikes!",
);
assert_eq!(
&msg.inner[msg.toc.part_rng(TocId::Newline)],
"\n",
);
assert_eq!(
msg.clone().without_ansi(),
msg,
);
}
}