use std::{
borrow::Cow,
char, env,
io::{stdin, stdout, Write},
};
use regex::{Captures, Regex};
#[cfg(test)]
mod tests;
mod consts;
use consts::*;
mod depth;
pub use depth::Depth;
const ESCAPE_IN_REGEX: [char; 14] = [
'[', ']', '(', ')', '{', '}', '*', '+', '.', '$', '^', '\\', '|', '?',
];
struct Patterns {
codes: Regex,
escaped: String,
}
impl Patterns {
pub fn new(marker: char) -> Self {
let escaped_marker = ESCAPE_IN_REGEX
.contains(&marker)
.then(|| format!(r"\{marker}"))
.unwrap_or_else(|| marker.to_string());
let regex = format!("{escaped_marker}(?:{})", *CODE_REGEX);
Self {
codes: Regex::new(®ex)
.expect("the pattern regex should be valid and properly escaped."),
escaped: format!("{marker}_"),
}
}
pub fn codes(&self) -> &Regex {
&self.codes
}
pub fn escaped(&self) -> &str {
&self.escaped
}
}
pub struct Dahlia {
depth: Option<Depth>,
auto_reset: bool,
patterns: Patterns,
marker: char,
}
impl Dahlia {
pub fn new(depth: Option<Depth>, auto_reset: bool, marker: char) -> Self {
let no_color = env::var("NO_COLOR").is_ok_and(|value| !value.is_empty());
let depth = if no_color { None } else { depth };
Self {
depth,
auto_reset,
patterns: Patterns::new(marker),
marker,
}
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.set_depth(depth);
self
}
pub fn with_auto_depth(mut self) -> Self {
self.set_auto_depth();
self
}
pub fn with_auto_reset(mut self, auto_reset: bool) -> Self {
self.set_auto_reset(auto_reset);
self
}
pub fn with_marker(mut self, marker: char) -> Self {
self.set_marker(marker);
self
}
pub fn set_depth(&mut self, depth: Depth) {
self.depth = Some(depth);
}
pub fn set_auto_depth(&mut self) {
self.depth = Depth::try_infer();
}
pub fn set_auto_reset(&mut self, auto_reset: bool) {
self.auto_reset = auto_reset;
}
pub fn set_marker(&mut self, marker: char) {
self.marker = marker;
self.patterns = Patterns::new(marker);
}
pub fn clean<'a>(&self, str: &'a str) -> Cow<'a, str> {
let converted = self.patterns.codes().replace_all(str, "");
self.finalize(converted)
}
pub fn convert<'a>(&self, str: &'a str) -> Cow<'a, str> {
if let Some(depth) = self.depth {
let replacer = |captures: &Captures<'_>| get_ansi(captures, depth);
let converted = self.patterns.codes().replace_all(str, replacer);
self.finalize(converted)
} else {
self.clean(str)
}
}
fn finalize<'a>(&self, str: Cow<'a, str>) -> Cow<'a, str> {
let str = if self.auto_reset && !str.ends_with("\x1b[0m") {
str + "\x1b[0m"
} else {
str
};
self.unescape(str)
}
fn unescape<'a>(&self, str: Cow<'a, str>) -> Cow<'a, str> {
let mut indices = str.match_indices(self.patterns.escaped()).peekable();
if indices.peek().is_none() {
return str;
}
let buffer = String::with_capacity(str.len());
let (new, last_match) = indices.fold((buffer, 0), |(acc, last_match), (start, chunk)| {
(
acc + &str[last_match..start] + &chunk[..chunk.len() - 1],
start + chunk.len(),
)
});
let tail = &str[last_match..];
Cow::Owned(new + tail)
}
pub fn input(&self, prompt: &str) -> std::io::Result<String> {
print!("{}", self.convert(prompt));
stdout().flush()?;
let mut inp = String::new();
stdin().read_line(&mut inp)?;
Ok(inp.trim_end().to_owned())
}
pub fn escape(&self, str: &str) -> String {
str.replace(self.marker, self.patterns.escaped())
}
}
fn get_ansi(captures: &Captures<'_>, depth: Depth) -> String {
if let Some(format) = captures.name("fmt") {
return format_to_ansi(format.as_str());
}
let bg = captures.name("bg").is_some();
let templater = if bg {
fmt_background_template
} else {
fmt_template
};
if let Some(hex) = captures.name("hex") {
return hex_to_ansi(hex.as_str(), templater);
}
let color = &captures["color"];
if depth == Depth::High {
let [r, g, b] = COLORS_24BIT(color).expect("the regex should match only valid color codes");
return fill_rgb_template(templater(Depth::High), r, g, b);
}
let color_map = colors(depth).expect("at this point depth should only be TTY, Low or Medium");
let mapped = color_map(color).expect("the regex should match only valid color codes");
let value = if bg && depth <= Depth::Low {
(mapped
.parse::<u8>()
.expect("color tables should contain valid numbers")
+ 10)
.to_string()
} else {
mapped.to_string()
};
fill_template(templater(depth), &value)
}
fn hex_to_ansi(hex: &str, templater: fn(Depth) -> &'static str) -> String {
let hex_digits = hex
.chars()
.map(|ch| ch.to_digit(16))
.collect::<Option<Vec<_>>>()
.expect("the regex should only match valid hexadecimal digits");
let [r, g, b] = match &hex_digits[..] {
[r, g, b] => [r, g, b].map(|&d| (0x11 * d).to_string()),
[r1, r2, g1, g2, b1, b2] => {
[(r1, r2), (g1, g2), (b1, b2)].map(|(h, l)| (h * 0x10 + l).to_string())
}
_ => unreachable!("the regex should only match codes of length 3 or 6"),
};
fill_rgb_template(templater(Depth::High), &r, &g, &b)
}
fn format_to_ansi(format: &str) -> String {
use std::fmt::Write;
let ansis = formatter(format)
.expect("the regex should match only valid formatter codes or reset codes.");
ansis.iter().fold(
String::with_capacity(ansis.len() * 5), |mut string, ansi| {
let _ = write!(string, "\x1b[{ansi}m");
string
},
)
}
impl Default for Dahlia {
fn default() -> Self {
Dahlia::new(None, true, '&')
}
}
fn fill_template(template: &str, value: &str) -> String {
template.replacen("{}", value, 1)
}
fn fill_rgb_template(template: &str, r: &str, g: &str, b: &str) -> String {
template
.replacen("{r}", r, 1)
.replacen("{g}", g, 1)
.replacen("{b}", b, 1)
}
pub fn clean_ansi(string: &str) -> Cow<'_, str> {
ANSI_REGEX.replace_all(string, "")
}
#[macro_export]
macro_rules! dprint {
($d:expr, $($arg:tt)*) => {
print!("{}", $d.convert(&format!($($arg)*)));
};
}
#[macro_export]
macro_rules! dprintln {
($d:expr, $($arg:tt)*) => {
println!("{}", $d.convert(&format!($($arg)*)));
};
}