utilprint 0.1.4

A utility crate for easily using colors or unicode in your terminal printing
Documentation
use lazy_static::lazy_static;
use std::char;
use std::collections::HashMap;
use std::fmt::Write;

pub struct ColorCodes {
    codes: HashMap<String, String>,
}

lazy_static! {
    static ref COLOR_CODES: ColorCodes = ColorCodes::new();
}

impl ColorCodes {
    pub fn new() -> Self {
        let color_pairs = vec![
            ("@d", "\x1b[30m"),
            ("@r", "\x1b[31m"),
            ("@g", "\x1b[32m"),
            ("@y", "\x1b[33m"),
            ("@b", "\x1b[34m"),
            ("@m", "\x1b[35m"),
            ("@c", "\x1b[36m"),
            ("@w", "\x1b[37m"),
            ("@D", "\x1b[90m"),
            ("@R", "\x1b[91m"),
            ("@G", "\x1b[92m"),
            ("@Y", "\x1b[93m"),
            ("@B", "\x1b[94m"),
            ("@M", "\x1b[95m"),
            ("@C", "\x1b[96m"),
            ("@W", "\x1b[97m"),
            ("@u", "\x1b[0m"),
        ];

        let mut codes = HashMap::with_capacity(color_pairs.len() + 256);
        for &(code, ansi_code) in &color_pairs {
            codes.insert(code.to_string(), ansi_code.to_string());
        }

        for i in 0..256 {
            codes.insert(format!("@x{:03}", i), format!("\x1b[38;5;{}m", i));
        }

        ColorCodes { codes }
    }

    pub fn utilprint(&self, text: &str) -> Result<(), String> {
        let mut output = String::with_capacity(text.len() * 2);
        let mut current_index = 0;

        while let Some(start) = text[current_index..].find(|c| c == '@' || c == '#') {
            let (end, code) = self.get_code(&text[current_index + start..]);
            write!(
                output,
                "{}{}",
                &text[current_index..current_index + start],
                code
            )
            .map_err(|e| e.to_string())?;
            current_index += start + end;
        }

        write!(output, "{}\x1b[0m", &text[current_index..]).map_err(|e| e.to_string())?;
        println!("{}", output);
        Ok(())
    }

    fn get_code(&self, text: &str) -> (usize, String) {
        if let Some(color) = self.codes.get(&text[..2]) {
            return (2, color.clone());
        }
        if text.starts_with("@@") || text.starts_with("##") {
            return (2, text[1..2].to_string());
        }

        if text.starts_with("@x") && text.len() >= 5 {
            return (5, self.codes.get(&text[..5]).cloned().unwrap_or_default());
        }
        if text.starts_with('#') && text.len() >= 5 {
            return (
                5,
                parse_unicode_code(&text[1..5]).map_or_else(String::new, |c| c.to_string()),
            );
        }
        (2, String::new())
    }
}

fn parse_unicode_code(code: &str) -> Option<char> {
    u32::from_str_radix(code, 16).ok().and_then(char::from_u32)
}

pub fn utilprint(text: impl AsRef<str>) {
    if let Err(e) = COLOR_CODES.utilprint(text.as_ref()) {
        eprintln!("Error printing text: {}", e);
    }
}

fn preprocess_text(s: &str) -> String {
    let mut result = String::new();
    let mut current_index = 0;

    while let Some(start) = s[current_index..].find(|c| c == '@' || c == '#') {
        result.push_str(&s[current_index..current_index + start]);

        let (end, code_type) = COLOR_CODES.get_code(&s[current_index + start..]);

        match code_type {
            _ if code_type.is_empty() => (),
            _ if code_type.starts_with("\x1b") => (),
            _ => result.push(' '),
        }

        current_index += start + end;
    }

    result.push_str(&s[current_index..]);
    result
}

fn visible_length(s: &str) -> usize {
    preprocess_text(s).chars().count()
}

pub fn print_help() {
    let help_text = vec![
        "                  UTILPRINT HELP              ",
        "",
        " Standard Colors:                             ",
        " @d@@d - @dBlack@u         @r@@r - @rRed@u",
        " @y@@y - @yYellow@u        @b@@b - @bBlue@u ",
        " @c@@c - @cCyan@u          @w@@w - @wWhite@u  ",
        " @g@@g - @gGreen@u         @m@@m - @mMagenta@u",
        "",
        " Bright Colors:                               ",
        " @D@@D - @DGray@u          @R@@R - @RBright Red@u",
        " @G@@G - @GBright Green@u  @Y@@Y - @YBright Yellow@u ",
        " @B@@B - @BBright Blue@u   @M@@M - @MBright Magenta@u ",
        " @C@@C - @CBright Cyan@u   @W@@W - @WBright White@u ",
        "",
        " Xterm 256 Colors:                            ",
        " Use @@xNNN where NNN is the color number.    ",
        " Example: @@x124 for a deep red.              ",
        "",
        " Unicode Characters:                          ",
        " Use ##NNNN where NNNN is the Unicode code.    ",
        " Example: ##263A for a smiley face. #263A  ",
    ];

    let max_visible_length = help_text
        .iter()
        .map(|line| visible_length(line))
        .max()
        .unwrap_or(0);

    let horizontal_border: String = "".repeat(max_visible_length + 2);
    println!("{}", horizontal_border);
    for line in help_text.iter() {
        let visible_line_length = visible_length(&preprocess_text(line));
        let padding = max_visible_length - visible_line_length;
        let formatted_line = format!("{}{}", line, " ".repeat(padding));
        utilprint(&formatted_line);
    }
    println!("{}", horizontal_border);
}

pub trait TextEffects {
    fn pastelbow(&self) -> String;
    fn rainbow(&self) -> String;
    fn lover(&self) -> String;
    fn red(&self) -> String;
}

impl TextEffects for &str {
    fn pastelbow(&self) -> String {
        let colors = vec![
            "@x226", "@x190", "@x155", "@x119", "@x120", "@x084", "@x085", "@x050", "@x051",
            "@x045", "@x075", "@x069", "@x105", "@x099", "@x135", "@x165", "@x201", "@x200",
            "@x205", "@x204", "@x210", "@x209", "@x215", "@x220", "@x226",
        ];
        let mut result = String::new();
        for (i, c) in self.chars().enumerate() {
            let color = &colors[i % colors.len()];
            result.push_str(&format!("{}{}", color, c));
        }
        result.push_str("@u");
        result
    }

    fn rainbow(&self) -> String {
        let colors = vec![
            "@x196", "@x202", "@x208", "@x214", "@x220", "@x226", "@x154", "@x082", "@x046",
            "@x035", "@x025", "@x021", "@x020", "@x019", "@x054", "@x055", "@x091", "@x092",
            "@x126", "@x161",
        ];
        let mut result = String::new();
        for (i, c) in self.chars().enumerate() {
            let color = &colors[i % colors.len()];
            result.push_str(&format!("{}{}", color, c));
        }
        result.push_str("@u");
        result
    }

    fn lover(&self) -> String {
        let colors = vec!["@x218", "@x176", "@x229", "@x219", "@x153", "@x219"];
        let mut result = String::new();
        for (i, c) in self.chars().enumerate() {
            let color = &colors[i % colors.len()];
            result.push_str(&format!("{}{}", color, c));
        }
        result.push_str("@u");
        result
    }

    fn red(&self) -> String {
        format!("@r{}@u", self)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_utilprint() {
        let text = "@rHello, @bworld!";
        assert_eq!(COLOR_CODES.utilprint(text), Ok(()));
    }

    #[test]
    fn test_parse_unicode_code() {
        assert_eq!(parse_unicode_code("263A"), Some(''));
        assert_eq!(parse_unicode_code("1F602"), Some('😂'));
    }

    #[test]
    fn test_pastelbow() {
        let text = "Hello".pastelbow();
        assert!(text.starts_with("@x226H"));
        assert!(text.contains("@x190e"));
        assert!(text.contains("@x155l"));
        assert!(text.contains("@x119o"));
    }

    #[test]
    fn test_rainbow() {
        let text = "Hello World".rainbow();
        assert!(text.contains("@x196H"));
        assert!(text.contains("@x202e"));
        assert!(text.contains("@x208l"));
        assert!(text.contains("@x214l"));
        assert!(text.contains("@x220o"));
        assert!(text.contains("@x226 "));
        assert!(text.contains("@x154W"));
        assert!(text.contains("@x082o"));
        assert!(text.contains("@x046r"));
        assert!(text.contains("@x035l"));
        assert!(text.contains("@x025d"));
    }

    #[test]
    fn test_lover() {
        let text = "Hello".lover();
        assert!(text.contains("@x218H"));
        assert!(text.contains("@x176e"));
        assert!(text.contains("@x229l"));
        assert!(text.contains("@x219l"));
        assert!(text.contains("@x153o"));
    }
}