use fyi_ansi::csi;
use std::{
fmt,
hash,
ops::Range,
};
#[cfg(any(feature = "fitted", feature = "progress"))]
use std::str::Chars;
const ANSI_ESCAPE: char = '\x1b';
const ANSI_BELL: char = '\x07';
const ANSI_OE: char = '\u{0153}';
include!(concat!(env!("OUT_DIR"), "/ansi-color.rs"));
impl fmt::Display for AnsiColor {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<str as fmt::Display>::fmt(self.as_str(), f)
}
}
impl From<u8> for AnsiColor {
#[inline]
fn from(num: u8) -> Self { Self::from_u8(num) }
}
impl hash::Hash for AnsiColor {
#[inline]
fn hash<H: hash::Hasher>(&self, state: &mut H) { state.write_u8(*self as u8); }
}
impl PartialEq<u8> for AnsiColor {
#[inline]
fn eq(&self, other: &u8) -> bool { *self as u8 == *other }
}
impl PartialEq<AnsiColor> for u8 {
#[inline]
fn eq(&self, other: &AnsiColor) -> bool { *self == *other as Self }
}
impl AnsiColor {
pub const RESET: &'static str = csi!();
pub(crate) const RESET_PREFIX: &'static str = concat!(":", csi!(), " ");
}
#[cfg(any(feature = "fitted", feature = "progress"))]
#[derive(Debug, Clone)]
pub(crate) struct NoAnsi<'a> {
iter: Chars<'a>,
buf: Option<char>,
pos: usize,
}
#[cfg(any(feature = "fitted", feature = "progress"))]
impl<'a> NoAnsi<'a> {
#[inline]
#[must_use]
pub(crate) fn new(src: &'a str) -> Self {
Self {
iter: src.chars(),
buf: None,
pos: 0,
}
}
#[inline]
#[must_use]
pub(crate) const fn byte_pos(&self) -> usize { self.pos }
#[cold]
fn drain_osc(&mut self) {
while let Some(c) = self.buf.take().or_else(|| self.iter.next()) {
self.pos += c.len_utf8();
match c {
ANSI_OE | ANSI_BELL => { break; },
ANSI_ESCAPE => {
match self.iter.next() {
Some('\\') => {
self.pos += 1;
break;
},
Some(c) => { self.buf.replace(c); },
_ => {},
}
},
_ => {},
}
}
}
}
#[cfg(any(feature = "fitted", feature = "progress"))]
impl Iterator for NoAnsi<'_> {
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
while let Some(next) = self.buf.take().or_else(|| self.iter.next()) {
if ANSI_ESCAPE == next {
match self.iter.next() {
Some('[') => {
self.pos += 2;
for c in self.iter.by_ref() {
self.pos += c.len_utf8();
if matches!(c, '\x40'..='\x7E') { break; }
}
continue;
},
Some(']') => {
self.pos += 2;
self.drain_osc();
continue;
},
Some(c) => { self.buf.replace(c); },
None => {},
}
}
self.pos += next.len_utf8();
return Some(next);
}
None
}
fn size_hint(&self) -> (usize, Option<usize>) {
let (_, max) = self.iter.size_hint();
(0, max)
}
}
#[cfg(any(feature = "fitted", feature = "progress"))]
impl std::iter::FusedIterator for NoAnsi<'_> {}
#[cfg(feature = "fitted")]
pub(crate) struct OnlyAnsi<'a>(&'a str);
#[cfg(feature = "fitted")]
impl<'a> OnlyAnsi<'a> {
#[inline]
#[must_use]
pub(crate) const fn new(src: &'a str) -> Self { Self(src) }
}
#[cfg(feature = "fitted")]
impl<'a> Iterator for OnlyAnsi<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let rng = next_ansi(self.0)?;
let end = rng.end;
let next = self.0.get(rng); self.0 = self.0.get(end..).unwrap_or(""); next
}
}
#[cfg(feature = "fitted")]
impl std::iter::FusedIterator for OnlyAnsi<'_> {}
pub(crate) fn next_ansi(src: &str) -> Option<Range<usize>> {
let len = src.len();
let start = src.as_bytes()
.array_windows()
.position(|[a, b]| *a == b'\x1b' && matches!(*b, b'[' | b']'))?;
let mut end = None;
let mut chars = src.get(start..)?.char_indices().skip(1);
match chars.next() {
Some((1, '[')) => {
end = chars.find_map(|(k, c)| matches!(c, '\x40'..='\x7E').then_some(k + c.len_utf8()));
},
Some((1, ']')) => {
let mut buf = None;
while let Some((k, c)) = buf.take().or_else(|| chars.next()) {
match c {
ANSI_OE | ANSI_BELL => {
end = Some(k + c.len_utf8());
break;
},
ANSI_ESCAPE => match chars.next() {
Some((k, '\\')) => {
end = Some(k + c.len_utf8());
break;
},
Some(nope) => { buf.replace(nope); },
None => {},
},
_ => {},
}
}
},
_ => {},
}
Some(start..start + end.unwrap_or(len))
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(any(feature = "fitted", feature = "progress"))]
#[test]
fn t_ansi_csi() {
const LIST: [&str; 4] = [
"One \x1b[1mTwo\x1b[0m Three",
"\x1b[38;5;199mBjörk\x1b[m \x1b[1mBjörk\x1b[0m \x1b[1;2;91mBjörk\x1b[0m",
"\x1b\x1b[1mFakeout!\x1b[0m",
"text/plain",
];
for (raw, expected) in LIST.iter().zip([
"One Two Three",
"Björk Björk Björk",
"\x1bFakeout!",
"text/plain",
]) {
assert_eq!(
NoAnsi::new(raw).collect::<String>(),
expected,
);
}
#[cfg(feature = "fitted")]
for (raw, expected) in LIST.iter().zip([
"\x1b[1m\x1b[0m",
"\x1b[38;5;199m\x1b[m\x1b[1m\x1b[0m\x1b[1;2;91m\x1b[0m",
"\x1b[1m\x1b[0m",
"",
]) {
assert_eq!(
OnlyAnsi::new(raw).collect::<String>(),
expected,
);
}
}
#[cfg(any(feature = "fitted", feature = "progress"))]
#[test]
fn t_ansi_osc() {
for i in [
"One \x1b]Two\x07 Three",
"One \x1b]Twoœ Three",
"One \x1b]Two\x1b\\ Three",
"One \x1b]Two\x1b\x1b\\ Three", "One \x1b]Two\x1bHi\x1b\\ Three", "One \x1b]Two\x1b\x07 Three", "One \x1b]Two\x1bœ Three", "One \x1b]Two\x1b\x1bœ Three", "One \x1b]Two\x1b\x1b\x1bœ Three", ] {
let stripped: String = NoAnsi::new(i).collect();
assert_eq!(stripped, "One Three", "Chars: {:?}", i.as_bytes());
#[cfg(feature = "fitted")]
assert_eq!(
OnlyAnsi::new(i).collect::<String>(),
i.strip_prefix("One ").and_then(|j| j.strip_suffix(" Three")).unwrap(),
"Raw: {i:?}",
);
}
}
#[test]
fn t_next_ansi() {
assert_eq!(
next_ansi("Björk \x1b[2mGuðmundsdóttir\x1b[0m"),
Some(7..11),
);
assert_eq!(
next_ansi("\x1b[38"),
Some(0..4),
);
}
#[cfg(any(feature = "fitted", feature = "progress"))]
#[test]
fn t_byte_pos() {
for s in [
"Björk \x1b[2mGuðmundsdóttir\x1b[0m",
"One \x1b]Two\x1b\x1b\x1bœ Three",
"One \x1b]Two\x1b\x1bœ Three",
"One \x1b]Two\x1bHi\x1b\\ Three",
"One \x1b]Twoœ Three",
] {
let mut iter = NoAnsi::new(s);
while iter.next().is_some() { }
assert_eq!(s.len(), iter.byte_pos());
}
let raw = "\x1b[1mHello\x1b[0m";
let mut iter = NoAnsi::new(raw);
assert_eq!(iter.next(), Some('H'));
assert_eq!(&raw[..iter.byte_pos()], "\x1b[1mH");
}
}