diff options
| author | akbash <akbash@herdr.dev> | 2026-08-20 00:17:25 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-20 00:17:25 +0300 |
| commit | 4d17957c43938ef49aaf395fa8ff954c604e0e35 (patch) | |
| tree | f36ed8874c9eb304206ceb8857f25d8825bb2ba5 /src/app | |
| parent | 4ace235de0ccfa5ebf4619de4e922a62889f7038 (diff) | |
fix: strip ansi sequences from tab bar status (#3003)
* fix: strip ansi sequences from tab bar status
refs #3001
* docs: clarify tab bar escape sequence scope
refs #3001
---------
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/tab_bar_status.rs | 109 |
1 files changed, 108 insertions, 1 deletions
diff --git a/src/app/tab_bar_status.rs b/src/app/tab_bar_status.rs index 4a617c1d..eca5a314 100644 --- a/src/app/tab_bar_status.rs +++ b/src/app/tab_bar_status.rs @@ -277,9 +277,75 @@ fn is_unicode_format_control(character: char) -> bool { fn command_output_text(output: &[u8]) -> Option<String> { let output = String::from_utf8_lossy(output); + let output = strip_terminal_control_sequences(output.as_bytes()); + let output = String::from_utf8_lossy(&output); output.lines().next_back().and_then(sanitize_status_text) } +#[derive(Clone, Copy)] +enum ControlSequenceState { + Text, + Escape, + EscapeIntermediate, + Csi, + Osc, + StString, +} + +fn strip_terminal_control_sequences(value: &[u8]) -> Vec<u8> { + use ControlSequenceState::*; + + let mut output = Vec::with_capacity(value.len()); + let mut state = Text; + for &byte in value { + state = match (state, byte) { + (Text, b'\x1b') => Escape, + (Text, _) => { + output.push(byte); + Text + } + (Escape, b'[') => Csi, + (Escape, b']') => Osc, + (Escape, b'P' | b'X' | b'^' | b'_') => StString, + (Escape, 0x20..=0x2f) => EscapeIntermediate, + (Escape, 0x30..=0x7e) => Text, + (Escape, b'\x1b') => Escape, + (Escape, b'\x18' | b'\x1a') => Text, + (Escape, byte) if byte.is_ascii_control() => Escape, + (Escape, _) => { + output.push(byte); + Text + } + (EscapeIntermediate, 0x20..=0x2f) => EscapeIntermediate, + (EscapeIntermediate, 0x30..=0x7e) => Text, + (EscapeIntermediate, b'\x1b') => Escape, + (EscapeIntermediate, b'\x18' | b'\x1a') => Text, + (EscapeIntermediate, byte) if byte.is_ascii_control() => EscapeIntermediate, + (EscapeIntermediate, _) => { + output.push(byte); + Text + } + (Csi, 0x20..=0x3f) => Csi, + (Csi, 0x40..=0x7e) => Text, + (Csi, b'\x1b') => Escape, + (Csi, b'\x18' | b'\x1a') => Text, + (Csi, byte) if byte.is_ascii_control() => Csi, + (Csi, _) => { + output.push(byte); + Text + } + (Osc, b'\x07') => Text, + (Osc, b'\x1b') => Escape, + (Osc, b'\x18' | b'\x1a') => Text, + (Osc, _) => Osc, + (StString, b'\x1b') => Escape, + (StString, b'\x18' | b'\x1a') => Text, + (StString, _) => StString, + }; + } + output +} + async fn read_last_output_line( mut stdout: tokio::process::ChildStdout, ) -> std::io::Result<Vec<u8>> { @@ -699,12 +765,53 @@ mod tests { fn command_output_uses_sanitized_last_line() { assert_eq!( command_output_text(b"old\n win\x1b[31mter\r\n"), - Some("win[31mter".into()) + Some("winter".into()) ); assert_eq!(command_output_text(b"\r\n"), None); } #[test] + fn command_output_strips_ansi_style_sequences() { + assert_eq!( + command_output_text(b"\x1b[32mHELLO\x1b[0m"), + Some("HELLO".into()) + ); + } + + #[test] + fn command_output_strips_terminal_control_sequence_families() { + assert_eq!( + command_output_text(b"\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\"), + Some("link".into()) + ); + assert_eq!( + command_output_text(b"\x1bPignored\x1b\\visible\x1b7"), + Some("visible".into()) + ); + assert_eq!(command_output_text(b"\x1b[31m\x1b[0m"), None); + assert_eq!( + command_output_text(b"\x1b\x07[32mHELLO\x1b[0m"), + Some("HELLO".into()) + ); + assert_eq!( + command_output_text(b"\x1bPignored\x18VISIBLE"), + Some("VISIBLE".into()) + ); + assert_eq!( + command_output_text(b"\x1bPignored\x1b7VISIBLE"), + Some("VISIBLE".into()) + ); + assert_eq!( + command_output_text(b"\x1b]ignored\x1aVISIBLE"), + Some("VISIBLE".into()) + ); + assert_eq!(command_output_text(b"\xc2\x1b[31m\xa2"), Some("��".into())); + + let styled = format!("\x1b[38;2;1;2;3m{}\x1b[0m", "x".repeat(80)); + assert_eq!(command_output_text(styled.as_bytes()), Some("x".repeat(80))); + } + + #[test] fn status_text_strips_bidi_and_zero_width_format_controls() { assert_eq!( sanitize_status_text("safe\u{202e}evil\u{200b}"), |
