From 51bdaa5f887cecf9c5c1ecb0430d249f9bd66ec8 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 5 Dec 2025 12:15:48 +0100 Subject: [PATCH 01/11] Strip overstriking to better support man pages --- src/preprocessor.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++ src/printer.rs | 28 +++++++++++++++++++++--- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/preprocessor.rs b/src/preprocessor.rs index dc2aa66e..74486489 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::Write; use crate::{ @@ -149,6 +150,31 @@ pub fn strip_ansi(line: &str) -> String { buffer } +/// Strips overstrike sequences (backspace formatting) from input. +/// +/// Overstrike formatting is used by man pages and some help output: +/// - Bold: `X\x08X` (character, backspace, same character) +/// - Underline: `_\x08X` (underscore, backspace, character) +/// +/// This function removes these sequences, keeping only the visible character. +pub fn strip_overstrike(line: &str) -> Cow<'_, str> { + if !line.contains('\x08') { + return Cow::Borrowed(line); + } + + let mut output = String::with_capacity(line.len()); + + for c in line.chars() { + if c == '\x08' { + output.pop(); + } else { + output.push(c); + } + } + + Cow::Owned(output) +} + #[derive(Debug, PartialEq, Clone, Copy, Default)] pub enum StripAnsiMode { #[default] @@ -211,3 +237,30 @@ fn test_strip_ansi() { "multiple sequences" ); } + +#[test] +fn test_strip_overstrike() { + // No overstrike - should return borrowed reference + assert_eq!(strip_overstrike("no overstrike"), "no overstrike"); + + // Empty string + assert_eq!(strip_overstrike(""), ""); + + // Bold: X\x08X (same char repeated) + assert_eq!(strip_overstrike("H\x08Hello"), "Hello"); + + // Underline: _\x08X (underscore before char) + assert_eq!(strip_overstrike("_\x08Hello"), "Hello"); + + // Multiple overstrike sequences + assert_eq!(strip_overstrike("B\x08Bo\x08ol\x08ld\x08d"), "Bold"); + + // Backspace at start of line (nothing to pop) + assert_eq!(strip_overstrike("\x08Hello"), "Hello"); + + // Multiple consecutive backspaces + assert_eq!(strip_overstrike("ABC\x08\x08\x08XYZ"), "XYZ"); + + // Unicode with overstrike + assert_eq!(strip_overstrike("ä\x08äöü"), "äöü"); +} diff --git a/src/printer.rs b/src/printer.rs index a28cb190..9079ca0e 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::vec::Vec; use nu_ansi_term::Color::{Fixed, Green, Red, Yellow}; @@ -29,8 +30,7 @@ use crate::error::*; use crate::input::OpenedInput; use crate::line_range::{MaxBufferedLineNumber, RangeCheckResult}; use crate::output::OutputHandle; -use crate::preprocessor::strip_ansi; -use crate::preprocessor::{expand_tabs, replace_nonprintable}; +use crate::preprocessor::{expand_tabs, replace_nonprintable, strip_ansi, strip_overstrike}; use crate::style::StyleComponent; use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; @@ -152,7 +152,7 @@ impl Printer for SimplePrinter<'_> { self.config.nonprintable_notation, ); write!(handle, "{line}")?; - } else { + } else if self.config.binary == BinaryBehavior::AsText { match handle { OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?, OutputHandle::FmtWrite(handle) => { @@ -166,6 +166,23 @@ impl Printer for SimplePrinter<'_> { )?; } } + } else { + match handle { + OutputHandle::IoWrite(handle) => { + // Only strip overstrike for valid UTF-8, otherwise write raw bytes + if let Ok(line) = std::str::from_utf8(line_buffer) { + let line = strip_overstrike(line); + handle.write_all(line.as_bytes())?; + } else { + handle.write_all(line_buffer)?; + } + } + OutputHandle::FmtWrite(handle) => { + let line = String::from_utf8_lossy(line_buffer); + let line = strip_overstrike(&line); + write!(handle, "{line}")?; + } + } }; } Ok(()) @@ -622,6 +639,11 @@ impl Printer for InteractivePrinter<'_> { } }; + // Strip overstrike sequences (used by man pages for bold/underline). + if line.contains('\x08') { + line = Cow::Owned(strip_overstrike(&line).into_owned()); + } + // If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting. if self.strip_ansi { line = strip_ansi(&line).into() From 80ef6832d3053453fc70b1a9ea6bcbd4c930dd9e Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 5 Dec 2025 14:31:37 +0100 Subject: [PATCH 02/11] Add a changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb40606b..080eda3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Bugfixes +- Strip overstriking to better support man pages, see #3517 (@akirk) - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) From 5fb8a25c309c88722d91f7be2c2df8d97eba9e28 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 9 Dec 2025 05:50:52 +0100 Subject: [PATCH 03/11] Performance optimization --- src/preprocessor.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/preprocessor.rs b/src/preprocessor.rs index 74486489..ecc79a2b 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -158,17 +158,24 @@ pub fn strip_ansi(line: &str) -> String { /// /// This function removes these sequences, keeping only the visible character. pub fn strip_overstrike(line: &str) -> Cow<'_, str> { - if !line.contains('\x08') { + let Some(first_bs) = line.find('\x08') else { return Cow::Borrowed(line); - } + }; let mut output = String::with_capacity(line.len()); + output.push_str(&line[..first_bs]); + output.pop(); - for c in line.chars() { - if c == '\x08' { + let mut remaining = &line[first_bs + 1..]; + + loop { + if let Some(bs_pos) = remaining.find('\x08') { + output.push_str(&remaining[..bs_pos]); output.pop(); + remaining = &remaining[bs_pos + 1..]; } else { - output.push(c); + output.push_str(remaining); + break; } } From 64a4b204a2acec916bcd919a3439e95ec41b45d6 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 9 Dec 2025 10:17:15 +0100 Subject: [PATCH 04/11] Only strip overstrike when a syntax highlighting theme is used --- CHANGELOG.md | 2 +- src/printer.rs | 22 +++++++------------ tests/examples/git-commit.man | 30 ++++++++++++++++++++++++++ tests/examples/overstrike.txt | 1 + tests/integration_tests.rs | 40 +++++++++++++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 tests/examples/git-commit.man create mode 100644 tests/examples/overstrike.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 080eda3f..dafc2347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ## Bugfixes -- Strip overstriking to better support man pages, see #3517 (@akirk) +- Strip overstriking before applying syntax highlighting to better support man pages, see #3517 (@akirk) - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) diff --git a/src/printer.rs b/src/printer.rs index 9079ca0e..7ce99632 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -168,19 +168,9 @@ impl Printer for SimplePrinter<'_> { } } else { match handle { - OutputHandle::IoWrite(handle) => { - // Only strip overstrike for valid UTF-8, otherwise write raw bytes - if let Ok(line) = std::str::from_utf8(line_buffer) { - let line = strip_overstrike(line); - handle.write_all(line.as_bytes())?; - } else { - handle.write_all(line_buffer)?; - } - } + OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?, OutputHandle::FmtWrite(handle) => { - let line = String::from_utf8_lossy(line_buffer); - let line = strip_overstrike(&line); - write!(handle, "{line}")?; + write!(handle, "{}", String::from_utf8_lossy(line_buffer))?; } } }; @@ -216,6 +206,7 @@ pub(crate) struct InteractivePrinter<'a> { background_color_highlight: Option, consecutive_empty_lines: usize, strip_ansi: bool, + strip_overstrike: bool, } impl<'a> InteractivePrinter<'a> { @@ -314,6 +305,9 @@ impl<'a> InteractivePrinter<'a> { _ => false, }; + // Strip overstrike only when we have syntax highlighting (not plain text). + let strip_overstrike = !is_plain_text; + Ok(InteractivePrinter { panel_width, colors, @@ -327,6 +321,7 @@ impl<'a> InteractivePrinter<'a> { background_color_highlight, consecutive_empty_lines: 0, strip_ansi, + strip_overstrike, }) } @@ -639,8 +634,7 @@ impl Printer for InteractivePrinter<'_> { } }; - // Strip overstrike sequences (used by man pages for bold/underline). - if line.contains('\x08') { + if self.strip_overstrike && line.contains('\x08') { line = Cow::Owned(strip_overstrike(&line).into_owned()); } diff --git a/tests/examples/git-commit.man b/tests/examples/git-commit.man new file mode 100644 index 00000000..a93a3808 --- /dev/null +++ b/tests/examples/git-commit.man @@ -0,0 +1,30 @@ +GIT-COMMIT(1) Git Manual GIT-COMMIT(1) + +NNAAMMEE + git-commit - Record changes to the repository + +SSYYNNOOPPSSIISS + ggiitt ccoommmmiitt [--aa | ----iinntteerraaccttiivvee | ----ppaattcchh] [--ss] [--vv] [--uu[_<_m_o_d_e_>]] [----aammeenndd] + [----ddrryy--rruunn] [(--cc | --CC | ----ssqquuaasshh) _<_c_o_m_m_i_t_> | ----ffiixxuupp [(aammeenndd|rreewwoorrdd)::]_<_c_o_m_m_i_t_>] + [--FF _<_f_i_l_e_> | --mm _<_m_s_g_>] [----rreesseett--aauutthhoorr] [----aallllooww--eemmppttyy] + [----aallllooww--eemmppttyy--mmeessssaaggee] [----nnoo--vveerriiffyy] [--ee] [----aauutthhoorr==_<_a_u_t_h_o_r_>] + [----ddaattee==_<_d_a_t_e_>] [----cclleeaannuupp==_<_m_o_d_e_>] [----[nnoo--]ssttaattuuss] + [--ii | --oo] [----ppaatthhssppeecc--ffrroomm--ffiillee==_<_f_i_l_e_> [----ppaatthhssppeecc--ffiillee--nnuull]] + [(----ttrraaiilleerr _<_t_o_k_e_n_>[(==|::)_<_v_a_l_u_e_>])...] [--SS[_<_k_e_y_i_d_>]] + [----] [_<_p_a_t_h_s_p_e_c_>...] + +DDEESSCCRRIIPPTTIIOONN + Create a new commit containing the current contents of the index and + the given log message describing the changes. The new commit is a + direct child of HEAD, usually the tip of the current branch, and the + branch is updated to point to it (unless no branch is associated with + the working tree, in which case HHEEAADD is "detached" as described in ggiitt-- + cchheecckkoouutt(1)). + + The content to be committed can be specified in several ways: + + 1. by using ggiitt--aadddd(1) to incrementally "add" changes to the index + before using the ccoommmmiitt command (Note: even modified files must be + "added"); + + 2. by using ggiitt--rrmm(1) to remove files from the working tree and the diff --git a/tests/examples/overstrike.txt b/tests/examples/overstrike.txt new file mode 100644 index 00000000..838764f3 --- /dev/null +++ b/tests/examples/overstrike.txt @@ -0,0 +1 @@ +BBold tteexxtt and _u_n_d_e_r_l_i_n_e diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 9a3d67ce..8822eab7 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2541,6 +2541,46 @@ fn binary_as_text() { .stderr(""); } +#[test] +fn no_strip_overstrike_for_plain_text() { + // Overstrike is preserved for plain text files (no syntax highlighting) + bat() + .arg("--color=never") + .arg("--decorations=never") + .arg("overstrike.txt") + .assert() + .success() + .stdout("B\x08Bold t\x08te\x08ex\x08xt\x08t and _\x08u_\x08n_\x08d_\x08e_\x08r_\x08l_\x08i_\x08n_\x08e\n") + .stderr(""); +} + +#[test] +fn strip_overstrike_with_syntax_highlighting() { + // Overstrike is stripped when syntax highlighting is applied (e.g., for help) + bat() + .arg("--force-colorization") + .arg("--language=help") + .arg("overstrike.txt") + .assert() + .success() + .stdout(predicate::str::contains("Bold text and underline")) + .stderr(""); +} + +#[test] +fn strip_overstrike_for_manpage_syntax() { + // Overstrike is stripped for .man files (Manpage syntax) + bat() + .arg("--force-colorization") + .arg("git-commit.man") + .assert() + .success() + .stdout(predicate::str::contains("NAME")) + .stdout(predicate::str::contains("git-commit - Record changes")) + .stdout(predicate::str::is_match(r"\x1b\[38;[0-9;]+m--interactive\x1b\[").unwrap()) + .stderr(""); +} + #[test] fn no_paging_arg() { bat() From f97f9ebf0382b69f157af4a814006eb896055e47 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Thu, 11 Dec 2025 05:19:21 +0100 Subject: [PATCH 05/11] Remove double backspace scan --- src/preprocessor.rs | 38 ++++++++++++++------------------------ src/printer.rs | 7 ++++--- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/preprocessor.rs b/src/preprocessor.rs index ecc79a2b..6b4e2935 100644 --- a/src/preprocessor.rs +++ b/src/preprocessor.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::fmt::Write; use crate::{ @@ -157,29 +156,26 @@ pub fn strip_ansi(line: &str) -> String { /// - Underline: `_\x08X` (underscore, backspace, character) /// /// This function removes these sequences, keeping only the visible character. -pub fn strip_overstrike(line: &str) -> Cow<'_, str> { - let Some(first_bs) = line.find('\x08') else { - return Cow::Borrowed(line); - }; - +/// `first_backspace` is the position of the first backspace in the line. +pub fn strip_overstrike(line: &str, first_backspace: usize) -> String { let mut output = String::with_capacity(line.len()); - output.push_str(&line[..first_bs]); + output.push_str(&line[..first_backspace]); output.pop(); - let mut remaining = &line[first_bs + 1..]; + let mut remaining = &line[first_backspace + 1..]; loop { - if let Some(bs_pos) = remaining.find('\x08') { - output.push_str(&remaining[..bs_pos]); + if let Some(pos) = remaining.find('\x08') { + output.push_str(&remaining[..pos]); output.pop(); - remaining = &remaining[bs_pos + 1..]; + remaining = &remaining[pos + 1..]; } else { output.push_str(remaining); break; } } - Cow::Owned(output) + output } #[derive(Debug, PartialEq, Clone, Copy, Default)] @@ -247,27 +243,21 @@ fn test_strip_ansi() { #[test] fn test_strip_overstrike() { - // No overstrike - should return borrowed reference - assert_eq!(strip_overstrike("no overstrike"), "no overstrike"); - - // Empty string - assert_eq!(strip_overstrike(""), ""); - // Bold: X\x08X (same char repeated) - assert_eq!(strip_overstrike("H\x08Hello"), "Hello"); + assert_eq!(strip_overstrike("H\x08Hello", 1), "Hello"); // Underline: _\x08X (underscore before char) - assert_eq!(strip_overstrike("_\x08Hello"), "Hello"); + assert_eq!(strip_overstrike("_\x08Hello", 1), "Hello"); // Multiple overstrike sequences - assert_eq!(strip_overstrike("B\x08Bo\x08ol\x08ld\x08d"), "Bold"); + assert_eq!(strip_overstrike("B\x08Bo\x08ol\x08ld\x08d", 1), "Bold"); // Backspace at start of line (nothing to pop) - assert_eq!(strip_overstrike("\x08Hello"), "Hello"); + assert_eq!(strip_overstrike("\x08Hello", 0), "Hello"); // Multiple consecutive backspaces - assert_eq!(strip_overstrike("ABC\x08\x08\x08XYZ"), "XYZ"); + assert_eq!(strip_overstrike("ABC\x08\x08\x08XYZ", 3), "XYZ"); // Unicode with overstrike - assert_eq!(strip_overstrike("ä\x08äöü"), "äöü"); + assert_eq!(strip_overstrike("ä\x08äöü", 2), "äöü"); } diff --git a/src/printer.rs b/src/printer.rs index 7ce99632..8cd0e1b0 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::vec::Vec; use nu_ansi_term::Color::{Fixed, Green, Red, Yellow}; @@ -634,8 +633,10 @@ impl Printer for InteractivePrinter<'_> { } }; - if self.strip_overstrike && line.contains('\x08') { - line = Cow::Owned(strip_overstrike(&line).into_owned()); + if self.strip_overstrike { + if let Some(pos) = line.find('\x08') { + line = strip_overstrike(&line, pos).into(); + } } // If ANSI escape sequences are supposed to be stripped, do it before syntax highlighting. From b22fc5db6b50b12dd6b6190c4383cf52869dfb7f Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Thu, 11 Dec 2025 05:20:02 +0100 Subject: [PATCH 06/11] Add show-all integration test --- tests/integration_tests.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 8822eab7..078daaf8 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2581,6 +2581,20 @@ fn strip_overstrike_for_manpage_syntax() { .stderr(""); } +#[test] +fn show_all_shows_backspace_with_caret_notation() { + // --show-all should display backspace characters (not strip them) + bat() + .arg("--show-all") + .arg("--nonprintable-notation=caret") + .arg("--decorations=never") + .arg("overstrike.txt") + .assert() + .success() + .stdout(predicate::str::contains("^H")) + .stderr(""); +} + #[test] fn no_paging_arg() { bat() From 59c58969024d48c0575c42250c7dd87a70106fbd Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Thu, 11 Dec 2025 05:20:50 +0100 Subject: [PATCH 07/11] Simplify MANPAGER guide --- CHANGELOG.md | 4 ++-- README.md | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dafc2347..91fc1ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ ## Features -## Bugfixes - - Strip overstriking before applying syntax highlighting to better support man pages, see #3517 (@akirk) + +## Bugfixes - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) diff --git a/README.md b/README.md index eb0da902..771db7ad 100644 --- a/README.md +++ b/README.md @@ -195,18 +195,13 @@ bat main.cpp | xclip `MANPAGER` environment variable: ```bash -export MANPAGER="sh -c 'awk '\''{ gsub(/\x1B\[[0-9;]*m/, \"\", \$0); gsub(/.\x08/, \"\", \$0); print }'\'' | bat -p -lman'" +export MANPAGER="bat -plman" man 2 select ``` (replace `bat` with `batcat` if you are on Debian or Ubuntu) If you prefer to have this bundled in a new command, you can also use [`batman`](https://github.com/eth-p/bat-extras/blob/master/doc/batman.md). -> [!WARNING] -> This will [not work](https://github.com/sharkdp/bat/issues/1145) out of the box with Mandoc's `man` implementation. -> -> Please either use `batman`, or convert the shell script to a [shebang executable](https://en.wikipedia.org/wiki/Shebang_(Unix)) and point `MANPAGER` to that. - Note that the [Manpage syntax](assets/syntaxes/02_Extra/Manpage.sublime-syntax) is developed in this repository and still needs some work. #### `prettier` / `shfmt` / `rustfmt` From de414ed631e96c3cff6d2dea19c04aaf40d9acb2 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Thu, 11 Dec 2025 05:45:44 +0100 Subject: [PATCH 08/11] Limit overstrike stripping to man pages and help --- src/printer.rs | 21 +++++++++++++-------- tests/integration_tests.rs | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/printer.rs b/src/printer.rs index 8cd0e1b0..499106ec 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -270,14 +270,18 @@ impl<'a> InteractivePrinter<'a> { || matches!(config.binary, BinaryBehavior::AsText)) && (config.colored_output || config.strip_ansi == StripAnsiMode::Auto); - let (is_plain_text, highlighter_from_set) = if needs_to_match_syntax { + let (is_plain_text, highlighter_from_set, syntax_name) = if needs_to_match_syntax { // Determine the type of syntax for highlighting const PLAIN_TEXT_SYNTAX: &str = "Plain Text"; match assets.get_syntax(config.language, input, &config.syntax_mapping) { - Ok(syntax_in_set) => ( - syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX, - Some(HighlighterFromSet::new(syntax_in_set, theme)), - ), + Ok(syntax_in_set) => { + let syntax_name = syntax_in_set.syntax.name.as_str(); + ( + syntax_name == PLAIN_TEXT_SYNTAX, + Some(HighlighterFromSet::new(syntax_in_set, theme)), + syntax_name, + ) + } Err(Error::UndetectedSyntax(_)) => ( true, @@ -287,12 +291,13 @@ impl<'a> InteractivePrinter<'a> { .map(|s| HighlighterFromSet::new(s, theme)) .expect("A plain text syntax is available"), ), + PLAIN_TEXT_SYNTAX, ), Err(e) => return Err(e), } } else { - (false, None) + (false, None, "") }; // Determine when to strip ANSI sequences @@ -304,8 +309,8 @@ impl<'a> InteractivePrinter<'a> { _ => false, }; - // Strip overstrike only when we have syntax highlighting (not plain text). - let strip_overstrike = !is_plain_text; + // Strip overstrike for man pages and help messages. + let strip_overstrike = matches!(syntax_name, "Manpage" | "Command Help"); Ok(InteractivePrinter { panel_width, diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 078daaf8..8896d9fa 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2556,7 +2556,7 @@ fn no_strip_overstrike_for_plain_text() { #[test] fn strip_overstrike_with_syntax_highlighting() { - // Overstrike is stripped when syntax highlighting is applied (e.g., for help) + // Overstrike is stripped for certain syntax highlighting like command help. bat() .arg("--force-colorization") .arg("--language=help") @@ -2581,6 +2581,19 @@ fn strip_overstrike_for_manpage_syntax() { .stderr(""); } +#[test] +fn no_strip_overstrike_for_other_syntax() { + // Overstrike is NOT stripped for other syntaxes (e.g., Rust) + bat() + .arg("--force-colorization") + .arg("--language=rust") + .arg("overstrike.txt") + .assert() + .success() + .stdout(predicate::str::contains("\x08")) + .stderr(""); +} + #[test] fn show_all_shows_backspace_with_caret_notation() { // --show-all should display backspace characters (not strip them) From 629a8968fca1c37e31a2e0deb13d4b540549e66c Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Thu, 11 Dec 2025 05:54:30 +0100 Subject: [PATCH 09/11] Remove unnecessary code change --- src/printer.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/printer.rs b/src/printer.rs index 499106ec..52ba50e9 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -151,7 +151,7 @@ impl Printer for SimplePrinter<'_> { self.config.nonprintable_notation, ); write!(handle, "{line}")?; - } else if self.config.binary == BinaryBehavior::AsText { + } else { match handle { OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?, OutputHandle::FmtWrite(handle) => { @@ -165,13 +165,6 @@ impl Printer for SimplePrinter<'_> { )?; } } - } else { - match handle { - OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?, - OutputHandle::FmtWrite(handle) => { - write!(handle, "{}", String::from_utf8_lossy(line_buffer))?; - } - } }; } Ok(()) From 4549ab3f2220f1800bf8ac1b9932ab38ccb9911d Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Thu, 11 Dec 2025 05:58:35 +0100 Subject: [PATCH 10/11] Move the strip_overstrike check --- src/printer.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/printer.rs b/src/printer.rs index 52ba50e9..1ddd5e66 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -263,34 +263,34 @@ impl<'a> InteractivePrinter<'a> { || matches!(config.binary, BinaryBehavior::AsText)) && (config.colored_output || config.strip_ansi == StripAnsiMode::Auto); - let (is_plain_text, highlighter_from_set, syntax_name) = if needs_to_match_syntax { + let (is_plain_text, strip_overstrike, highlighter_from_set) = if needs_to_match_syntax { // Determine the type of syntax for highlighting const PLAIN_TEXT_SYNTAX: &str = "Plain Text"; + const MANPAGE_SYNTAX: &str = "Manpage"; + const COMMAND_HELP_SYNTAX: &str = "Command Help"; match assets.get_syntax(config.language, input, &config.syntax_mapping) { - Ok(syntax_in_set) => { - let syntax_name = syntax_in_set.syntax.name.as_str(); - ( - syntax_name == PLAIN_TEXT_SYNTAX, - Some(HighlighterFromSet::new(syntax_in_set, theme)), - syntax_name, - ) - } + Ok(syntax_in_set) => ( + syntax_in_set.syntax.name == PLAIN_TEXT_SYNTAX, + syntax_in_set.syntax.name == MANPAGE_SYNTAX + || syntax_in_set.syntax.name == COMMAND_HELP_SYNTAX, + Some(HighlighterFromSet::new(syntax_in_set, theme)), + ), Err(Error::UndetectedSyntax(_)) => ( true, + false, Some( assets .find_syntax_by_name(PLAIN_TEXT_SYNTAX)? .map(|s| HighlighterFromSet::new(s, theme)) .expect("A plain text syntax is available"), ), - PLAIN_TEXT_SYNTAX, ), Err(e) => return Err(e), } } else { - (false, None, "") + (false, false, None) }; // Determine when to strip ANSI sequences @@ -302,9 +302,6 @@ impl<'a> InteractivePrinter<'a> { _ => false, }; - // Strip overstrike for man pages and help messages. - let strip_overstrike = matches!(syntax_name, "Manpage" | "Command Help"); - Ok(InteractivePrinter { panel_width, colors, From 4db185834b97f513346dec02029a7e9a4dc8c33f Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Thu, 11 Dec 2025 06:12:44 +0100 Subject: [PATCH 11/11] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91fc1ba7..ddefdcf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Features -- Strip overstriking before applying syntax highlighting to better support man pages, see #3517 (@akirk) +- Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) ## Bugfixes - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall)