Files
Ely-GPUI-Components/src/terminal/ansi.rs
T

181 lines
5.7 KiB
Rust

use std::ops::Range;
use alacritty_terminal::{
term::color::Colors,
vte::ansi::{Attr, Color, Handler, Processor},
};
use gpui::{
App, FontStyle, FontWeight, HighlightStyle, IntoElement, ParentElement, RenderOnce,
SharedString, StrikethroughStyle, Styled, StyledText, UnderlineStyle, Window, div,
};
use super::colors::Ink;
use crate::theme::{ActiveTheme, TextSize};
/// How text prints after the escape codes so far.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Pen {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub strike: bool,
pub inverse: bool,
}
/// Collects the printed text and the pen each stretch of it took.
#[derive(Default)]
struct Printed {
text: String,
spans: Vec<(Range<usize>, Pen)>,
pen: Pen,
}
impl Handler for Printed {
fn input(&mut self, ch: char) {
let start = self.text.len();
self.text.push(ch);
let end = self.text.len();
match self.spans.last_mut() {
Some((range, pen)) if range.end == start && *pen == self.pen => range.end = end,
_ if self.pen == Pen::default() => {}
_ => self.spans.push((start..end, self.pen)),
}
}
fn linefeed(&mut self) {
self.text.push('\n');
}
fn put_tab(&mut self, count: u16) {
self.text.extend(std::iter::repeat_n('\t', count as usize));
}
fn terminal_attribute(&mut self, attr: Attr) {
let pen = &mut self.pen;
match attr {
Attr::Reset => *pen = Pen::default(),
Attr::Bold => pen.bold = true,
Attr::Dim => pen.dim = true,
Attr::Italic => pen.italic = true,
Attr::Underline
| Attr::DoubleUnderline
| Attr::Undercurl
| Attr::DottedUnderline
| Attr::DashedUnderline => pen.underline = true,
Attr::Reverse => pen.inverse = true,
Attr::Strike => pen.strike = true,
Attr::CancelBold => pen.bold = false,
Attr::CancelBoldDim => (pen.bold, pen.dim) = (false, false),
Attr::CancelItalic => pen.italic = false,
Attr::CancelUnderline => pen.underline = false,
Attr::CancelReverse => pen.inverse = false,
Attr::CancelStrike => pen.strike = false,
Attr::Foreground(color) => pen.fg = Some(color),
Attr::Background(color) => pen.bg = Some(color),
Attr::BlinkSlow
| Attr::BlinkFast
| Attr::CancelBlink
| Attr::Hidden
| Attr::CancelHidden
| Attr::UnderlineColor(_) => {}
}
}
}
/// The text `source` prints, carriage returns and other codes gone, and the pens that are not plain.
pub(crate) fn printed(source: &str) -> (String, Vec<(Range<usize>, Pen)>) {
let mut printed = Printed::default();
let mut parser: Processor = Processor::new();
parser.advance(&mut printed, source.as_bytes());
(printed.text, printed.spans)
}
impl Pen {
fn style(&self, ink: &Ink, rule: gpui::Pixels) -> HighlightStyle {
let set = Colors::default();
let paint =
|color: Option<Color>, plain| color.map_or(plain, |color| ink.resolve(color, &set));
let (mut fg, mut bg) = (paint(self.fg, ink.fg), paint(self.bg, ink.bg));
if self.inverse {
std::mem::swap(&mut fg, &mut bg);
}
if self.dim {
fg = fg.opacity(0.7);
}
HighlightStyle {
color: Some(fg),
background_color: (self.bg.is_some() || self.inverse).then_some(bg),
font_weight: self.bold.then_some(FontWeight::BOLD),
font_style: self.italic.then_some(FontStyle::Italic),
underline: self.underline.then_some(UnderlineStyle {
thickness: rule,
color: Some(fg),
wavy: false,
}),
strikethrough: self.strike.then_some(StrikethroughStyle {
thickness: rule,
color: Some(fg),
}),
..HighlightStyle::default()
}
}
}
/// Text colored by the escape codes in it, as a terminal would print it; other codes drop away.
#[derive(IntoElement)]
pub struct AnsiText {
source: SharedString,
}
impl AnsiText {
pub fn new(source: impl Into<SharedString>) -> Self {
Self {
source: source.into(),
}
}
}
impl RenderOnce for AnsiText {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let ink = Ink::new(&theme.colors);
let (text, spans) = printed(&self.source);
let rule = theme.underline_thickness();
let styles: Vec<(Range<usize>, HighlightStyle)> = spans
.into_iter()
.map(|(range, pen)| (range, pen.style(&ink, rule)))
.collect();
div()
.font_family(theme.mono_family.clone())
.text_size(theme.text_size(TextSize::Sm))
.text_color(ink.fg)
.child(StyledText::new(text).with_highlights(styles))
}
}
#[cfg(test)]
mod tests {
use alacritty_terminal::vte::ansi::NamedColor;
use super::*;
#[test]
fn codes_become_pens_and_drop_out_of_the_text() {
let (text, spans) = printed("\x1b[1;31merror\x1b[0m: bad\r\n\x1b[4mnext\x1b[24m ok");
assert_eq!(text, "error: bad\nnext ok");
let red = Pen {
fg: Some(Color::Named(NamedColor::Red)),
bold: true,
..Pen::default()
};
let under = Pen {
underline: true,
..Pen::default()
};
assert_eq!(spans, [(0..5, red), (11..15, under)]);
}
}