//! What a tool printed, with its terminal styling applied and everything //! else taken out. Ported from `app/.../Ansi.kt`, module for module: the //! Kotlin version builds a Compose `AnnotatedString`, which does not exist //! here, so a [`StyledText`] of plain text plus non-overlapping //! `(Range, Style)` spans stands in for it -- a future UI layer maps //! [`Style`] onto whatever it draws with. //! //! Bash output arrives exactly as the program wrote it, escape sequences //! included, and drawn verbatim those are line noise in the middle of the //! thing being read. Stripping them all would be the other half-answer -- //! colour is often the whole of what a diff or a test run is saying. //! //! So the sequences that decide how text *looks* become spans, and every //! other one is dropped rather than shown: the rest move a cursor around a //! grid this is not, and "go to column 40" has no meaning in a scrolling //! document. //! //! A carriage return is honoured the way a terminal honours it: what was //! written since the last line break is thrown away and the line starts //! again. That is what makes a progress bar show its final state rather //! than every state it passed through. use std::ops::Range; /// An RGB colour, the same shape wherever this crate names one -- no alpha, /// because the one place that needs partial transparency (dimming) says so /// with a separate flag rather than baking it into the colour. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Rgb { pub r: u8, pub g: u8, pub b: u8, } impl Rgb { pub const fn new(r: u8, g: u8, b: u8) -> Self { Self { r, g, b } } } /// The sixteen colours a terminal program names, and the two it assumes. /// /// Its own palette rather than the syntax one: a program that prints in red /// has chosen red, where a highlighter's colours are this app's reading of /// somebody else's code. #[derive(Debug, Clone)] pub struct AnsiPalette { /// Indexes 0-7, then 8-15 bright, in the terminal's own order. pub colours: [Rgb; 16], /// What uncoloured text is, needed only where a style has to state a colour. pub foreground: Rgb, /// What the text sits on, needed for reverse video. pub background: Rgb, } /// One span's worth of styling. `None` fields mean "unspecified", the same /// meaning `Color.Unspecified` and a null `FontWeight` carried in the Kotlin. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Style { pub color: Option, /// How much of `color`'s alpha survives, 0.0-1.0; `None` is opaque. pub alpha: Option, pub background: Option, pub bold: bool, pub italic: bool, pub underline: bool, pub strikethrough: bool, } /// Plain text plus the non-overlapping, ordered spans that style parts of it /// -- this crate's stand-in for Compose's `AnnotatedString`. #[derive(Debug, Clone, PartialEq, Default)] pub struct StyledText { pub text: String, pub spans: Vec<(Range, Style)>, } impl StyledText { fn plain(text: String) -> Self { Self { text, spans: Vec::new(), } } } const ESC: char = '\u{1B}'; const BELL: char = '\u{7}'; /// [text] with its terminal styling applied and everything else taken out; /// see the module doc. pub fn ansi_styled(text: &str, palette: &AnsiPalette) -> StyledText { // The common case by a long way -- nothing to do, and nothing allocated // to find that out. if !text.contains(ESC) && !text.contains('\r') { return StyledText::plain(text.to_string()); } let chars: Vec = text.chars().collect(); let mut runs: Vec<(String, Option