summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2020-08-03 09:46:56 -0400
committerDan Davison <dandavison7@gmail.com>2020-08-14 10:14:54 -0400
commit0a9c48c75051fb507ec1a801ca9d0cf96fadbc48 (patch)
treee1e0b136b7254b962d62f19dc9e87658f82400dc
parent5ff4e13f10b80574f15db6968086f1c45fd1860a (diff)
New ANSI escape sequence parser based on vte
Reimplement utility functions from `console` crate, but with support for OSC sequences.
-rw-r--r--src/ansi/console_tests.rs75
-rw-r--r--src/ansi/iterator.rs487
-rw-r--r--src/ansi/mod.rs184
-rw-r--r--src/ansi/parse.rs205
-rw-r--r--src/paint.rs4
-rw-r--r--src/style.rs24
6 files changed, 738 insertions, 241 deletions
diff --git a/src/ansi/console_tests.rs b/src/ansi/console_tests.rs
new file mode 100644
index 00000000..d6372316
--- /dev/null
+++ b/src/ansi/console_tests.rs
@@ -0,0 +1,75 @@
+// This file contains some unit tests copied from the `console` project:
+// https://github.com/mitsuhiko/console
+//
+// The MIT License (MIT)
+
+// Copyright (c) 2017 Armin Ronacher <armin.ronacher@active-4.com>
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+#[cfg(test)]
+mod tests {
+ use console::{self, style};
+
+ use crate::ansi::{measure_text_width, truncate_str};
+
+ #[test]
+ fn test_text_width() {
+ let s = style("foo")
+ .red()
+ .on_black()
+ .bold()
+ .force_styling(true)
+ .to_string();
+ assert_eq!(measure_text_width(&s), 3);
+ }
+
+ #[test]
+ fn test_truncate_str() {
+ let s = format!("foo {}", style("bar").red().force_styling(true));
+ assert_eq!(
+ &truncate_str(&s, 5, ""),
+ &format!("foo {}", style("b").red().force_styling(true))
+ );
+ let s = format!("foo {}", style("bar").red().force_styling(true));
+ // DED: I'm changing this test assertion: delta does not move `!` inside the styled region.
+ // assert_eq!(
+ // &truncate_str(&s, 5, "!"),
+ // &format!("foo {}", style("!").red().force_styling(true))
+ // );
+ assert_eq!(
+ &truncate_str(&s, 5, "!"),
+ &format!("foo {}!", style("").red().force_styling(true))
+ );
+ let s = format!("foo {} baz", style("bar").red().force_styling(true));
+ assert_eq!(
+ &truncate_str(&s, 10, "..."),
+ &format!("foo {}...", style("bar").red().force_styling(true))
+ );
+ let s = format!("foo {}", style("バー").red().force_styling(true));
+ assert_eq!(
+ &truncate_str(&s, 5, ""),
+ &format!("foo {}", style("").red().force_styling(true))
+ );
+ let s = format!("foo {}", style("バー").red().force_styling(true));
+ assert_eq!(
+ &truncate_str(&s, 6, ""),
+ &format!("foo {}", style("バ").red().force_styling(true))
+ );
+ }
+
+ #[test]
+ fn test_truncate_str_no_ansi() {
+ assert_eq!(&truncate_str("foo bar", 5, ""), "foo b");
+ assert_eq!(&truncate_str("foo bar", 5, "!"), "foo !");
+ assert_eq!(&truncate_str("foo bar baz", 10, "..."), "foo bar...");
+ }
+}
diff --git a/src/ansi/iterator.rs b/src/ansi/iterator.rs
new file mode 100644
index 00000000..44fe49fb
--- /dev/null
+++ b/src/ansi/iterator.rs
@@ -0,0 +1,487 @@
+use core::str::Bytes;
+
+use ansi_term;
+use vte;
+
+pub struct AnsiElementIterator<'a> {
+ // The input bytes
+ bytes: Bytes<'a>,
+
+ // The state machine
+ machine: vte::Parser,
+
+ // Becomes non-None when the parser finishes parsing an ANSI sequence.
+ // This is never Element::Text.
+ element: Option<Element>,
+
+ // Number of text bytes seen since the last element was emitted.
+ text_length: usize,
+
+ // Byte offset of start of current element.
+ start: usize,
+
+ // Byte offset of most rightward byte processed so far
+ pos: usize,
+}
+
+struct Performer {
+ // Becomes non-None when the parser finishes parsing an ANSI sequence.
+ // This is never Element::Text.
+ element: Option<Element>,
+
+ // Number of text bytes seen since the last element was emitted.
+ text_length: usize,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Element {
+ CSI(ansi_term::Style, usize, usize),
+ ESC(usize, usize),
+ OSC(usize, usize),
+ Text(usize, usize),
+}
+
+impl<'a> AnsiElementIterator<'a> {
+ pub fn new(s: &'a str) -> Self {
+ Self {
+ machine: vte::Parser::new(),
+ bytes: s.bytes(),
+ element: None,
+ text_length: 0,
+ start: 0,
+ pos: 0,
+ }
+ }
+
+ #[allow(dead_code)]
+ pub fn to_vec(self) -> Vec<Element> {
+ self.collect()
+ }
+
+ #[allow(dead_code)]
+ pub fn dbg(s: &str) {
+ for el in AnsiElementIterator::new(s) {
+ match el {
+ Element::CSI(_, i, j) => println!("CSI({}, {}, {:?})", i, j, &s[i..j]),
+ Element::ESC(i, j) => println!("ESC({}, {}, {:?})", i, j, &s[i..j]),
+ Element::OSC(i, j) => println!("OSC({}, {}, {:?})", i, j, &s[i..j]),
+ Element::Text(i, j) => println!("Text({}, {}, {:?})", i, j, &s[i..j]),
+ }
+ }
+ }
+}
+
+impl<'a> Iterator for AnsiElementIterator<'a> {
+ type Item = Element;
+
+ fn next(&mut self) -> Option<Element> {
+ loop {
+ // If the last element emitted was text, then there may be a non-text element waiting
+ // to be emitted. In that case we do not consume a new byte.
+ let byte = if self.element.is_some() {
+ None
+ } else {
+ self.bytes.next()
+ };
+ if byte.is_some() || self.element.is_some() {
+ if let Some(byte) = byte {
+ let mut performer = Performer {
+ element: None,
+ text_length: 0,
+ };
+ self.machine.advance(&mut performer, byte);
+ self.element = performer.element;
+ self.text_length += performer.text_length;
+ self.pos += 1;
+ }
+ if self.element.is_some() {
+ // There is a non-text element waiting to be emitted, but it may have preceding
+ // text, which must be emitted first.
+ if self.text_length > 0 {
+ let start = self.start;
+ self.start += self.text_length;
+ self.text_length = 0;
+ return Some(Element::Text(start, self.start));
+ }
+ let start = self.start;
+ self.start = self.pos;
+ let element = match self.element.as_ref().unwrap() {
+ Element::CSI(style, _, _) => Element::CSI(*style, start, self.pos),
+ Element::ESC(_, _) => Element::ESC(start, self.pos),
+ Element::OSC(_, _) => Element::OSC(start, self.pos),
+ Element::Text(_, _) => unreachable!(),
+ };
+ self.element = None;
+ return Some(element);
+ }
+ } else if self.text_length > 0 {
+ self.text_length = 0;
+ return Some(Element::Text(self.start, self.pos));
+ } else {
+ return None;
+ }
+ }
+ }
+}
+
+// Based on https://github.com/alacritty/vte/blob/0310be12d3007e32be614c5df94653d29fcc1a8b/examples/parselog.rs
+impl vte::Perform for Performer {
+ fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) {
+ if ignore || intermediates.len() > 1 {
+ return;
+ }
+
+ match (c, intermediates.get(0)) {
+ ('m', None) => {
+ if params.is_empty() {
+ // Attr::Reset;
+ } else {
+ self.element = Some(Element::CSI(
+ ansi_term_style_from_sgr_parameters(params),
+ 0,
+ 0,
+ ));
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn print(&mut self, c: char) {
+ self.text_length += c.len_utf8();
+ }
+
+ fn execute(&mut self, byte: u8) {
+ // E.g. '\n'
+ if byte < 128 {
+ self.text_length += 1;
+ }
+ }
+
+ fn hook(&mut self, _params: &[i64], _intermediates: &[u8], _ignore: bool, _c: char) {}
+
+ fn put(&mut self, _byte: u8) {}
+
+ fn unhook(&mut self) {}
+
+ fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) {
+ self.element = Some(Element::OSC(0, 0));
+ }
+
+ fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {
+ self.element = Some(Element::ESC(0, 0));
+ }
+}
+
+// Based on https://github.com/alacritty/alacritty/blob/57c4ac9145a20fb1ae9a21102503458d3da06c7b/alacritty_terminal/src/ansi.rs#L1168
+fn ansi_term_style_from_sgr_parameters(parameters: &[i64]) -> ansi_term::Style {
+ let mut i = 0;
+ let mut style = ansi_term::Style::new();
+ loop {
+ if i >= parameters.len() {
+ break;
+ }
+
+ match parameters[i] {
+ // 0 => Some(Attr::Reset),
+ 1 => style.is_bold = true,
+ 2 => style.is_dimmed = true,
+ 3 => style.is_italic = true,
+ 4 => style.is_underline = true,
+ 5 => style.is_blink = true, // blink slow
+ 6 => style.is_blink = true, // blink fast
+ 7 => style.is_reverse = true,
+ 8 => style.is_hidden = true,
+ 9 => style.is_strikethrough = true,
+ // 21 => Some(Attr::CancelBold),
+ // 22 => Some(Attr::CancelBoldDim),
+ // 23 => Some(Attr::CancelItalic),
+ // 24 => Some(Attr::CancelUnderline),
+ // 25 => Some(Attr::CancelBlink),
+ // 27 => Some(Attr::CancelReverse),
+ // 28 => Some(Attr::CancelHidden),
+ // 29 => Some(Attr::CancelStrike),
+ 30 => style.foreground = Some(ansi_term::Color::Black),
+ 31 => style.foreground = Some(ansi_term::Color::Red),
+ 32 => style.foreground = Some(ansi_term::Color::Green),
+ 33 => style.foreground = Some(ansi_term::Color::Yellow),
+ 34 => style.foreground = Some(ansi_term::Color::Blue),
+ 35 => style.foreground = Some(ansi_term::Color::Purple),
+ 36 => style.foreground = Some(ansi_term::Color::Cyan),
+ 37 => style.foreground = Some(ansi_term::Color::White),
+ 38 => {
+ let mut start = 0;
+ if let Some(color) = parse_sgr_color(&parameters[i..], &mut start) {
+ i += start;
+ style.foreground = Some(color);
+ }
+ }
+ // 39 => Some(Attr::Foreground(Color::Named(NamedColor::Foreground))),
+ 40 => style.background = Some(ansi_term::Color::Black),
+ 41 => style.background = Some(ansi_term::Color::Red),
+ 42 => style.background = Some(ansi_term::Color::Green),
+ 43 => style.background = Some(ansi_term::Color::Yellow),
+ 44 => style.background = Some(ansi_term::Color::Blue),
+ 45 => style.background = Some(ansi_term::Color::Purple),
+ 46 => style.background = Some(ansi_term::Color::Cyan),
+ 47 => style.background = Some(ansi_term::Color::White),
+ 48 => {
+ let mut start = 0;
+ if let Some(color) = parse_sgr_color(&parameters[i..], &mut start) {
+ i += start;
+ style.background = Some(color);
+ }
+ }
+ // 49 => Some(Attr::Background(Color::Named(NamedColor::Background))),
+ // "bright" colors. ansi_term doesn't offer a way to emit them as, e.g., 90m; instead
+ // that would be 38;5;8.
+ 90 => style.foreground = Some(ansi_term::Color::Fixed(8)),
+ 91 => style.foreground = Some(ansi_term::Color::Fixed(9)),
+ 92 => style.foreground = Some(ansi_term::Color::Fixed(10)),
+ 93 => style.foreground = Some(ansi_term::Color::Fixed(11)),
+ 94 => style.foreground = Some(ansi_term::Color::Fixed(12)),
+ 95 => style.foreground = Some(ansi_term::Color::Fixed(13)),
+ 96 => style.foreground = Some(ansi_term::Color::Fixed(14)),
+ 97 => style.foreground = Some(ansi_term::Color::Fixed(15)),
+ 100 => style.background = Some(ansi_term::Color::Fixed(8)),
+ 101 => style.background = Some(ansi_term::Color::Fixed(9)),
+ 102 => style.background = Some(ansi_term::Color::Fixed(10)),
+ 103 => style.background = Some(ansi_term::Color::Fixed(11)),
+ 104 => style.background = Some(ansi_term::Color::Fixed(12)),
+ 105 => style.background = Some(ansi_term::Color::Fixed(13)),
+ 106 => style.background = Some(ansi_term::Color::Fixed(14)),
+ 107 => style.background = Some(ansi_term::Color::Fixed(15)),
+ _ => {}
+ };
+ i += 1;
+ }
+ style
+}
+
+// Based on https://github.com/alacritty/alacritty/blob/57c4ac9145a20fb1ae9a21102503458d3da06c7b/alacritty_terminal/src/ansi.rs#L1258
+fn parse_sgr_color(attrs: &[i64], i: &mut usize) -> Option<ansi_term::Color> {
+ if attrs.len() < 2 {
+ return None;
+ }
+
+ match attrs[*i + 1] {
+ 2 => {
+ // RGB color spec.
+ if attrs.len() < 5 {
+ // debug!("Expected RGB color spec; got {:?}", attrs);
+ return None;
+ }
+
+ let r = attrs[*i + 2];
+ let g = attrs[*i + 3];
+ let b = attrs[*i + 4];
+
+ *i += 4;
+
+ let range = 0..256;
+ if !range.contains(&r) || !range.contains(&g) || !range.contains(&b) {
+ // debug!("Invalid RGB color spec: ({}, {}, {})", r, g, b);
+ return None;
+ }
+
+ Some(ansi_term::Color::RGB(r as u8, g as u8, b as u8))
+ }
+ 5 => {
+ if attrs.len() < 3 {
+ // debug!("Expected color index; got {:?}", attrs);
+ None
+ } else {
+ *i += 2;
+ let idx = attrs[*i];
+ match idx {
+ 0..=255 => Some(ansi_term::Color::Fixed(idx as u8)),
+ _ => {
+ // debug!("Invalid color index: {}", idx);
+ None
+ }
+ }
+ }
+ }
+ _ => {
+ // debug!("Unexpected color attr: {}", attrs[*i + 1]);
+ None
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::{AnsiElementIterator, Element};
+ use crate::style;
+
+ #[test]
+ fn test_iterator_parse_git_style_strings() {
+ for (git_style_string, git_output) in &*style::tests::GIT_STYLE_STRING_EXAMPLES {
+ let mut it = AnsiElementIterator::new(git_output);
+
+ if *git_style_string == "normal" {
+ // This one has a different pattern
+ assert!(
+ matches!(it.next().unwrap(), Element::CSI(s, _, _) if s == ansi_term::Style::default())
+ );
+ assert!(
+ matches!(it.next().unwrap(), Element::Text(i, j) if &git_output[i..j] == "text")
+ );
+ assert!(
+ matches!(it.next().unwrap(), Element::CSI(s, _, _) if s == ansi_term::Style::default())
+ );
+ continue;
+ }
+
+ // First element should be a style
+ let element = it.next().unwrap();
+ match element {
+ Element::CSI(style, _, _) => assert!(style::ansi_term_style_equality(
+ style,
+ style::Style::from_git_str(git_style_string).ansi_term_style
+ )),
+ _ => assert!(false),
+ }
+
+ // Second element should be text: "+"
+ assert!(matches!(
+ it.next().unwrap(),
+ Element::Text(i, j) if &git_output[i..j] == "+"));
+
+ // Third element is the reset style
+ assert!(matches!(
+ it.next().unwrap(),
+ Element::CSI(s, _, _) if s == ansi_term::Style::default()));
+
+ // Fourth element should be a style
+ let element = it.next().unwrap();
+ match element {
+ Element::CSI(style, _, _) => assert!(style::ansi_term_style_equality(
+ style,
+ style::Style::from_git_str(git_style_string).ansi_term_style
+ )),
+ _ => assert!(false),
+ }
+
+ // Fifth element should be text: "text"
+ assert!(matches!(
+ it.next().unwrap(),
+ Element::Text(i, j) if &git_output[i..j] == "text"));
+
+ // Sixth element is the reset style
+ assert!(matches!(
+ it.next().unwrap(),
+ Element::CSI(s, _, _) if s == ansi_term::Style::default()));
+
+ assert!(matches!(
+ it.next().unwrap(),
+ Element::Text(i, j) if &git_output[i..j] == "\n"));
+
+ assert!(it.next().is_none());
+ }
+ }
+
+ #[test]
+ fn test_iterator_1() {
+ let minus_line = "\x1b[31m0123\x1b[m\n";
+ assert_eq!(
+ AnsiElementIterator::new(minus_line).to_vec(),
+ vec![
+ Element::CSI(
+ ansi_term::Style {
+ foreground: Some(ansi_term::Color::Red),
+ ..ansi_term::Style::default()
+ },
+ 0,
+ 5
+ ),
+ Element::Text(5, 9),
+ Element::CSI(ansi_term::Style::default(), 9, 12),
+ Element::Text(12, 13),
+ ]
+ );
+ assert_eq!("0123", &minus_line[5..9]);
+ assert_eq!("\n", &minus_line[12..13]);
+ }
+
+ #[test]
+ fn test_iterator_2() {
+ let minus_line = "\x1b[31m0123\x1b[m456\n";
+ assert_eq!(
+ AnsiElementIterator::new(minus_line).to_vec(),
+ vec![
+ Element::CSI(
+ ansi_term::Style {
+ foreground: Some(ansi_term::Color::Red),
+ ..ansi_term::Style::default()
+ },
+ 0,
+ 5
+ ),
+ Element::Text(5, 9),
+ Element::CSI(ansi_term::Style::default(), 9, 12),
+ Element::Text(12, 16),
+ ]
+ );
+ assert_eq!("0123", &minus_line[5..9]);
+ assert_eq!("456\n", &minus_line[12..16]);
+ }
+
+ #[test]
+ fn test_iterator_styled_non_ascii() {
+ let s = "\x1b[31mバー\x1b[0m";
+ assert_eq!(
+ AnsiElementIterator::new(s).to_vec(),
+ vec![
+ Element::CSI(
+ ansi_term::Style {
+ foreground: Some(ansi_term::Color::Red),
+ ..ansi_term::Style::default()
+ },
+ 0,
+ 5
+ ),
+ Element::Text(5, 11),
+ Element::CSI(ansi_term::Style::default(), 11, 15),
+ ]
+ );
+ assert_eq!("バー", &s[5..11]);
+ }
+
+ #[test]
+ fn test_iterator_osc_hyperlinks_styled_non_ascii() {
+ let s = "\x1b[38;5;4m\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b\\src/ansi/modバー.rs\x1b]8;;\x1b\\\x1b[0m\n";
+ assert_eq!(&s[0..9], "\x1b[38;5;4m");
+ assert_eq!(
+ &s[9..58],
+ "\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b"
+ );
+ assert_eq!(&s[58..59], "\\");
+ assert_eq!(&s[59..80], "src/ansi/modバー.rs");
+ assert_eq!(&s[80..86], "\x1b]8;;\x1b");
+ assert_eq!(&s[86..87], "\\");
+ assert_eq!(&s[87..91], "\x1b[0m");
+ assert_eq!(&s[91..92], "\n");
+ assert_eq!(
+ AnsiElementIterator::new(s).to_vec(),
+ vec![
+ Element::CSI(
+ ansi_term::Style {
+ foreground: Some(ansi_term::Color::Fixed(4)),
+ ..ansi_term::Style::default()
+ },
+ 0,
+ 9
+ ),
+ Element::OSC(9, 58),
+ Element::ESC(58, 59),
+ Element::Text(59, 80),
+ Element::OSC(80, 86),
+ Element::ESC(86, 87),
+ Element::CSI(ansi_term::Style::default(), 87, 91),
+ Element::Text(91, 92),
+ ]
+ );
+ }
+}
diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs
index 158b562c..e2fdaf7f 100644
--- a/src/ansi/mod.rs
+++ b/src/ansi/mod.rs
@@ -1,18 +1,79 @@
-pub mod parse;
+mod console_tests;
+mod iterator;
-use std::cmp::min;
+use std::borrow::Cow;
-use console;
use itertools::Itertools;
+use unicode_segmentation::UnicodeSegmentation;
+use unicode_width::UnicodeWidthStr;
+
+use iterator::{AnsiElementIterator, Element};
pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K";
pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K";
pub const ANSI_SGR_RESET: &str = "\x1b[0m";
-pub fn string_starts_with_ansi_escape_sequence(s: &str) -> bool {
- console::AnsiCodeIterator::new(s)
+pub fn strip_ansi_codes(s: &str) -> String {
+ strip_ansi_codes_from_strings_iterator(ansi_strings_iterator(s))
+}
+
+pub fn measure_text_width(s: &str) -> usize {
+ // TODO: how should e.g. '\n' be handled?
+ strip_ansi_codes(s).width()
+}
+
+/// Truncate string such that `tail` is present as a suffix, preceded by as much of `s` as can be
+/// displayed in the requested width.
+// Return string constructed as follows:
+// 1. `display_width` characters are available. If the string fits, return it.
+//
+// 2. Contribute graphemes and ANSI escape sequences from `tail` until either (1) `tail` is
+// exhausted, or (2) the display width of the result would exceed `display_width`.
+//
+// 3. If tail was exhausted, then contribute graphemes and ANSI escape sequences from `s` until the
+// display_width of the result would exceed `display_width`.
+pub fn truncate_str<'a, 'b>(s: &'a str, display_width: usize, tail: &'b str) -> Cow<'a, str> {
+ let items = ansi_strings_iterator(s).collect::<Vec<(&str, bool)>>();
+ let width = strip_ansi_codes_from_strings_iterator(items.iter().map(|el| *el)).width();
+ if width <= display_width {
+ return Cow::from(s);
+ }
+ let result_tail = if !tail.is_empty() {
+ truncate_str(tail, display_width, "").to_string()
+ } else {
+ String::new()
+ };
+ let mut used = measure_text_width(&result_tail);
+ let mut result = String::new();
+ for (t, is_ansi) in items {
+ if !is_ansi {
+ for g in t.graphemes(true) {
+ let w = g.width();
+ if used + w > display_width {
+ break;
+ }
+ result.push_str(g);
+ used += w;
+ }
+ } else {
+ result.push_str(t);
+ }
+ }
+
+ return Cow::from(format!("{}{}", result, result_tail));
+}
+
+pub fn parse_first_style(s: &str) -> Option<ansi_term::Style> {
+ AnsiElementIterator::new(s).find_map(|el| match el {
+ Element::CSI(style, _, _) => Some(style),
+ _ => None,
+ })
+}
+
+pub fn string_starts_with_ansi_style_sequence(s: &str) -> bool {
+ AnsiElementIterator::new(s)
.nth(0)
- .map(|(_, is_ansi)| is_ansi)
+ .map(|el| matches!(el, Element::CSI(_, _, _)))
.unwrap_or(false)
}
@@ -20,36 +81,111 @@ pub fn string_starts_with_ansi_escape_sequence(s: &str) -> bool {
/// counts bytes in non-ANSI-escape-sequence content only. All ANSI escape sequences in the
/// original string are preserved.
pub fn ansi_preserving_slice(s: &str, start: usize) -> String {
- console::AnsiCodeIterator::new(s)
- .scan(0, |i, (substring, is_ansi)| {
- // i is the index in non-ANSI-escape-sequence content.
- let substring_slice = if is_ansi || *i > start {
- substring
- } else {
- &substring[min(substring.len(), start - *i)..]
- };
- if !is_ansi {
- *i += substring.len();
- }
- Some(substring_slice)
+ AnsiElementIterator::new(s)
+ .scan(0, |index, element| {
+ // `index` is the index in non-ANSI-escape-sequence content.
+ Some(match element {
+ Element::CSI(_, a, b) => &s[a..b],
+ Element::ESC(a, b) => &s[a..b],
+ Element::OSC(a, b) => &s[a..b],
+ Element::Text(a, b) => {
+ let i = *index;
+ *index += b - a;
+ if *index <= start {
+ // This text segment ends before start, so contributes no bytes.
+ ""
+ } else if i > start {
+ // This section starts after `start`, so contributes all its bytes.
+ &s[a..b]
+ } else {
+ // This section contributes those bytes that are >= start
+ &s[(a + start - i)..b]
+ }
+ }
+ })
})
.join("")
}
+fn ansi_strings_iterator(s: &str) -> impl Iterator<Item = (&str, bool)> {
+ AnsiElementIterator::new(s).map(move |el| match el {
+ Element::CSI(_, i, j) => (&s[i..j], true),
+ Element::ESC(i, j) => (&s[i..j], true),
+ Element::OSC(i, j) => (&s[i..j], true),
+ Element::Text(i, j) => (&s[i..j], false),
+ })
+}
+
+fn strip_ansi_codes_from_strings_iterator<'a>(
+ strings: impl Iterator<Item = (&'a str, bool)>,
+) -> String {
+ strings
+ .filter_map(|(el, is_ansi)| if !is_ansi { Some(el) } else { None })
+ .join("")
+}
+
#[cfg(test)]
mod tests {
- use crate::ansi::ansi_preserving_slice;
- use crate::ansi::string_starts_with_ansi_escape_sequence;
+ use super::{
+ ansi_preserving_slice, measure_text_width, parse_first_style,
+ string_starts_with_ansi_style_sequence, strip_ansi_codes,
+ };
+
+ #[test]
+ fn test_strip_ansi_codes() {
+ for s in &["src/ansi/mod.rs", "バー", "src/ansi/modバー.rs"] {
+ assert_eq!(strip_ansi_codes(s), *s);
+ }
+ assert_eq!(strip_ansi_codes("\x1b[31mバー\x1b[0m"), "バー");
+ }
+
+ #[test]
+ fn test_measure_text_width() {
+ assert_eq!(measure_text_width("src/ansi/mod.rs"), 15);
+ assert_eq!(measure_text_width("バー"), 4);
+ assert_eq!(measure_text_width("src/ansi/modバー.rs"), 19);
+ assert_eq!(measure_text_width("\x1b[31mバー\x1b[0m"), 4);
+ assert_eq!(measure_text_width("a\nb\n"), 2);
+ }
+
+ #[test]
+ fn test_strip_ansi_codes_osc_hyperlink() {
+ assert_eq!(strip_ansi_codes("\x1b[38;5;4m\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b\\src/ansi/mod.rs\x1b]8;;\x1b\\\x1b[0m\n"),
+ "src/ansi/mod.rs\n");
+ }
+
+ #[test]
+ fn test_measure_text_width_osc_hyperlink() {
+ assert_eq!(measure_text_width("\x1b[38;5;4m\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b\\src/ansi/mod.rs\x1b]8;;\x1b\\\x1b[0m"),
+ measure_text_width("src/ansi/mod.rs"));
+ }
+
+ #[test]
+ fn test_measure_text_width_osc_hyperlink_non_ascii() {
+ assert_eq!(measure_text_width("\x1b[38;5;4m\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b\\src/ansi/modバー.rs\x1b]8;;\x1b\\\x1b[0m"),
+ measure_text_width("src/ansi/modバー.rs"));
+ }
+
+ #[test]
+ fn test_parse_first_style() {
+ let minus_line_from_unconfigured_git = "\x1b[31m-____\x1b[m\n";
+ let style = parse_first_style(minus_line_from_unconfigured_git);
+ let expected_style = ansi_term::Style {
+ foreground: Some(ansi_term::Color::Red),
+ ..ansi_term::Style::default()
+ };
+ assert_eq!(Some(expected_style), style);
+ }
#[test]
fn test_string_starts_with_ansi_escape_sequence() {
- assert!(!string_starts_with_ansi_escape_sequence(""));
- assert!(!string_starts_with_ansi_escape_sequence("-"));
- assert!(string_starts_with_ansi_escape_sequence(
+ assert!(!string_starts_with_ansi_style_sequence(""));
+ assert!(!string_starts_with_ansi_style_sequence("-"));
+ assert!(string_starts_with_ansi_style_sequence(
"\x1b[31m-XXX\x1b[m\n"
));
- assert!(string_starts_with_ansi_escape_sequence("\x1b[32m+XXX"));
+ assert!(string_starts_with_ansi_style_sequence("\x1b[32m+XXX"));
}
#[test]
diff --git a/src/ansi/parse.rs b/src/ansi/parse.rs
deleted file mode 100644
index 94d902c4..00000000
--- a/src/ansi/parse.rs
+++ /dev/null
@@ -1,205 +0,0 @@
-use ansi_term;
-use vte;
-
-pub fn parse_first_style(s: &str) -> Option<ansi_term::Style> {
- let mut machine = vte::Parser::new();
- let mut performer = Performer { style: None };
- for b in s.bytes() {
- if performer.style.is_some() {
- return performer.style;
- }
- machine.advance(&mut performer, b)
- }
- None
-}
-
-struct Performer {
- style: Option<ansi_term::Style>,
-}
-
-// Based on https://github.com/alacritty/vte/blob/0310be12d3007e32be614c5df94653d29fcc1a8b/examples/parselog.rs
-impl vte::Perform for Performer {
- fn csi_dispatch(&mut self, params: &[i64], intermediates: &[u8], ignore: bool, c: char) {
- if ignore || intermediates.len() > 1 {
- return;
- }
-
- m