use std::{
borrow::Cow,
fmt::{self, Display},
str::FromStr,
sync::atomic::{AtomicBool, Ordering},
};
use crate::escape::{
self,
csi::{Csi, Sgr},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Underline {
#[default]
None = 0,
Single = 1,
Double = 2,
Curly = 3,
Dotted = 4,
Dashed = 5,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum CursorStyle {
#[default]
Default = 0,
BlinkingBlock = 1,
SteadyBlock = 2,
BlinkingUnderline = 3,
SteadyUnderline = 4,
BlinkingBar = 5,
SteadyBar = 6,
}
impl Display for CursorStyle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", *self as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WebColor(pub u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RgbColor {
pub red: u8,
pub green: u8,
pub blue: u8,
}
impl RgbColor {
pub const fn new(red: u8, green: u8, blue: u8) -> Self {
Self { red, green, blue }
}
pub fn new_f32(red: f32, green: f32, blue: f32) -> Self {
let red = (red * 255.) as u8;
let green = (green * 255.) as u8;
let blue = (blue * 255.) as u8;
Self { red, green, blue }
}
fn channel_from_hex(s: &str) -> Result<u8, InvalidFormatError> {
if s.is_empty() || s.len() > 4 {
return Err(InvalidFormatError);
}
let color: u16 = u16::from_str_radix(s, 16).map_err(|_| InvalidFormatError)?;
let divisor: usize = match s.len() {
1 => 0xf,
2 => 0xff,
3 => 0xfff,
4 => 0xffff,
_ => return Err(InvalidFormatError),
};
Ok(((color as usize) * 0xff / divisor) as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidFormatError;
impl FromStr for RgbColor {
type Err = InvalidFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(rgb) = s.strip_prefix("rgb:") {
let mut parts = rgb.split('/').map(Self::channel_from_hex);
let Some(r) = parts.next().transpose()? else {
return Err(InvalidFormatError);
};
let Some(g) = parts.next().transpose()? else {
return Err(InvalidFormatError);
};
let Some(b) = parts.next().transpose()? else {
return Err(InvalidFormatError);
};
Ok(Self::new(r, g, b))
} else if let Some(hex) = s.strip_prefix('#') {
let (r, g, b) = match hex.len() {
3 => (
Self::channel_from_hex(&hex[0..1])?,
Self::channel_from_hex(&hex[1..2])?,
Self::channel_from_hex(&hex[2..3])?,
),
6 => (
Self::channel_from_hex(&hex[0..2])?,
Self::channel_from_hex(&hex[2..4])?,
Self::channel_from_hex(&hex[4..6])?,
),
9 => (
Self::channel_from_hex(&hex[0..3])?,
Self::channel_from_hex(&hex[3..6])?,
Self::channel_from_hex(&hex[6..9])?,
),
12 => (
Self::channel_from_hex(&hex[0..4])?,
Self::channel_from_hex(&hex[4..8])?,
Self::channel_from_hex(&hex[8..12])?,
),
_ => return Err(InvalidFormatError),
};
Ok(Self::new(r, g, b))
} else {
Err(InvalidFormatError)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RgbaColor {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
}
impl From<RgbaColor> for RgbColor {
fn from(color: RgbaColor) -> Self {
Self {
red: color.red,
green: color.green,
blue: color.blue,
}
}
}
impl From<RgbColor> for RgbaColor {
fn from(color: RgbColor) -> Self {
Self {
red: color.red,
green: color.green,
blue: color.blue,
alpha: 255,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnsiColor {
Black = 0,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
}
pub type PaletteIndex = u8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorSpec {
Reset,
PaletteIndex(PaletteIndex),
TrueColor(RgbaColor),
}
impl ColorSpec {
pub const BLACK: Self = Self::PaletteIndex(AnsiColor::Black as PaletteIndex);
pub const RED: Self = Self::PaletteIndex(AnsiColor::Red as PaletteIndex);
pub const GREEN: Self = Self::PaletteIndex(AnsiColor::Green as PaletteIndex);
pub const YELLOW: Self = Self::PaletteIndex(AnsiColor::Yellow as PaletteIndex);
pub const BLUE: Self = Self::PaletteIndex(AnsiColor::Blue as PaletteIndex);
pub const MAGENTA: Self = Self::PaletteIndex(AnsiColor::Magenta as PaletteIndex);
pub const CYAN: Self = Self::PaletteIndex(AnsiColor::Cyan as PaletteIndex);
pub const WHITE: Self = Self::PaletteIndex(AnsiColor::White as PaletteIndex);
pub const BRIGHT_BLACK: Self = Self::PaletteIndex(AnsiColor::BrightBlack as PaletteIndex);
pub const BRIGHT_RED: Self = Self::PaletteIndex(AnsiColor::BrightRed as PaletteIndex);
pub const BRIGHT_GREEN: Self = Self::PaletteIndex(AnsiColor::BrightGreen as PaletteIndex);
pub const BRIGHT_YELLOW: Self = Self::PaletteIndex(AnsiColor::BrightYellow as PaletteIndex);
pub const BRIGHT_BLUE: Self = Self::PaletteIndex(AnsiColor::BrightBlue as PaletteIndex);
pub const BRIGHT_MAGENTA: Self = Self::PaletteIndex(AnsiColor::BrightMagenta as PaletteIndex);
pub const BRIGHT_CYAN: Self = Self::PaletteIndex(AnsiColor::BrightCyan as PaletteIndex);
pub const BRIGHT_WHITE: Self = Self::PaletteIndex(AnsiColor::BrightWhite as PaletteIndex);
}
impl From<AnsiColor> for ColorSpec {
fn from(color: AnsiColor) -> Self {
Self::PaletteIndex(color as u8)
}
}
impl From<WebColor> for ColorSpec {
fn from(color: WebColor) -> Self {
Self::PaletteIndex(color.0)
}
}
impl From<RgbColor> for ColorSpec {
fn from(color: RgbColor) -> Self {
Self::TrueColor(color.into())
}
}
impl From<RgbaColor> for ColorSpec {
fn from(color: RgbaColor) -> Self {
Self::TrueColor(color)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Intensity {
#[default]
Normal,
Bold,
Dim,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Blink {
#[default]
None,
Slow,
Rapid,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Font {
#[default]
Default,
Alternate(u8),
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum VerticalAlign {
#[default]
BaseLine = 0,
SuperScript = 1,
SubScript = 2,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stylized<'a> {
pub content: Cow<'a, str>,
styles: Vec<Sgr>,
}
static INITIALIZER: parking_lot::Once = parking_lot::Once::new();
static NO_COLOR: AtomicBool = AtomicBool::new(false);
impl Stylized<'_> {
pub fn is_ansi_color_disabled() -> bool {
INITIALIZER.call_once(|| {
NO_COLOR.store(
std::env::var("NO_COLOR").is_ok_and(|e| !e.is_empty()),
Ordering::SeqCst,
);
});
NO_COLOR.load(Ordering::SeqCst)
}
pub fn force_ansi_color(enable_color: bool) {
let _ = Self::is_ansi_color_disabled();
NO_COLOR.store(!enable_color, Ordering::SeqCst);
}
}
impl Display for Stylized<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let no_color = Self::is_ansi_color_disabled();
let mut styles = self
.styles
.iter()
.filter(|sgr| {
!(no_color
&& matches!(
sgr,
Sgr::Foreground(_) | Sgr::Background(_) | Sgr::UnderlineColor(_)
))
})
.peekable();
if styles.peek().is_none() {
write!(f, "{}", self.content)?;
} else {
write!(f, "{}0", escape::CSI)?;
for sgr in styles {
write!(f, ";{sgr}")?;
}
write!(f, "m{}{}", self.content, Csi::Sgr(Sgr::Reset))?;
}
Ok(())
}
}
pub trait StyleExt<'a>: Sized {
fn stylized(self) -> Stylized<'a>;
fn foreground(self, color: impl Into<ColorSpec>) -> Stylized<'a> {
let mut this = self.stylized();
this.styles.push(Sgr::Foreground(color.into()));
this
}
fn red(self) -> Stylized<'a> {
self.foreground(ColorSpec::RED)
}
fn yellow(self) -> Stylized<'a> {
self.foreground(ColorSpec::YELLOW)
}
fn green(self) -> Stylized<'a> {
self.foreground(ColorSpec::GREEN)
}
fn underlined(self) -> Stylized<'a> {
let mut this = self.stylized();
this.styles.push(Sgr::Underline(Underline::Single));
this
}
fn bold(self) -> Stylized<'a> {
let mut this = self.stylized();
this.styles.push(Sgr::Intensity(Intensity::Bold));
this
}
}
impl<'a> StyleExt<'a> for Cow<'a, str> {
fn stylized(self) -> Stylized<'a> {
Stylized {
content: self,
styles: Vec::with_capacity(2),
}
}
}
impl<'a> StyleExt<'a> for &'a str {
fn stylized(self) -> Stylized<'a> {
Cow::Borrowed(self).stylized()
}
}
impl StyleExt<'static> for String {
fn stylized(self) -> Stylized<'static> {
Cow::<str>::Owned(self).stylized()
}
}
impl<'a> StyleExt<'a> for Stylized<'a> {
fn stylized(self) -> Stylized<'a> {
self
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_color() {
assert_eq!("#282828".parse(), Ok(RgbColor::new(40, 40, 40)));
assert_eq!("rgb:28/28/28".parse(), Ok(RgbColor::new(40, 40, 40)));
assert_eq!("rgb:2828/2828/2828".parse(), Ok(RgbColor::new(40, 40, 40)));
}
}