From 64a4b204a2acec916bcd919a3439e95ec41b45d6 Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 9 Dec 2025 10:17:15 +0100 Subject: [PATCH] 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()