From 156ef1d2f477692dc9f17b58cdb0f9395586dcee Mon Sep 17 00:00:00 2001 From: mikehasa Date: Tue, 22 Sep 2026 04:33:12 -0700 Subject: [PATCH] Track strikethrough (SGR 9) so it is not dropped across highlighted tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Attributes` ANSI style tracker in `src/vscreen.rs` declares, initializes, clears (in `sgr_reset`), and re-emits (in `Display`) a `strike` field documented as `ON: ^[9m / OFF: ^[29m`, but `update_with_sgr()` had no match arm for SGR 9 (set strikethrough) or 29 (clear strikethrough). As a result `strike` was only ever cleared, never set. Because bat re-applies the tracked input ANSI style before each highlighted text run (nu_ansi_term appends a full reset after every token), attributes that are tracked — bold, dim, italic, underline — survive across tokens, while strikethrough was silently dropped after the first token. This path is active by default (`--strip-ansi`/`--sanitize` default to `never`). Add the missing `9` and `29` arms, mirroring the sibling underline (4/24) and italic (3/23) arms, so strikethrough is tracked and re-emitted like every other supported attribute. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + src/vscreen.rs | 32 +++++++++++++++++++++++++++++--- tests/integration_tests.rs | 19 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fee6615b..8d5bcb2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Syntax highlighting for Python files using uv as script runner in shebang #3689 (@janlarres) ## Bugfixes +- Track strikethrough (SGR 9 / 29) in the ANSI style tracker so it is re-emitted like bold, dim, italic, and underline, and is no longer dropped after the first highlighted token when displaying ANSI input with syntax highlighting, see #4015 (@mikehasa) - Allow boolean flags to be given more than once, so that a flag set in the config file can also be passed on the command line without erroring, see #3912 (@logarithmone1128) - Use parsed CLI arguments to detect number flags, see #3908 (@cuishuang) - Detect binary content beyond the first line, preventing encrypted files with early line breaks from being treated as text. Closes #3554, see #3877 (@Matei02355) diff --git a/src/vscreen.rs b/src/vscreen.rs index 6478f577..b5a8134a 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -173,8 +173,10 @@ impl Attributes { 2 => self.dim = "\x1B[2m".to_owned(), 3 => self.italic = "\x1B[3m".to_owned(), 4 => self.underline = "\x1B[4m".to_owned(), + 9 => self.strike = "\x1B[9m".to_owned(), 23 => self.italic.clear(), 24 => self.underline.clear(), + 29 => self.strike.clear(), 22 => { self.bold.clear(); self.dim.clear(); @@ -1112,10 +1114,10 @@ mod tests { fn test_sgr_attributes_do_not_leak_into_wrong_field() { let mut attrs = crate::vscreen::Attributes::new(); - // Bold, Dim, Italic, Underline, Foreground, Background + // Bold, Dim, Italic, Underline, Strike, Foreground, Background attrs.update(EscapeSequence::CSI { - raw_sequence: "\x1B[1;2;3;4;31;41m", - parameters: "1;2;3;4;31;41", + raw_sequence: "\x1B[1;2;3;4;9;31;41m", + parameters: "1;2;3;4;9;31;41", intermediates: "", final_byte: "m", }); @@ -1124,6 +1126,7 @@ mod tests { assert_eq!(attrs.dim, "\x1B[2m"); assert_eq!(attrs.italic, "\x1B[3m"); assert_eq!(attrs.underline, "\x1B[4m"); + assert_eq!(attrs.strike, "\x1B[9m"); assert_eq!(attrs.foreground, "\x1B[31m"); assert_eq!(attrs.background, "\x1B[41m"); @@ -1140,4 +1143,27 @@ mod tests { assert_eq!(attrs.foreground, "\x1B[94m"); assert_eq!(attrs.background, "\x1B[103m"); } + + #[test] + fn test_sgr_strike_is_tracked_and_cleared() { + let mut attrs = crate::vscreen::Attributes::new(); + + // Strikethrough on (SGR 9) must be tracked so it can be re-emitted. + attrs.update(EscapeSequence::CSI { + raw_sequence: "\x1B[9m", + parameters: "9", + intermediates: "", + final_byte: "m", + }); + assert_eq!(attrs.strike, "\x1B[9m"); + + // Strikethrough off (SGR 29) must clear it, just like italic (23) and underline (24). + attrs.update(EscapeSequence::CSI { + raw_sequence: "\x1B[29m", + parameters: "29", + intermediates: "", + final_byte: "m", + }); + assert_eq!(attrs.strike, ""); + } } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 3f15f281..5d682c8c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3355,6 +3355,25 @@ fn ansi_sgr_joins_attributes_when_wrapped() { .stderr(""); } +// Ensure that strikethrough (SGR 9) is tracked like the other attributes, so it is +// re-emitted (and therefore preserved) across a wrap boundary instead of being dropped. +#[test] +fn ansi_sgr_strike_joins_attributes_when_wrapped() { + bat() + .arg("--paging=never") + .arg("--color=never") + .arg("--terminal-width=20") + .arg("--wrap=character") + .arg("--decorations=always") + .arg("--style=plain") + .write_stdin("\x1B[33mColor. \x1B[9mStrike.......Struck+color.\n") + .assert() + .success() + .stdout("\x1B[33m\x1B[33mColor. \x1B[9m\x1B[33m\x1B[9mStrike.......\n\x1B[33m\x1B[9mStruck+color.\n") + // FIXME: ~~~~~~~~ ~~~~~~~~~~~~~~~ should not be emitted twice. + .stderr(""); +} + #[test] fn ignored_suffix_arg() { bat()