use std::fmt;
#[derive(Debug, Clone, Default)]
pub struct AnsiTracker {
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub blink: bool,
pub inverse: bool,
pub hidden: bool,
pub strikethrough: bool,
pub fg: Option<u32>,
pub bg: Option<u32>,
}
impl AnsiTracker {
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn to_sgr(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if self.bold {
parts.push("1".to_string());
}
if self.dim {
parts.push("2".to_string());
}
if self.italic {
parts.push("3".to_string());
}
if self.underline {
parts.push("4".to_string());
}
if self.blink {
parts.push("5".to_string());
}
if self.inverse {
parts.push("7".to_string());
}
if self.hidden {
parts.push("8".to_string());
}
if self.strikethrough {
parts.push("9".to_string());
}
if let Some(fg) = self.fg {
let s = format!("38;5;{}", fg);
parts.push(s);
}
if let Some(bg) = self.bg {
let s = format!("48;5;{}", bg);
parts.push(s);
}
if parts.is_empty() {
String::new()
} else {
format!("\x1b[{}m", parts.join(";"))
}
}
pub fn reset_sgr() -> String {
"\x1b[0m".to_string()
}
pub fn transition(from: &AnsiTracker, to: &AnsiTracker) -> String {
if from == to {
return String::new();
}
if to.bold
|| to.dim
|| to.italic
|| to.underline
|| to.blink
|| to.inverse
|| to.hidden
|| to.strikethrough
|| to.fg.is_some()
|| to.bg.is_some()
{
format!("{}{}", Self::reset_sgr(), to.to_sgr())
} else if from.bold
|| from.dim
|| from.italic
|| from.underline
|| from.blink
|| from.inverse
|| from.hidden
|| from.strikethrough
|| from.fg.is_some()
|| from.bg.is_some()
{
Self::reset_sgr()
} else {
String::new()
}
}
}
impl PartialEq for AnsiTracker {
fn eq(&self, other: &Self) -> bool {
self.bold == other.bold
&& self.dim == other.dim
&& self.italic == other.italic
&& self.underline == other.underline
&& self.blink == other.blink
&& self.inverse == other.inverse
&& self.hidden == other.hidden
&& self.strikethrough == other.strikethrough
&& self.fg == other.fg
&& self.bg == other.bg
}
}
impl fmt::Display for AnsiTracker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_sgr())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_is_reset() {
let tracker = AnsiTracker::default();
assert!(!tracker.bold);
assert!(!tracker.italic);
assert!(tracker.fg.is_none());
assert!(tracker.bg.is_none());
}
#[test]
fn test_reset_clears_all() {
let mut tracker = AnsiTracker {
bold: true,
italic: true,
fg: Some(196),
..Default::default()
};
tracker.reset();
assert!(!tracker.bold);
assert!(!tracker.italic);
assert!(tracker.fg.is_none());
}
#[test]
fn test_sgr_bold() {
let tracker = AnsiTracker {
bold: true,
..Default::default()
};
assert_eq!(tracker.to_sgr(), "\x1b[1m");
}
#[test]
fn test_sgr_multiple() {
let tracker = AnsiTracker {
bold: true,
italic: true,
..Default::default()
};
let sgr = tracker.to_sgr();
assert!(sgr.contains("1"));
assert!(sgr.contains("3"));
}
#[test]
fn test_sgr_empty() {
let tracker = AnsiTracker::default();
assert!(tracker.to_sgr().is_empty());
}
#[test]
fn test_transition_same() {
let a = AnsiTracker::default();
assert!(AnsiTracker::transition(&a, &a).is_empty());
}
#[test]
fn test_transition_to_styled() {
let from = AnsiTracker::default();
let to = AnsiTracker {
bold: true,
..Default::default()
};
let t = AnsiTracker::transition(&from, &to);
assert!(t.contains("\x1b[0m"));
assert!(t.contains("1"));
}
#[test]
fn test_transition_from_styled() {
let from = AnsiTracker {
bold: true,
..Default::default()
};
let to = AnsiTracker::default();
let t = AnsiTracker::transition(&from, &to);
assert_eq!(t, "\x1b[0m");
}
}