Revert "Remove code that tries to handle ANSI escape inputs"

This reverts commit 8174e02279. 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.
This commit is contained in:
Martin Nordholts 2022-09-09 20:21:22 +02:00
parent 6680f65e4b
commit 352309b056
6 changed files with 396 additions and 116 deletions

View File

@ -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

View File

@ -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};

View File

@ -1,28 +1,37 @@
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);
pub fn expand_tabs(line: &str, width: usize, cursor: &mut usize) -> String {
let mut buffer = String::with_capacity(line.len() * 2);
while let Some(index) = text.find('\t') {
// Add previous text.
if index > 0 {
*cursor += index;
buffer.push_str(&text[0..index]);
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));
// Next.
text = &text[index + 1..text.len()];
}
*cursor += text.len();
buffer.push_str(text);
}
}
// Add tab.
let spaces = width - (*cursor % width);
*cursor += spaces;
buffer.push_str(&*" ".repeat(spaces));
// Next.
text = &text[index + 1..text.len()];
}
*cursor += text.len();
buffer.push_str(text);
buffer
}

View File

@ -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');
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)?;
}
write!(
handle,
"{}",
as_terminal_escaped(
style,
text_trimmed,
true_color,
colored_output,
italics,
background_color
)
)?;
// Regular text.
(text, false) => {
let text = &*self.preprocess(text, &mut cursor_total);
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
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()
};
write!(
handle,
"{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, text_trimmed),
true_color,
colored_output,
italics,
background_color
)
)?;
let width = if cursor_total <= cursor_max {
cursor_max - cursor_total + 1
} else {
0
};
write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
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()
)?;
// Regular text.
(text, false) => {
let text = self.preprocess(
text.trim_end_matches(|c| c == '\r' || c == '\n'),
&mut cursor_total,
);
cursor = 0;
max_width = cursor_max;
let mut max_width = cursor_max - cursor;
line_buf.clear();
current_width = cw;
// 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);
}
// 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
)
)?;
}
}
line_buf.push(c);
}
// 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")?;
}

212
src/vscreen.rs Normal file
View File

@ -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)
}

View File

@ -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()