Repository: sharkdp/bat (language: rust)

Issue:
Broken line breaks when using it with `delta` as git-diff pager
**What steps will reproduce the bug?**

1. `export PAGER=bat`
2. `git config pager.diff=delta`
3. Go to any git clone that contains a long line, remove it.

**What happens?**
Line breaks are broken.
![neu](https://user-images.githubusercontent.com/103587359/189113658-80452f4a-b274-440f-a34e-cb1dd1813b0c.png)



...

**What did you expect to happen instead?**
Line breaks are okay.
![alt](https://user-images.githubusercontent.com/103587359/189113671-8dbf81ae-913c-408c-ba5d-fbf5dc61ed3f.png)

...


**How did you install `bat`?**

`pacman -S bat`, can also reproduce it via `cargo install`, as well as `git clone` followed by `cargo install --path .`

---

* Good version: `bat 0.21.0 (405e5f74)`
* Bad version: `bat 0.22.0 (1f9519d8)` (release)
* Bad version: `bat 0.22.0 (6680f65)` (HEAD)
* `git bisect` points to 899fdbb347b6444eb06d8a26375ed10e35c612e2

The error is triggered when running `git-diff`, with `pager.diff` set to `delta`. My `$PAGER` is set to `bat`. Therefore I don't know how to apply the `--diagnostics` part.

P.S.: The `gdi` command is an alias to `git diff`.


Discussion / hints:
I just found it affects more applications. This is happens when installing updates on Arch Linux with the AUR-helper `paru`.
![screen](https://user-images.githubusercontent.com/103587359/189139717-e537a013-d7ee-495b-ab91-8e443dd6bb82.png)

Quick question before I have time for a more deeper investigation: Does it look OK to you if you disable wrapping? Like this:
```
export PAGER='bat --wrap=never'
```

Yes, it does. What does that mean for me?
@sharkdp I think we should revert https://github.com/sharkdp/bat/pull/2189 and do a 0.22.1 point release ASAP. Do you agree?

I didn't know about this use case, but it seems like a basic use case, so the short term solution should be to revert that PR, I think. That will make https://github.com/sharkdp/bat/issues/2185 occur again, but that seems like more of an edge case than the use case in this issue.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ce2c2c4119..a8245e0faa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,16 +1,8 @@
-# unreleased
-
-## Features
+# v0.22.1
 
 ## Bugfixes
 
-## Other
-
-## Syntaxes
-
-## Themes
-
-## `bat` as a library
+- Bring back pre-processing of ANSI escape characters to so that some common `bat` use cases starts working again. See #2308 (@Enselic)
 
 
 # v0.22.0
diff --git a/Cargo.lock b/Cargo.lock
index 6793cc53a0..4aa42a7c27 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -74,7 +74,7 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
 [[package]]
 name = "bat"
-version = "0.22.0"
+version = "0.22.1"
 dependencies = [
  "ansi_colours",
  "ansi_term",
diff --git a/Cargo.toml b/Cargo.toml
index 7420dd7cf7..b51d5e4b27 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ homepage = "https://github.com/sharkdp/bat"
 license = "MIT/Apache-2.0"
 name = "bat"
 repository = "https://github.com/sharkdp/bat"
-version = "0.22.0"
+version = "0.22.1"
 exclude = ["assets/syntaxes/*", "assets/themes/*"]
 build = "build.rs"
 edition = '2018'
diff --git a/src/lib.rs b/src/lib.rs
index 4e3fcb95b0..0ca67bb978 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 6e775595b8..745903550c 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 4f962df9eb..27c92b91f7 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 0000000000..ea5d4da6f0
--- /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)
+}


Test patch:
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 02e60a7581..9352c372a6 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()