use std::fmt;
#[derive(Eq, PartialEq, Debug, Copy, Clone, Default, Hash)]
pub enum Color {
#[default]
Default,
Index(u8),
Rgb(u8, u8, u8),
}
impl Color {
#[must_use]
pub fn as_rgb(self) -> Option<(u8, u8, u8)> {
match self {
Color::Rgb(r, g, b) => Some((r, g, b)),
_ => None,
}
}
#[must_use]
pub fn as_index(self) -> Option<u8> {
match self {
Color::Index(index) => Some(index),
_ => None,
}
}
#[must_use]
pub fn is_default(self) -> bool {
matches!(self, Color::Default)
}
fn write_fg(&self, out: &mut Vec<u8>) {
match self {
Color::Default => out.extend_from_slice(b"39"),
Color::Index(i) if *i < 8 => {
out.push(b'3');
out.push(b'0' + i);
}
Color::Index(i) if *i < 16 => {
out.push(b'9');
out.push(b'0' + (i - 8));
}
Color::Index(i) => {
out.extend_from_slice(b"38;5;");
out.extend_from_slice(i.to_string().as_bytes());
}
Color::Rgb(r, g, b) => {
out.extend_from_slice(b"38;2;");
out.extend_from_slice(r.to_string().as_bytes());
out.push(b';');
out.extend_from_slice(g.to_string().as_bytes());
out.push(b';');
out.extend_from_slice(b.to_string().as_bytes());
}
}
}
fn write_bg(&self, out: &mut Vec<u8>) {
match self {
Color::Default => out.extend_from_slice(b"49"),
Color::Index(i) if *i < 8 => {
out.push(b'4');
out.push(b'0' + i);
}
Color::Index(i) if *i < 16 => {
out.extend_from_slice(b"10");
out.push(b'0' + (i - 8));
}
Color::Index(i) => {
out.extend_from_slice(b"48;5;");
out.extend_from_slice(i.to_string().as_bytes());
}
Color::Rgb(r, g, b) => {
out.extend_from_slice(b"48;2;");
out.extend_from_slice(r.to_string().as_bytes());
out.push(b';');
out.extend_from_slice(g.to_string().as_bytes());
out.push(b';');
out.extend_from_slice(b.to_string().as_bytes());
}
}
}
fn write_underline(&self, out: &mut Vec<u8>) {
match self {
Color::Default => out.extend_from_slice(b"59"),
Color::Index(i) => {
out.extend_from_slice(b"58;5;");
out.extend_from_slice(i.to_string().as_bytes());
}
Color::Rgb(r, g, b) => {
out.extend_from_slice(b"58;2;");
out.extend_from_slice(r.to_string().as_bytes());
out.push(b';');
out.extend_from_slice(g.to_string().as_bytes());
out.push(b';');
out.extend_from_slice(b.to_string().as_bytes());
}
}
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Color::Default => f.write_str("default"),
Color::Index(index) => write!(f, "color({index})"),
Color::Rgb(r, g, b) => write!(f, "rgb({r},{g},{b})"),
}
}
}
const TEXT_MODE_INTENSITY: u16 = 0b0000_0000_0011;
const TEXT_MODE_BOLD: u16 = 0b0000_0000_0001;
const TEXT_MODE_DIM: u16 = 0b0000_0000_0010;
const TEXT_MODE_ITALIC: u16 = 0b0000_0000_0100;
const TEXT_MODE_INVERSE: u16 = 0b0000_0000_1000;
const TEXT_MODE_STRIKETHROUGH: u16 = 0b0000_0001_0000;
const TEXT_MODE_HIDDEN: u16 = 0b0000_0010_0000;
const TEXT_MODE_OVERLINE: u16 = 0b0010_0000_0000;
const TEXT_MODE_BLINK: u16 = 0b0100_0000_0000;
const UNDERLINE_STYLE_MASK: u16 = 0b0001_1100_0000;
const UNDERLINE_STYLE_SHIFT: u32 = 6;
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
#[repr(u8)]
#[non_exhaustive]
pub enum UnderlineStyle {
#[default]
None = 0,
Single = 1,
Double = 2,
Curly = 3,
Dotted = 4,
Dashed = 5,
}
impl UnderlineStyle {
fn from_u16(n: u16) -> Self {
match n {
1 => Self::Single,
2 => Self::Double,
3 => Self::Curly,
4 => Self::Dotted,
5 => Self::Dashed,
_ => Self::None,
}
}
pub(crate) fn from_sgr(n: u16) -> Self {
match n {
0 => Self::None,
1 => Self::Single,
2 => Self::Double,
3 => Self::Curly,
4 => Self::Dotted,
5 => Self::Dashed,
_ => Self::Single,
}
}
}
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Hash)]
#[non_exhaustive]
pub struct Attrs {
pub fg_color: Color,
pub bg_color: Color,
pub underline_color: Color,
pub(crate) mode: u16,
}
macro_rules! flag_accessors {
($($getter:ident / $setter:ident => $flag:ident),* $(,)?) => {
$(
#[doc = concat!("Return whether the ", stringify!($getter), " attribute is set.")]
pub fn $getter(&self) -> bool {
self.mode & $flag != 0
}
#[doc = concat!("Set or clear the ", stringify!($getter), " attribute.")]
pub fn $setter(&mut self, on: bool) {
if on {
self.mode |= $flag;
} else {
self.mode &= !$flag;
}
}
)*
};
}
impl Attrs {
pub fn bold(&self) -> bool {
self.mode & TEXT_MODE_BOLD != 0
}
pub fn dim(&self) -> bool {
self.mode & TEXT_MODE_DIM != 0
}
pub fn set_bold(&mut self) {
self.mode &= !TEXT_MODE_INTENSITY;
self.mode |= TEXT_MODE_BOLD;
}
pub fn set_dim(&mut self) {
self.mode &= !TEXT_MODE_INTENSITY;
self.mode |= TEXT_MODE_DIM;
}
pub fn set_normal_intensity(&mut self) {
self.mode &= !TEXT_MODE_INTENSITY;
}
flag_accessors! {
italic / set_italic => TEXT_MODE_ITALIC,
inverse / set_inverse => TEXT_MODE_INVERSE,
strikethrough / set_strikethrough => TEXT_MODE_STRIKETHROUGH,
hidden / set_hidden => TEXT_MODE_HIDDEN,
overline / set_overline => TEXT_MODE_OVERLINE,
blink / set_blink => TEXT_MODE_BLINK,
}
pub fn underline(&self) -> bool {
self.underline_style() != UnderlineStyle::None
}
pub fn underline_style(&self) -> UnderlineStyle {
let raw = (self.mode & UNDERLINE_STYLE_MASK) >> UNDERLINE_STYLE_SHIFT;
UnderlineStyle::from_u16(raw)
}
pub fn set_underline(&mut self, underline: bool) {
if underline {
self.set_underline_style(UnderlineStyle::Single);
} else {
self.set_underline_style(UnderlineStyle::None);
}
}
pub fn set_underline_style(&mut self, style: UnderlineStyle) {
self.mode &= !UNDERLINE_STYLE_MASK;
self.mode |= (style as u16) << UNDERLINE_STYLE_SHIFT;
}
#[must_use]
pub fn to_escape_sequence(&self, prev: &Attrs) -> Vec<u8> {
if *self == *prev {
return Vec::new();
}
let mut params: Vec<u8> = Vec::new();
let mut needs_sep = false;
let any_off = (prev.bold() && !self.bold())
|| (prev.dim() && !self.dim())
|| (prev.italic() && !self.italic())
|| (prev.underline() && !self.underline())
|| (prev.inverse() && !self.inverse())
|| (prev.strikethrough() && !self.strikethrough())
|| (prev.hidden() && !self.hidden())
|| (prev.overline() && !self.overline())
|| (prev.blink() && !self.blink());
let base = if any_off {
params.push(b'0');
needs_sep = true;
&Attrs::default()
} else {
prev
};
macro_rules! add_param {
($param:expr) => {
if needs_sep {
params.push(b';');
}
params.extend_from_slice($param);
needs_sep = true;
};
}
if self.bold() && !base.bold() {
add_param!(b"1");
}
if self.dim() && !base.dim() {
add_param!(b"2");
}
if self.italic() && !base.italic() {
add_param!(b"3");
}
if self.underline() && self.underline_style() != base.underline_style() {
match self.underline_style() {
UnderlineStyle::Single => {
add_param!(b"4");
}
UnderlineStyle::Double => {
add_param!(b"4:2");
}
UnderlineStyle::Curly => {
add_param!(b"4:3");
}
UnderlineStyle::Dotted => {
add_param!(b"4:4");
}
UnderlineStyle::Dashed => {
add_param!(b"4:5");
}
UnderlineStyle::None => {}
}
}
if self.blink() && !base.blink() {
add_param!(b"5");
}
if self.inverse() && !base.inverse() {
add_param!(b"7");
}
if self.hidden() && !base.hidden() {
add_param!(b"8");
}
if self.strikethrough() && !base.strikethrough() {
add_param!(b"9");
}
if self.overline() && !base.overline() {
add_param!(b"53");
}
if self.fg_color != base.fg_color {
if needs_sep {
params.push(b';');
}
self.fg_color.write_fg(&mut params);
needs_sep = true;
}
if self.bg_color != base.bg_color {
if needs_sep {
params.push(b';');
}
self.bg_color.write_bg(&mut params);
needs_sep = true;
}
if self.underline_color != base.underline_color {
if needs_sep {
params.push(b';');
}
self.underline_color.write_underline(&mut params);
needs_sep = true;
}
let _ = needs_sep;
if params.is_empty() {
return Vec::new();
}
let mut out = Vec::with_capacity(params.len() + 3);
out.extend_from_slice(b"\x1b[");
out.extend_from_slice(¶ms);
out.push(b'm');
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_attrs() {
let attrs = Attrs::default();
assert!(!attrs.bold());
assert!(!attrs.dim());
assert!(!attrs.italic());
assert!(!attrs.underline());
assert!(!attrs.inverse());
assert!(!attrs.strikethrough());
assert!(!attrs.hidden());
assert_eq!(attrs.fg_color, Color::Default);
assert_eq!(attrs.bg_color, Color::Default);
}
#[test]
fn bold_clears_dim() {
let mut attrs = Attrs::default();
attrs.set_dim();
assert!(attrs.dim());
attrs.set_bold();
assert!(attrs.bold());
assert!(!attrs.dim());
}
#[test]
fn dim_clears_bold() {
let mut attrs = Attrs::default();
attrs.set_bold();
assert!(attrs.bold());
attrs.set_dim();
assert!(attrs.dim());
assert!(!attrs.bold());
}
#[test]
fn normal_intensity_clears_both() {
let mut attrs = Attrs::default();
attrs.set_bold();
attrs.set_normal_intensity();
assert!(!attrs.bold());
assert!(!attrs.dim());
}
#[test]
fn toggle_italic() {
let mut attrs = Attrs::default();
attrs.set_italic(true);
assert!(attrs.italic());
attrs.set_italic(false);
assert!(!attrs.italic());
}
#[test]
fn underline_style_variants() {
let mut attrs = Attrs::default();
assert_eq!(attrs.underline_style(), UnderlineStyle::None);
assert!(!attrs.underline());
attrs.set_underline_style(UnderlineStyle::Single);
assert!(attrs.underline());
assert_eq!(attrs.underline_style(), UnderlineStyle::Single);
attrs.set_underline_style(UnderlineStyle::Curly);
assert!(attrs.underline());
assert_eq!(attrs.underline_style(), UnderlineStyle::Curly);
attrs.set_underline_style(UnderlineStyle::Double);
assert_eq!(attrs.underline_style(), UnderlineStyle::Double);
attrs.set_underline_style(UnderlineStyle::Dotted);
assert_eq!(attrs.underline_style(), UnderlineStyle::Dotted);
attrs.set_underline_style(UnderlineStyle::Dashed);
assert_eq!(attrs.underline_style(), UnderlineStyle::Dashed);
attrs.set_underline(false);
assert_eq!(attrs.underline_style(), UnderlineStyle::None);
}
#[test]
fn set_underline_bool_uses_single() {
let mut attrs = Attrs::default();
attrs.set_underline(true);
assert_eq!(attrs.underline_style(), UnderlineStyle::Single);
}
#[test]
fn underline_color() {
let mut attrs = Attrs::default();
assert_eq!(attrs.underline_color, Color::Default);
attrs.underline_color = Color::Rgb(255, 0, 0);
assert_eq!(attrs.underline_color, Color::Rgb(255, 0, 0));
}
#[test]
fn toggle_strikethrough() {
let mut attrs = Attrs::default();
attrs.set_strikethrough(true);
assert!(attrs.strikethrough());
attrs.set_strikethrough(false);
assert!(!attrs.strikethrough());
}
#[test]
fn toggle_hidden() {
let mut attrs = Attrs::default();
attrs.set_hidden(true);
assert!(attrs.hidden());
attrs.set_hidden(false);
assert!(!attrs.hidden());
}
#[test]
fn color_variants() {
assert_eq!(Color::Default, Color::default());
assert_ne!(Color::Index(1), Color::Index(2));
assert_eq!(Color::Rgb(10, 20, 30), Color::Rgb(10, 20, 30));
}
#[test]
fn escape_sequence_bold() {
let prev = Attrs::default();
let mut next = Attrs::default();
next.set_bold();
assert_eq!(next.to_escape_sequence(&prev), b"\x1b[1m");
}
#[test]
fn escape_sequence_reset_bold() {
let mut prev = Attrs::default();
prev.set_bold();
let next = Attrs::default();
assert_eq!(next.to_escape_sequence(&prev), b"\x1b[0m");
}
#[test]
fn escape_sequence_fg_color_idx() {
let prev = Attrs::default();
let next = Attrs {
fg_color: Color::Index(1),
..Attrs::default()
};
assert_eq!(next.to_escape_sequence(&prev), b"\x1b[31m");
}
#[test]
fn escape_sequence_rgb_bg() {
let prev = Attrs::default();
let next = Attrs {
bg_color: Color::Rgb(255, 128, 0),
..Attrs::default()
};
assert_eq!(next.to_escape_sequence(&prev), b"\x1b[48;2;255;128;0m");
}
#[test]
fn escape_sequence_no_change() {
let attrs = Attrs::default();
assert!(attrs.to_escape_sequence(&attrs).is_empty());
}
#[test]
fn escape_sequence_bright_fg() {
let prev = Attrs::default();
let next = Attrs {
fg_color: Color::Index(9),
..Attrs::default()
};
assert_eq!(next.to_escape_sequence(&prev), b"\x1b[91m");
}
#[test]
fn escape_sequence_256_color() {
let prev = Attrs::default();
let next = Attrs {
fg_color: Color::Index(200),
..Attrs::default()
};
assert_eq!(next.to_escape_sequence(&prev), b"\x1b[38;5;200m");
}
#[test]
fn escape_sequence_underline_style_transition() {
let mut prev = Attrs::default();
prev.set_underline_style(UnderlineStyle::Single);
let mut next = Attrs::default();
next.set_underline_style(UnderlineStyle::Curly);
let seq = next.to_escape_sequence(&prev);
assert_eq!(seq, b"\x1b[4:3m");
}
#[test]
fn escape_sequence_reset_and_reapply() {
let mut prev = Attrs::default();
prev.set_bold();
prev.fg_color = Color::Index(1);
let next = Attrs {
fg_color: Color::Index(1),
..Attrs::default()
};
let seq = next.to_escape_sequence(&prev);
assert_eq!(seq, b"\x1b[0;31m");
}
#[test]
fn escape_sequence_multiple_attrs() {
let prev = Attrs::default();
let mut next = Attrs::default();
next.set_bold();
next.set_italic(true);
next.fg_color = Color::Index(1);
let seq = next.to_escape_sequence(&prev);
assert_eq!(seq, b"\x1b[1;3;31m");
}
#[test]
fn to_escape_sequence_emits_underline_color_when_underline_off() {
let prev = Attrs::default();
let next = Attrs {
underline_color: Color::Rgb(170, 170, 170),
..Attrs::default()
};
assert_eq!(next.to_escape_sequence(&prev), b"\x1b[58;2;170;170;170m");
}
}