Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved ANSI passthrough. #1596

Merged
merged 7 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

## Bugfixes

- Fix for poor performance when ANSI escape sequences are piped to `bat`, see #1596 (@eth-p)
- Fix for incorrect handling of ANSI escape sequences when using `--wrap=never`, see #1596 (@eth-p)

## Other

- `Input::ordinary_file` and `Input::with_name` now accept `Path` rather than `OsStr` see #1571 (@matklad)
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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};
Expand Down
109 changes: 49 additions & 60 deletions src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use crate::input::OpenedInput;
use crate::line_range::RangeCheckResult;
use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::vscreen::AnsiStyle;
use crate::wrapping::WrappingMode;

pub(crate) trait Printer {
Expand Down Expand Up @@ -104,7 +105,7 @@ pub(crate) struct InteractivePrinter<'a> {
config: &'a Config<'a>,
decorations: Vec<Box<dyn Decoration>>,
panel_width: usize,
ansi_prefix_sgr: String,
ansi_style: AnsiStyle,
content_type: Option<ContentType>,
#[cfg(feature = "git")]
pub line_changes: &'a Option<LineChanges>,
Expand Down Expand Up @@ -188,7 +189,7 @@ impl<'a> InteractivePrinter<'a> {
config,
decorations,
content_type: input.reader.content_type,
ansi_prefix_sgr: String::new(),
ansi_style: AnsiStyle::new(),
#[cfg(feature = "git")]
line_changes,
highlighter,
Expand Down Expand Up @@ -430,33 +431,49 @@ impl<'a> Printer for InteractivePrinter<'a> {
let italics = self.config.use_italic_text;

for &(style, region) in regions.iter() {
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 mut ansi_style = Style::default();
ansi_style.background = to_ansi_color(background_color, true_color);
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 mut ansi_style = Style::default();
ansi_style.background =
to_ansi_color(background_color, true_color);
let width = if cursor_total <= cursor_max {
cursor_max - cursor_total + 1
Enselic marked this conversation as resolved.
Show resolved Hide resolved
} else {
0
};
write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
}
write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}
}
write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}

Expand All @@ -466,31 +483,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
} else {
for &(style, region) in regions.iter() {
let ansi_iterator = AnsiCodeIterator::new(region);
let mut ansi_prefix: String = String::new();
for chunk in ansi_iterator {
match chunk {
// ANSI escape passthrough.
(text, true) => {
let is_ansi_csi = text.starts_with("\x1B[");

if is_ansi_csi && text.ends_with('m') {
// It's an ANSI SGR sequence.
// We should be mostly safe to just append these together.
ansi_prefix.push_str(text);
if text == "\x1B[0m" {
self.ansi_prefix_sgr = "\x1B[0m".to_owned();
} else {
self.ansi_prefix_sgr.push_str(text);
}
} else if is_ansi_csi {
// It's a regular CSI sequence.
// We should be mostly safe to just append these together.
ansi_prefix.push_str(text);
} else {
// It's probably a VT100 code.
// Passing it through is the safest bet.
write!(handle, "{}", text)?;
}
(ansi, true) => {
self.ansi_style.update(ansi);
write!(handle, "{}", ansi)?;
}

// Regular text.
Expand Down Expand Up @@ -540,10 +538,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
"{}\n{}",
as_terminal_escaped(
style,
&*format!(
"{}{}{}",
self.ansi_prefix_sgr, ansi_prefix, line_buf
),
&*format!("{}{}", self.ansi_style, line_buf),
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
Expand All @@ -569,19 +564,13 @@ impl<'a> Printer for InteractivePrinter<'a> {
"{}",
as_terminal_escaped(
style,
&*format!(
"{}{}{}",
self.ansi_prefix_sgr, ansi_prefix, line_buf
),
&*format!("{}{}", self.ansi_style, line_buf),
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
background_color
)
)?;

// Clear the ANSI prefix buffer.
ansi_prefix.clear();
}
}
}
Expand Down
212 changes: 212 additions & 0 deletions src/vscreen.rs
Original file line number Diff line number Diff line change
@@ -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)
}
Loading