egui_sgr 0.3.0

A Rust library that converts ASCII/ANSI escape sequence color models into colored text in egui
Documentation
use crate::{AnsiColor, AnsiIntensity, AnsiStyle, UnderlineStyle};
use vte::Params;

const MAX_SGR_PARAMS: usize = 32;
const EMPTY_PARAM: &[u16] = &[];

pub(crate) fn apply_sgr(params: &Params, style: &mut AnsiStyle) {
    let params = SgrParams::new(params);

    if params.is_empty() {
        style.reset();
        return;
    }

    let mut i = 0;
    while i < params.len() {
        let param = params.get(i);
        let code = param.first().copied().unwrap_or(0);

        if param.len() > 1 {
            match code {
                4 => {
                    style.underline = underline_from_subparam(param.get(1).copied().unwrap_or(1));
                }
                38 | 48 | 58 => {
                    if let Some(color) = extended_color_from_subparams(&param[1..]) {
                        apply_extended_color(style, code, color);
                    }
                }
                _ => apply_simple_sgr(style, code),
            }
            i += 1;
            continue;
        }

        match code {
            38 | 48 | 58 => {
                let consumed = apply_semicolon_extended_color(&params, i, style, code);
                i += consumed.max(1);
            }
            _ => {
                apply_simple_sgr(style, code);
                i += 1;
            }
        }
    }
}

fn apply_simple_sgr(style: &mut AnsiStyle, code: u16) {
    match code {
        0 => style.reset(),
        1 => style.intensity = AnsiIntensity::Bold,
        2 => style.intensity = AnsiIntensity::Faint,
        3 => style.italic = true,
        4 => style.underline = UnderlineStyle::Single,
        7 => style.reverse = true,
        8 => style.hidden = true,
        9 => style.strikethrough = true,
        21 => style.underline = UnderlineStyle::Double,
        22 => style.intensity = AnsiIntensity::Normal,
        23 => style.italic = false,
        24 => style.underline = UnderlineStyle::None,
        27 => style.reverse = false,
        28 => style.hidden = false,
        29 => style.strikethrough = false,
        30..=37 => style.foreground = AnsiColor::Indexed((code - 30) as u8),
        40..=47 => style.background = AnsiColor::Indexed((code - 40) as u8),
        90..=97 => style.foreground = AnsiColor::Indexed((code - 90 + 8) as u8),
        100..=107 => style.background = AnsiColor::Indexed((code - 100 + 8) as u8),
        39 => style.foreground = AnsiColor::Default,
        49 => style.background = AnsiColor::Default,
        59 => style.underline_color = None,
        _ => {}
    }
}

fn apply_semicolon_extended_color(
    params: &SgrParams<'_>,
    index: usize,
    style: &mut AnsiStyle,
    target: u16,
) -> usize {
    let Some(mode) = params.first(index + 1) else {
        return 1;
    };

    match mode {
        5 => {
            if let Some(color_code) = params
                .first(index + 2)
                .and_then(|value| u8::try_from(value).ok())
            {
                apply_extended_color(style, target, AnsiColor::Indexed(color_code));
                3
            } else {
                1
            }
        }
        2 => {
            let rgb = [
                params.first(index + 2),
                params.first(index + 3),
                params.first(index + 4),
            ];

            if let [Some(r), Some(g), Some(b)] = rgb
                && let Some(color) = rgb_color(r, g, b)
            {
                apply_extended_color(style, target, color);
                return 5;
            }

            1
        }
        _ => 1,
    }
}

struct SgrParams<'a> {
    groups: [&'a [u16]; MAX_SGR_PARAMS],
    len: usize,
}

impl<'a> SgrParams<'a> {
    fn new(params: &'a Params) -> Self {
        let mut groups = [EMPTY_PARAM; MAX_SGR_PARAMS];
        let mut len = 0;

        for param in params {
            groups[len] = if param.is_empty() { EMPTY_PARAM } else { param };
            len += 1;
        }

        Self { groups, len }
    }

    fn is_empty(&self) -> bool {
        self.len == 0
    }

    fn len(&self) -> usize {
        self.len
    }

    fn get(&self, index: usize) -> &'a [u16] {
        self.groups.get(index).copied().unwrap_or(EMPTY_PARAM)
    }

    fn first(&self, index: usize) -> Option<u16> {
        self.get(index).first().copied()
    }
}

fn extended_color_from_subparams(subparams: &[u16]) -> Option<AnsiColor> {
    let mode = subparams.first().copied()?;

    match mode {
        5 => subparams
            .get(1)
            .and_then(|value| u8::try_from(*value).ok())
            .map(AnsiColor::Indexed),
        2 => {
            if subparams.len() < 4 {
                return None;
            }

            let rgb_start = subparams.len() - 3;
            rgb_color(
                subparams[rgb_start],
                subparams[rgb_start + 1],
                subparams[rgb_start + 2],
            )
        }
        _ => None,
    }
}

fn rgb_color(r: u16, g: u16, b: u16) -> Option<AnsiColor> {
    Some(AnsiColor::Rgb(
        u8::try_from(r).ok()?,
        u8::try_from(g).ok()?,
        u8::try_from(b).ok()?,
    ))
}

fn apply_extended_color(style: &mut AnsiStyle, target: u16, color: AnsiColor) {
    match target {
        38 => style.foreground = color,
        48 => style.background = color,
        58 => style.underline_color = Some(color),
        _ => {}
    }
}

fn underline_from_subparam(value: u16) -> UnderlineStyle {
    match value {
        0 => UnderlineStyle::None,
        1 => UnderlineStyle::Single,
        2 => UnderlineStyle::Double,
        3 => UnderlineStyle::Curly,
        4 => UnderlineStyle::Dotted,
        5 => UnderlineStyle::Dashed,
        _ => UnderlineStyle::Single,
    }
}