summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Nordholts <enselic@gmail.com>2022-09-09 20:21:22 +0200
committerMartin Nordholts <enselic@gmail.com>2022-09-10 17:59:33 +0200
commit352309b0566519ffb64adba1848fbdefce878ab6 (patch)
treea5a33794b4b1d47ffb713ea6d74660f6bb9b80fc
parent6680f65e4b25b0f18c455f7a4639a96e97519dc5 (diff)
Revert "Remove code that tries to handle ANSI escape inputs"
This reverts commit 8174e022797924aec735ff440806920767f80496. Turns out it is needed for a common use case, see https://github.com/sharkdp/bat/issues/2307. It is not a clean revert, because I adjust CHANGELOG.md and also add a comment to the test. I also had to resolve a small `use` conflict.
-rw-r--r--CHANGELOG.md2
-rw-r--r--src/lib.rs1
-rw-r--r--src/preprocessor.rs45
-rw-r--r--src/printer.rs240
-rw-r--r--src/vscreen.rs212
-rw-r--r--tests/integration_tests.rs20
6 files changed, 400 insertions, 120 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce2c2c41..c8460faf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
## Bugfixes
+- Bring back pre-processing of ANSI escape characters to so that some common `bat` use cases starts working again. See #2308 (@Enselic)
+
## Other
## Syntaxes
diff --git a/src/lib.rs b/src/lib.rs
index 4e3fcb95..0ca67bb9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -46,6 +46,7 @@ pub(crate) mod printer;
pub mod style;
pub(crate) mod syntax_mapping;
mod terminal;
+mod vscreen;
pub(crate) mod wrapping;
pub use pretty_printer::{Input, PrettyPrinter, Syntax};
diff --git a/src/preprocessor.rs b/src/preprocessor.rs
index 6e775595..74590355 100644
--- a/src/preprocessor.rs
+++ b/src/preprocessor.rs
@@ -1,27 +1,36 @@
use std::fmt::Write;
+use console::AnsiCodeIterator;
+
/// Expand tabs like an ANSI-enabled expand(1).
-pub fn expand_tabs(mut text: &str, width: usize, cursor: &mut usize) -> String {
- let mut buffer = String::with_capacity(text.len() * 2);
-
- while let Some(index) = text.find('\t') {
- // Add previous text.
- if index > 0 {
- *cursor += index;
- buffer.push_str(&text[0..index]);
- }
+pub fn expand_tabs(line: &str, width: usize, cursor: &mut usize) -> String {
+ let mut buffer = String::with_capacity(line.len() * 2);
+
+ for chunk in AnsiCodeIterator::new(line) {
+ match chunk {
+ (text, true) => buffer.push_str(text),
+ (mut text, false) => {
+ while let Some(index) = text.find('\t') {
+ // Add previous text.
+ if index > 0 {
+ *cursor += index;
+ buffer.push_str(&text[0..index]);
+ }
- // Add tab.
- let spaces = width - (*cursor % width);
- *cursor += spaces;
- buffer.push_str(&*" ".repeat(spaces));
+ // Add tab.
+ let spaces = width - (*cursor % width);
+ *cursor += spaces;
+ buffer.push_str(&*" ".repeat(spaces));
- // Next.
- text = &text[index + 1..text.len()];
- }
+ // Next.
+ text = &text[index + 1..text.len()];
+ }
- *cursor += text.len();
- buffer.push_str(text);
+ *cursor += text.len();
+ buffer.push_str(text);
+ }
+ }
+ }
buffer
}
diff --git a/src/printer.rs b/src/printer.rs
index 4f962df9..27c92b91 100644
--- a/src/printer.rs
+++ b/src/printer.rs
@@ -6,6 +6,8 @@ use ansi_term::Style;
use bytesize::ByteSize;
+use console::AnsiCodeIterator;
+
use syntect::easy::HighlightLines;
use syntect::highlighting::Color;
use syntect::highlighting::Theme;
@@ -31,6 +33,7 @@ use crate::line_range::RangeCheckResult;
use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color};
+use crate::vscreen::AnsiStyle;
use crate::wrapping::WrappingMode;
pub(crate) trait Printer {
@@ -119,6 +122,7 @@ pub(crate) struct InteractivePrinter<'a> {
config: &'a Config<'a>,
decorations: Vec<Box<dyn Decoration>>,
panel_width: usize,
+ ansi_style: AnsiStyle,
content_type: Option<ContentType>,
#[cfg(feature = "git")]
pub line_changes: &'a Option<LineChanges>,
@@ -202,6 +206,7 @@ impl<'a> InteractivePrinter<'a> {
config,
decorations,
content_type: input.reader.content_type,
+ ansi_style: AnsiStyle::new(),
#[cfg(feature = "git")]
line_changes,
highlighter_from_set,
@@ -480,7 +485,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
if highlight_this_line && self.config.theme == "ansi" {
- write!(handle, "\x1B[4m")?;
+ self.ansi_style.update("^[4m");
}
let background_color = self
@@ -507,37 +512,51 @@ impl<'a> Printer for InteractivePrinter<'a> {
let italics = self.config.use_italic_text;
for &(style, region) in &regions {
- let text = &*self.preprocess(region, &mut cursor_total);
- let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
-
- write!(
- handle,
- "{}",
- as_terminal_escaped(
- style,
- text_trimmed,
- true_color,
- colored_output,
- italics,
- background_color
- )
- )?;
+ let ansi_iterator = AnsiCodeIterator::new(region);
+ for chunk in ansi_iterator {
+ match chunk {
+ // ANSI escape passthrough.
+ (ansi, true) => {
+ self.ansi_style.update(ansi);
+ write!(handle, "{}", ansi)?;
+ }
- if text.len() != text_trimmed.len() {
- if let Some(background_color) = background_color {
- let ansi_style = Style {
- background: to_ansi_color(background_color, true_color),
- ..Default::default()
- };
-
- let width = if cursor_total <= cursor_max {
- cursor_max - cursor_total + 1
- } else {
- 0
- };
- write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
+ // Regular text.
+ (text, false) => {
+ let text = &*self.preprocess(text, &mut cursor_total);
+ let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
+
+ write!(
+ handle,
+ "{}",
+ as_terminal_escaped(
+ style,
+ &format!("{}{}", self.ansi_style, text_trimmed),
+ true_color,
+ colored_output,
+ italics,
+ background_color
+ )
+ )?;
+
+ if text.len() != text_trimmed.len() {
+ if let Some(background_color) = background_color {
+ let ansi_style = Style {
+ background: to_ansi_color(background_color, true_color),
+ ..Default::default()
+ };
+
+ let width = if cursor_total <= cursor_max {
+ cursor_max - cursor_total + 1
+ } else {
+ 0
+ };
+ write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
+ }
+ write!(handle, "{}", &text[text_trimmed.len()..])?;
+ }
+ }
}
- write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}
@@ -546,82 +565,98 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
} else {
for &(style, region) in &regions {
- let text = self.preprocess(
- region.trim_end_matches(|c| c == '\r' || c == '\n'),
- &mut cursor_total,
- );
-
- let mut max_width = cursor_max - cursor;
-
- // line buffer (avoid calling write! for every character)
- let mut line_buf = String::with_capacity(max_width * 4);
-
- // Displayed width of line_buf
- let mut current_width = 0;
-
- for c in text.chars() {
- // calculate the displayed width for next character
- let cw = c.width().unwrap_or(0);
- current_width += cw;
-
- // if next character cannot be printed on this line,
- // flush the buffer.
- if current_width > max_width {
- // Generate wrap padding if not already generated.
- if panel_wrap.is_none() {
- panel_wrap = if self.panel_width > 0 {
- Some(format!(
- "{} ",
- self.decorations
- .iter()
- .map(|d| d.generate(line_number, true, self).text)
- .collect::<Vec<String>>()
- .join(" ")
- ))
- } else {
- Some("".to_string())
- }
+ let ansi_iterator = AnsiCodeIterator::new(region);
+ for chunk in ansi_iterator {
+ match chunk {
+ // ANSI escape passthrough.
+ (ansi, true) => {
+ self.ansi_style.update(ansi);
+ write!(handle, "{}", ansi)?;
}
- // It wraps.
- write!(
- handle,
- "{}\n{}",
- as_terminal_escaped(
- style,
- &line_buf,
- self.config.true_color,
- self.config.colored_output,
- self.config.use_italic_text,
- background_color
- ),
- panel_wrap.clone().unwrap()
- )?;
-
- cursor = 0;
- max_width = cursor_max;
-
- line_buf.clear();
- current_width = cw;
- }
+ // Regular text.
+ (text, false) => {
+ let text = self.preprocess(
+ text.trim_end_matches(|c| c == '\r' || c == '\n'),
+ &mut cursor_total,
+ );
+
+ let mut max_width = cursor_max - cursor;
+
+ // line buffer (avoid calling write! for every character)
+ let mut line_buf = String::with_capacity(max_width * 4);
+
+ // Displayed width of line_buf
+ let mut current_width = 0;
+
+ for c in text.chars() {
+ // calculate the displayed width for next character
+ let cw = c.width().unwrap_or(0);
+ current_width += cw;
+
+ // if next character cannot be printed on this line,
+ // flush the buffer.
+ if current_width > max_width {
+ // Generate wrap padding if not already generated.
+ if panel_wrap.is_none() {
+ panel_wrap = if self.panel_width > 0 {
+ Some(format!(
+ "{} ",
+ self.decorations
+ .iter()
+ .map(|d| d
+ .generate(line_number, true, self)
+ .text)
+ .collect::<Vec<String>>()
+ .join(" ")
+ ))
+ } else {
+ Some("".to_string())
+ }
+ }
+
+ // It wraps.
+ write!(
+ handle,
+ "{}\n{}",
+ as_terminal_escaped(
+ style,
+ &*format!("{}{}", self.ansi_style, line_buf),
+ self.config.true_color,
+ self.config.colored_output,
+ self.config.use_italic_text,
+ background_color
+ ),
+ panel_wrap.clone().unwrap()
+ )?;
+
+ cursor = 0;
+ max_width = cursor_max;
+
+ line_buf.clear();
+ current_width = cw;
+ }
+
+ line_buf.push(c);
+ }
- line_buf.push(c);
+ // flush the buffer
+ cursor += current_width;
+ write!(
+ handle,
+ "{}",
+ as_terminal_escaped(
+ style,
+ &*format!("{}{}", self.ansi_style, line_buf),
+ self.config.true_color,
+ self.config.colored_output,
+ self.config.use_italic_text,
+ background_color
+ )
+ )?;
+ }
+ }
}
-
- // flush the buffer
- cursor += current_width;
- write!(
- handle,
- "{}",
- as_terminal_escaped(
- style,
- &line_buf,
- self.config.true_color,
- self.config.colored_output,
- self.config.use_italic_text,
- background_color
- )
- )?;
}
if let Some(background_color) = background_color {
@@ -640,6 +675,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
if highlight_this_line && self.config.theme == "ansi" {
+ self.ansi_style.update("^[24m");
write!(handle, "\x1B[24m")?;
}
diff --git a/src/vscreen.rs b/src/vscreen.rs
new file mode 100644
index 00000000..ea5d4da6
--- /dev/null
+++ b/src/vscreen.rs
@@ -0,0 +1,212 @@
+use std::fmt::{Display, Formatter};
+
+// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
+pub struct AnsiStyle {
+ attributes: Option<Attributes>,
+}
+
+impl AnsiStyle {
+ pub fn new() -> Self {
+ AnsiStyle { attributes: None }
+ }
+
+ pub fn update(&mut self, sequence: &str) -> bool {
+ match &mut self.attributes {
+ Some(a) => a.update(sequence),
+ None => {
+ self.attributes = Some(Attributes::new());
+ self.attributes.as_mut().unwrap().update(sequence)
+ }
+ }
+ }
+}
+
+impl Display for AnsiStyle {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self.attributes {
+ Some(ref a) => a.fmt(f),
+ None => Ok(()),
+ }
+ }
+}
+
+struct Attributes {
+ foreground: String,
+ background: String,
+ underlined: String,
+
+ /// The character set to use.
+ /// REGEX: `\^[()][AB0-3]`
+ charset: String,
+
+ /// A buffer for unknown sequences.
+ unknown_buffer: String,
+
+ /// ON: ^[1m
+ /// OFF: ^[22m
+ bold: String,
+
+ /// ON: ^[2m
+ /// OFF: ^[22m
+ dim: String,
+
+ /// ON: ^[4m
+ /// OFF: ^[24m
+ underline: String,
+
+ /// ON: ^[3m
+ /// OFF: ^[23m
+ italic: String,
+
+ /// ON: ^[9m
+ /// OFF: ^[29m
+ strike: String,
+}
+
+impl Attributes {
+ pub fn new() -> Self {
+ Attributes {
+ foreground: "".to_owned(),
+ background: "".to_owned(),
+ underlined: "".to_owned(),
+ charset: "".to_owned(),
+ unknown_buffer: "".to_owned(),
+ bold: "".to_owned(),
+ dim: "".to_owned(),
+ underline: "".to_owned(),
+ italic: "".to_owned(),
+ strike: "".to_owned(),
+ }
+ }
+
+ /// Update the attributes with an escape sequence.
+ /// Returns `false` if the sequence is unsupported.
+ pub fn update(&mut self, sequence: &str) -> bool {
+ let mut chars = sequence.char_indices().skip(1);
+
+ if let Some((_, t)) = chars.next() {
+ match t {
+ '(' => self.update_with_charset('(', chars.map(|(_, c)| c)),
+ ')' => self.update_with_charset(')', chars.map(|(_, c)| c)),
+ '[' => {
+ if let Some((i, last)) = chars.last() {
+ // SAFETY: Always starts with ^[ and ends with m.
+ self.update_with_csi(last, &sequence[2..i])
+ } else {
+ false
+ }
+ }
+ _ => self.update_with_unsupported(sequence),
+ }
+ } else {
+ false
+ }
+ }
+
+ fn sgr_reset(&mut self) {
+ self.foreground.clear();
+ self.background.clear();
+ self.underlined.clear();
+ self.bold.clear();
+ self.dim.clear();
+ self.underline.clear();
+ self.italic.clear();
+ self.strike.clear();
+ }
+
+ fn update_with_sgr(&mut self, parameters: &str) -> bool {
+ let mut iter = parameters
+ .split(';')
+ .map(|p| if p.is_empty() { "0" } else { p })
+ .map(|p| p.parse::<u16>())
+ .map(|p| p.unwrap_or(0)); // Treat errors as 0.
+
+ while let Some(p) = iter.next() {
+ match p {
+ 0 => self.sgr_reset(),
+ 1 => self.bold = format!("\x1B[{}m", parameters),
+ 2 => self.dim = format!("\x1B[{}m", parameters),
+ 3 => self.italic = format!("\x1B[{}m", parameters),
+ 4 => self.underline = format!("\x1B[{}m", parameters),
+ 23 => self.italic.clear(),
+ 24 => self.underline.clear(),
+ 22 => {
+ self.bold.clear();
+ self.dim.clear();
+ }
+ 30..=39 => self.foreground = Self::parse_color(p, &mut iter),
+ 40..=49 => self.background = Self::parse_color(p, &mut iter),
+ 58..=59 => self.underlined = Self::parse_color(p, &mut iter),
+ 90..=97 => self.foreground = Self::parse_color(p, &mut iter),
+ 100..=107 => self.foreground = Self::parse_color(p, &mut iter),
+ _ => {
+ // Unsupported SGR sequence.
+ // Be compatible and pretend one just wasn't was provided.
+ }
+ }
+ }
+
+ true
+ }
+
+ fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool {
+ if finalizer == 'm' {
+ self.update_with_sgr(sequence)
+ } else {
+ false
+ }
+ }
+
+ fn update_with_unsupported(&mut self, sequence: &str) -> bool {
+ self.unknown_buffer.push_str(sequence);
+ false
+ }
+
+ fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
+ self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
+ true
+ }
+
+ fn parse_color(color: u16, parameters: &mut dyn Iterator<Item = u16>) -> String {
+ match color % 10 {
+ 8 => match parameters.next() {
+ Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)),
+ Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
+ Some(c) => format!("\x1B[{};{}m", color, c),
+ _ => "".to_owned(),
+ },
+ 9 => "".to_owned(),
+ _ => format!("\x1B[{}m", color),
+ }
+ }
+}
+
+impl Display for Attributes {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}{}{}{}{}{}{}{}{}",
+ self.foreground,
+ self.background,
+ self.underlined,
+ self.charset,
+ self.bold,
+ self.dim,
+ self.underline,
+ self.italic,
+ self.strike,
+ )
+ }
+}
+
+fn join(
+ delimiter: &str,
+ limit: usize,
+ iterator: &mut dyn Iterator<Item = impl ToString>,
+) -> String {
+ iterator
+ .take(limit)
+ .map(|i| i.to_string())
+ .collect::<Vec<String>>()
+ .join(delimiter)
+}
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 02e60a75..9352c372 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -1442,6 +1442,26 @@ fn ansi_highlight_underline() {
.stderr("");
}
+// Ensure that ANSI passthrough is emitted properly for both wrapping and non-wrapping printer.
+// See https://github.com/sharkdp/bat/issues/2307 for what common use case this test tests.
+#[test]
+fn ansi_passthrough_emit() {
+ for wrapping in &["never", "character"] {
+ bat()
+ .arg("--paging=never")
+ .arg("--color=never")
+ .arg("--terminal-width=80")
+ .arg(format!("--wrap={}", wrapping))
+ .arg("--decorations=always")
+ .arg("--style=plain")
+ .write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n")
+ .assert()
+ .success()
+ .stdout("\x1B[33m\x1B[33mColor\n\x1B[33mColor \x1B[m\nPlain\n")
+ .stderr("");
+ }
+}
+
#[test]
fn ignored_suffix_arg() {
bat()