on master help sign in
Improved ANSI passthrough (#1596)

Improve handling of ANSI passthrough. Fix ANSI passthrough for --wrap=never. Add test for ANSI passthrough.

commit63ad53817d2d2f3f6b33983c95d6d3ed31184ce9 authorEthan P <eth-p+git@hidden.email> dateWed 8 Dec 2021, 07:06 (5 years ago) parent6d0eb07
5 files changed+285−62
CHANGELOG.md+2 src/lib.rs+1 src/printer.rs+51−62 src/vscreen.rs+212 tests/integration_tests.rs+19
CHANGELOG.md+2
@@ -9,6 +9,8 @@
99
1010## Bugfixes
1111
12+- Fix for poor performance when ANSI escape sequences are piped to `bat`, see #1596 (@eth-p)
13+- Fix for incorrect handling of ANSI escape sequences when using `--wrap=never`, see #1596 (@eth-p)
1214- Python syntax highlighting no longer suffers from abysmal performance in specific scenarios. See #1688 (@keith-hall)
1315- First line not shown in diff context. See #1891 (@divagant-martian)
1416- Do not ignore syntaxes that handle file names with a `*.conf` extension. See #1703 (@cbolgiano)
src/lib.rs+1
@@ -46,6 +46,7 @@ pub(crate) mod printer;
4646pub mod style;
4747pub(crate) mod syntax_mapping;
4848mod terminal;
49+mod vscreen;
4950pub(crate) mod wrapping;
5051
5152pub use pretty_printer::{Input, PrettyPrinter};
src/printer.rs+51−62
@@ -30,6 +30,7 @@ use crate::input::OpenedInput;
3030use crate::line_range::RangeCheckResult;
3131use crate::preprocessor::{expand_tabs, replace_nonprintable};
3232use crate::terminal::{as_terminal_escaped, to_ansi_color};
33+use crate::vscreen::AnsiStyle;
3334use crate::wrapping::WrappingMode;
3435
3536pub(crate) trait Printer {
@@ -118,7 +119,7 @@ pub(crate) struct InteractivePrinter<'a> {
118119 config: &'a Config<'a>,
119120 decorations: Vec<Box<dyn Decoration>>,
120121 panel_width: usize,
121− ansi_prefix_sgr: String,
122+ ansi_style: AnsiStyle,
122123 content_type: Option<ContentType>,
123124 #[cfg(feature = "git")]
124125 pub line_changes: &'a Option<LineChanges>,
@@ -202,7 +203,7 @@ impl<'a> InteractivePrinter<'a> {
202203 config,
203204 decorations,
204205 content_type: input.reader.content_type,
205− ansi_prefix_sgr: String::new(),
206+ ansi_style: AnsiStyle::new(),
206207 #[cfg(feature = "git")]
207208 line_changes,
208209 highlighter_from_set,
@@ -444,35 +445,51 @@ impl<'a> Printer for InteractivePrinter<'a> {
444445 let italics = self.config.use_italic_text;
445446
446447 for &(style, region) in &regions {
447− let text = &*self.preprocess(region, &mut cursor_total);
448− let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
449− write!(
450− handle,
451− "{}",
452− as_terminal_escaped(
453− style,
454− text_trimmed,
455− true_color,
456− colored_output,
457− italics,
458− background_color
459− )
460− )?;
448+ let ansi_iterator = AnsiCodeIterator::new(region);
449+ for chunk in ansi_iterator {
450+ match chunk {
451+ // ANSI escape passthrough.
452+ (ansi, true) => {
453+ self.ansi_style.update(ansi);
454+ write!(handle, "{}", ansi)?;
455+ }
461456
462− if text.len() != text_trimmed.len() {
463− if let Some(background_color) = background_color {
464− let ansi_style = Style {
465− background: to_ansi_color(background_color, true_color),
466− ..Default::default()
467− };
468− let width = if cursor_total <= cursor_max {
469− cursor_max - cursor_total + 1
470− } else {
471− 0
472− };
473− write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
457+ // Regular text.
458+ (text, false) => {
459+ let text = &*self.preprocess(text, &mut cursor_total);
460+ let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
461+
462+ write!(
463+ handle,
464+ "{}",
465+ as_terminal_escaped(
466+ style,
467+ &format!("{}{}", self.ansi_style, text_trimmed),
468+ true_color,
469+ colored_output,
470+ italics,
471+ background_color
472+ )
473+ )?;
474+
475+ if text.len() != text_trimmed.len() {
476+ if let Some(background_color) = background_color {
477+ let ansi_style = Style {
478+ background: to_ansi_color(background_color, true_color),
479+ ..Default::default()
480+ };
481+
482+ let width = if cursor_total <= cursor_max {
483+ cursor_max - cursor_total + 1
484+ } else {
485+ 0
486+ };
487+ write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
488+ }
489+ write!(handle, "{}", &text[text_trimmed.len()..])?;
490+ }
491+ }
474492 }
475− write!(handle, "{}", &text[text_trimmed.len()..])?;
476493 }
477494 }
478495
@@ -482,31 +499,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
482499 } else {
483500 for &(style, region) in &regions {
484501 let ansi_iterator = AnsiCodeIterator::new(region);
485− let mut ansi_prefix: String = String::new();
486502 for chunk in ansi_iterator {
487503 match chunk {
488504 // ANSI escape passthrough.
489− (text, true) => {
490− let is_ansi_csi = text.starts_with("\x1B[");
491−
492− if is_ansi_csi && text.ends_with('m') {
493− // It's an ANSI SGR sequence.
494− // We should be mostly safe to just append these together.
495− ansi_prefix.push_str(text);
496− if text == "\x1B[0m" {
497− self.ansi_prefix_sgr = "\x1B[0m".to_owned();
498− } else {
499− self.ansi_prefix_sgr.push_str(text);
500− }
501− } else if is_ansi_csi {
502− // It's a regular CSI sequence.
503− // We should be mostly safe to just append these together.
504− ansi_prefix.push_str(text);
505− } else {
506− // It's probably a VT100 code.
507− // Passing it through is the safest bet.
508− write!(handle, "{}", text)?;
509− }
505+ (ansi, true) => {
506+ self.ansi_style.update(ansi);
507+ write!(handle, "{}", ansi)?;
510508 }
511509
512510 // Regular text.
@@ -556,10 +554,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
556554 "{}\n{}",
557555 as_terminal_escaped(
558556 style,
559− &*format!(
560− "{}{}{}",
561− self.ansi_prefix_sgr, ansi_prefix, line_buf
562− ),
557+ &*format!("{}{}", self.ansi_style, line_buf),
563558 self.config.true_color,
564559 self.config.colored_output,
565560 self.config.use_italic_text,
@@ -585,19 +580,13 @@ impl<'a> Printer for InteractivePrinter<'a> {
585580 "{}",
586581 as_terminal_escaped(
587582 style,
588− &*format!(
589− "{}{}{}",
590− self.ansi_prefix_sgr, ansi_prefix, line_buf
591− ),
583+ &*format!("{}{}", self.ansi_style, line_buf),
592584 self.config.true_color,
593585 self.config.colored_output,
594586 self.config.use_italic_text,
595587 background_color
596588 )
597589 )?;
598−
599− // Clear the ANSI prefix buffer.
600− ansi_prefix.clear();
601590 }
602591 }
603592 }
src/vscreen.rsnew file+212
@@ -0,0 +1,212 @@
1+use std::fmt::{Display, Formatter};
2+
3+// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
4+pub struct AnsiStyle {
5+ attributes: Option<Attributes>,
6+}
7+
8+impl AnsiStyle {
9+ pub fn new() -> Self {
10+ AnsiStyle { attributes: None }
11+ }
12+
13+ pub fn update(&mut self, sequence: &str) -> bool {
14+ match &mut self.attributes {
15+ Some(a) => a.update(sequence),
16+ None => {
17+ self.attributes = Some(Attributes::new());
18+ self.attributes.as_mut().unwrap().update(sequence)
19+ }
20+ }
21+ }
22+}
23+
24+impl Display for AnsiStyle {
25+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26+ match self.attributes {
27+ Some(ref a) => a.fmt(f),
28+ None => Ok(()),
29+ }
30+ }
31+}
32+
33+struct Attributes {
34+ foreground: String,
35+ background: String,
36+ underlined: String,
37+
38+ /// The character set to use.
39+ /// REGEX: `\^[()][AB0-3]`
40+ charset: String,
41+
42+ /// A buffer for unknown sequences.
43+ unknown_buffer: String,
44+
45+ /// ON: ^[1m
46+ /// OFF: ^[22m
47+ bold: String,
48+
49+ /// ON: ^[2m
50+ /// OFF: ^[22m
51+ dim: String,
52+
53+ /// ON: ^[4m
54+ /// OFF: ^[24m
55+ underline: String,
56+
57+ /// ON: ^[3m
58+ /// OFF: ^[23m
59+ italic: String,
60+
61+ /// ON: ^[9m
62+ /// OFF: ^[29m
63+ strike: String,
64+}
65+
66+impl Attributes {
67+ pub fn new() -> Self {
68+ Attributes {
69+ foreground: "".to_owned(),
70+ background: "".to_owned(),
71+ underlined: "".to_owned(),
72+ charset: "".to_owned(),
73+ unknown_buffer: "".to_owned(),
74+ bold: "".to_owned(),
75+ dim: "".to_owned(),
76+ underline: "".to_owned(),
77+ italic: "".to_owned(),
78+ strike: "".to_owned(),
79+ }
80+ }
81+
82+ /// Update the attributes with an escape sequence.
83+ /// Returns `false` if the sequence is unsupported.
84+ pub fn update(&mut self, sequence: &str) -> bool {
85+ let mut chars = sequence.char_indices().skip(1);
86+
87+ if let Some((_, t)) = chars.next() {
88+ match t {
89+ '(' => self.update_with_charset('(', chars.map(|(_, c)| c)),
90+ ')' => self.update_with_charset(')', chars.map(|(_, c)| c)),
91+ '[' => {
92+ if let Some((i, last)) = chars.last() {
93+ // SAFETY: Always starts with ^[ and ends with m.
94+ self.update_with_csi(last, &sequence[2..i])
95+ } else {
96+ false
97+ }
98+ }
99+ _ => self.update_with_unsupported(sequence),
100+ }
101+ } else {
102+ false
103+ }
104+ }
105+
106+ fn sgr_reset(&mut self) {
107+ self.foreground.clear();
108+ self.background.clear();
109+ self.underlined.clear();
110+ self.bold.clear();
111+ self.dim.clear();
112+ self.underline.clear();
113+ self.italic.clear();
114+ self.strike.clear();
115+ }
116+
117+ fn update_with_sgr(&mut self, parameters: &str) -> bool {
118+ let mut iter = parameters
119+ .split(';')
120+ .map(|p| if p.is_empty() { "0" } else { p })
121+ .map(|p| p.parse::<u16>())
122+ .map(|p| p.unwrap_or(0)); // Treat errors as 0.
123+
124+ while let Some(p) = iter.next() {
125+ match p {
126+ 0 => self.sgr_reset(),
127+ 1 => self.bold = format!("\x1B[{}m", parameters),
128+ 2 => self.dim = format!("\x1B[{}m", parameters),
129+ 3 => self.italic = format!("\x1B[{}m", parameters),
130+ 4 => self.underline = format!("\x1B[{}m", parameters),
131+ 23 => self.italic.clear(),
132+ 24 => self.underline.clear(),
133+ 22 => {
134+ self.bold.clear();
135+ self.dim.clear();
136+ }
137+ 30..=39 => self.foreground = Self::parse_color(p, &mut iter),
138+ 40..=49 => self.background = Self::parse_color(p, &mut iter),
139+ 58..=59 => self.underlined = Self::parse_color(p, &mut iter),
140+ 90..=97 => self.foreground = Self::parse_color(p, &mut iter),
141+ 100..=107 => self.foreground = Self::parse_color(p, &mut iter),
142+ _ => {
143+ // Unsupported SGR sequence.
144+ // Be compatible and pretend one just wasn't was provided.
145+ }
146+ }
147+ }
148+
149+ true
150+ }
151+
152+ fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool {
153+ if finalizer == 'm' {
154+ self.update_with_sgr(sequence)
155+ } else {
156+ false
157+ }
158+ }
159+
160+ fn update_with_unsupported(&mut self, sequence: &str) -> bool {
161+ self.unknown_buffer.push_str(sequence);
162+ false
163+ }
164+
165+ fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
166+ self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
167+ true
168+ }
169+
170+ fn parse_color(color: u16, parameters: &mut dyn Iterator<Item = u16>) -> String {
171+ match color % 10 {
172+ 8 => match parameters.next() {
173+ Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)),
174+ Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
175+ Some(c) => format!("\x1B[{};{}m", color, c),
176+ _ => "".to_owned(),
177+ },
178+ 9 => "".to_owned(),
179+ _ => format!("\x1B[{}m", color),
180+ }
181+ }
182+}
183+
184+impl Display for Attributes {
185+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186+ write!(
187+ f,
188+ "{}{}{}{}{}{}{}{}{}",
189+ self.foreground,
190+ self.background,
191+ self.underlined,
192+ self.charset,
193+ self.bold,
194+ self.dim,
195+ self.underline,
196+ self.italic,
197+ self.strike,
198+ )
199+ }
200+}
201+
202+fn join(
203+ delimiter: &str,
204+ limit: usize,
205+ iterator: &mut dyn Iterator<Item = impl ToString>,
206+) -> String {
207+ iterator
208+ .take(limit)
209+ .map(|i| i.to_string())
210+ .collect::<Vec<String>>()
211+ .join(delimiter)
212+}
tests/integration_tests.rs+19
@@ -1226,6 +1226,25 @@ fn grid_for_file_without_newline() {
12261226 .stderr("");
12271227}
12281228
1229+// Ensure that ANSI passthrough is emitted properly for both wrapping and non-wrapping printer.
1230+#[test]
1231+fn ansi_passthrough_emit() {
1232+ for wrapping in &["never", "character"] {
1233+ bat()
1234+ .arg("--paging=never")
1235+ .arg("--color=never")
1236+ .arg("--terminal-width=80")
1237+ .arg(format!("--wrap={}", wrapping))
1238+ .arg("--decorations=always")
1239+ .arg("--style=plain")
1240+ .write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n")
1241+ .assert()
1242+ .success()
1243+ .stdout("\x1B[33m\x1B[33mColor\n\x1B[33mColor \x1B[m\nPlain\n")
1244+ .stderr("");
1245+ }
1246+}
1247+
12291248#[test]
12301249fn ignored_suffix_arg() {
12311250 bat()