egui_sgr 0.3.0

A Rust library that converts ASCII/ANSI escape sequence color models into colored text in egui
Documentation
use eframe::{App, Frame, egui};
use egui_sgr::{AnsiColor, AnsiSpanBuffer, EguiAnsiTheme};

const CHUNKS: &[&str] = &[
    "\x1b[32mstream ",
    "keeps ",
    "green\x1b[0m ",
    "\x1b[38;5;208mthen orange\x1b[0m",
];

struct StreamingExample {
    buffer: AnsiSpanBuffer,
    next_chunk: usize,
}

impl Default for StreamingExample {
    fn default() -> Self {
        let mut example = Self {
            buffer: AnsiSpanBuffer::new(),
            next_chunk: 0,
        };
        example.push_next_chunk();
        example.push_next_chunk();
        example
    }
}

impl App for StreamingExample {
    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut Frame) {
        ui.heading("AnsiSpanBuffer");

        ui.horizontal(|ui| {
            if ui.button("Push").clicked() {
                self.push_next_chunk();
            }
            if ui.button("Finish").clicked() {
                self.buffer.finish();
            }
            if ui.button("Reset").clicked() {
                self.buffer.clear();
                self.next_chunk = 0;
            }
        });

        ui.horizontal_wrapped(|ui| {
            for (index, chunk) in CHUNKS.iter().enumerate() {
                let text = display_ansi_sequence(chunk);
                if index < self.next_chunk {
                    ui.monospace(text);
                } else {
                    ui.label(egui::RichText::new(text).monospace().weak());
                }
            }
        });

        ui.separator();

        let theme = EguiAnsiTheme::default();
        ui.label(self.buffer.to_layout_job(&theme));

        ui.add_space(8.0);
        egui::Grid::new("streamed_spans")
            .num_columns(2)
            .striped(true)
            .spacing([16.0, 4.0])
            .show(ui, |ui| {
                ui.strong("Color");
                ui.strong("Text");
                ui.end_row();

                for span in self.buffer.spans() {
                    ui.monospace(color_label(span.style.foreground));
                    ui.monospace(&span.text);
                    ui.end_row();
                }
            });
    }
}

impl StreamingExample {
    fn push_next_chunk(&mut self) {
        if let Some(chunk) = CHUNKS.get(self.next_chunk) {
            self.buffer.push_str(chunk);
            self.next_chunk += 1;
        }
    }
}

fn display_ansi_sequence(input: &str) -> String {
    input.replace('\x1b', "\\x1b")
}

fn color_label(color: AnsiColor) -> String {
    match color {
        AnsiColor::Default => "default".to_owned(),
        AnsiColor::Indexed(index) => format!("idx {index}"),
        AnsiColor::Rgb(r, g, b) => format!("rgb({r},{g},{b})"),
    }
}

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([560.0, 340.0]),
        ..Default::default()
    };

    eframe::run_native(
        "egui_sgr streaming",
        options,
        Box::new(|_cc| Ok(Box::new(StreamingExample::default()))),
    )
}