summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEthan P <eth-p+git@hidden.email>2024-02-09 22:20:30 -0800
committerGitHub <noreply@github.com>2024-02-09 22:20:30 -0800
commit5a2a20af42e7a426943122524287844acc5ee975 (patch)
treedb5e36141eb4ee909f470ea404657d6122f06300
parentc29bf2ff281f5190f90ce377eb98dac29045b6c4 (diff)
parent61029c8bd264f2bbce1844fd074c8136a9cec2ba (diff)
Merge pull request #2544 from eth-p/fix-2541
Treat OSC ANSI Sequences as Invisible Text & Add OSC 8 Support
-rw-r--r--CHANGELOG.md1
-rw-r--r--src/printer.rs67
-rw-r--r--src/vscreen.rs735
-rw-r--r--tests/examples/regression_tests/issue_2541.txt1
-rw-r--r--tests/integration_tests.rs70
5 files changed, 821 insertions, 53 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ee759a4..b6e13469 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
- Fix long file name wrapping in header, see #2835 (@FilipRazek)
- Fix `NO_COLOR` support, see #2767 (@acuteenvy)
+- Fix handling of inputs with OSC ANSI escape sequences, see #2541 and #2544 (@eth-p)
## Other
diff --git a/src/printer.rs b/src/printer.rs
index 257cc766..f413fdc3 100644
--- a/src/printer.rs
+++ b/src/printer.rs
@@ -7,8 +7,6 @@ use nu_ansi_term::Style;
use bytesize::ByteSize;
-use console::AnsiCodeIterator;
-
use syntect::easy::HighlightLines;
use syntect::highlighting::Color;
use syntect::highlighting::Theme;
@@ -33,9 +31,23 @@ 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::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
use crate::wrapping::WrappingMode;
+const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
+ raw_sequence: "\x1B[4m",
+ parameters: "4",
+ intermediates: "",
+ final_byte: "m",
+};
+
+const ANSI_UNDERLINE_DISABLE: EscapeSequence = EscapeSequence::CSI {
+ raw_sequence: "\x1B[24m",
+ parameters: "24",
+ intermediates: "",
+ final_byte: "m",
+};
+
pub enum OutputHandle<'a> {
IoWrite(&'a mut dyn io::Write),
FmtWrite(&'a mut dyn fmt::Write),
@@ -554,7 +566,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" {
- self.ansi_style.update("^[4m");
+ self.ansi_style.update(ANSI_UNDERLINE_ENABLE);
}
let background_color = self
@@ -581,23 +593,17 @@ impl<'a> Printer for InteractivePrinter<'a> {
let italics = self.config.use_italic_text;
for &(style, region) in &regions {
- let ansi_iterator = AnsiCodeIterator::new(region);
+ let ansi_iterator = EscapeSequenceIterator::new(region);
for chunk in ansi_iterator {
match chunk {
- // ANSI escape passthrough.
- (ansi, true) => {
- self.ansi_style.update(ansi);
- write!(handle, "{}", ansi)?;
- }
-
// Regular text.
- (text, false) => {
- let text = &*self.preprocess(text, &mut cursor_total);
+ EscapeSequence::Text(text) => {
+ 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),
@@ -605,9 +611,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
colored_output,
italics,
background_color
- )
+ ),
+ self.ansi_style.to_reset_sequence(),
)?;
+ // Pad the rest of the line.
if text.len() != text_trimmed.len() {
if let Some(background_color) = background_color {
let ansi_style = Style {
@@ -625,6 +633,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}
+
+ // ANSI escape passthrough.
+ _ => {
+ write!(handle, "{}", chunk.raw())?;
+ self.ansi_style.update(chunk);
+ }
}
}
}
@@ -634,17 +648,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
} else {
for &(style, region) in &regions {
- let ansi_iterator = AnsiCodeIterator::new(region);
+ let ansi_iterator = EscapeSequenceIterator::new(region);
for chunk in ansi_iterator {
match chunk {
- // ANSI escape passthrough.
- (ansi, true) => {
- self.ansi_style.update(ansi);
- write!(handle, "{}", ansi)?;
- }
-
// Regular text.
- (text, false) => {
+ EscapeSequence::Text(text) => {
let text = self.preprocess(
text.trim_end_matches(|c| c == '\r' || c == '\n'),
&mut cursor_total,
@@ -687,7 +695,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
// It wraps.
write!(
handle,
- "{}\n{}",
+ "{}{}\n{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, line_buf),
@@ -696,6 +704,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.use_italic_text,
background_color
),
+ self.ansi_style.to_reset_sequence(),
panel_wrap.clone().unwrap()
)?;
@@ -724,6 +733,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
)
)?;
}
+
+ // ANSI escape passthrough.
+ _ => {
+ write!(handle, "{}", chunk.raw())?;
+ self.ansi_style.update(chunk);
+ }
}
}
}
@@ -744,8 +759,8 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
if highlight_this_line && self.config.theme == "ansi" {
- self.ansi_style.update("^[24m");
- write!(handle, "\x1B[24m")?;
+ write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?;
+ self.ansi_style.update(ANSI_UNDERLINE_DISABLE);
}
Ok(())
diff --git a/src/vscreen.rs b/src/vscreen.rs
index ea5d4da6..c902d42b 100644
--- a/src/vscreen.rs
+++ b/src/vscreen.rs
@@ -1,4 +1,8 @@
-use std::fmt::{Display, Formatter};
+use std::{
+ fmt::{Display, Formatter},
+ iter::Peekable,
+ str::CharIndices,
+};
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
pub struct AnsiStyle {
@@ -10,7 +14,7 @@ impl AnsiStyle {
AnsiStyle { attributes: None }
}
- pub fn update(&mut self, sequence: &str) -> bool {
+ pub fn update(&mut self, sequence: EscapeSequence) -> bool {
match &mut self.attributes {
Some(a) => a.update(sequence),
None => {
@@ -19,6 +23,13 @@ impl AnsiStyle {
}
}
}
+
+ pub fn to_reset_sequence(&mut self) -> String {
+ match &mut self.attributes {
+ Some(a) => a.to_reset_sequence(),
+ None => String::new(),
+ }
+ }
}
impl Display for AnsiStyle {
@@ -31,6 +42,8 @@ impl Display for AnsiStyle {
}
struct Attributes {
+ has_sgr_sequences: bool,
+
foreground: String,
background: String,
underlined: String,
@@ -61,11 +74,20 @@ struct Attributes {
/// ON: ^[9m
/// OFF: ^[29m
strike: String,
+
+ /// The hyperlink sequence.
+ /// FORMAT: \x1B]8;{ID};{URL}\e\\
+ ///
+ /// `\e\\` may be replaced with BEL `\x07`.
+ /// Setting both {ID} and {URL} to an empty string represents no hyperlink.
+ hyperlink: String,
}
impl Attributes {
pub fn new() -> Self {
Attributes {
+ has_sgr_sequences: false,
+
foreground: "".to_owned(),
background: "".to_owned(),
underlined: "".to_owned(),
@@ -76,34 +98,56 @@ impl Attributes {
underline: "".to_owned(),
italic: "".to_owned(),
strike: "".to_owned(),
+ hyperlink: "".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
+ pub fn update(&mut self, sequence: EscapeSequence) -> bool {
+ use EscapeSequence::*;
+ match sequence {
+ Text(_) => return false,
+ Unknown(_) => { /* defer to update_with_unsupported */ }
+ OSC {
+ raw_sequence,
+ command,
+ ..
+ } => {
+ if command.starts_with("8;") {
+ return self.update_with_hyperlink(raw_sequence);
+ }
+ /* defer to update_with_unsupported */
+ }
+ CSI {
+ final_byte,
+ parameters,
+ ..
+ } => {
+ match final_byte {
+ "m" => return self.update_with_sgr(parameters),
+ _ => {
+ // NOTE(eth-p): We might want to ignore these, since they involve cursor or buffer manipulation.
+ /* defer to update_with_unsupported */
}
}
- _ => self.update_with_unsupported(sequence),
}
- } else {
- false
+ NF { nf_sequence, .. } => {
+ let mut iter = nf_sequence.chars();
+ match iter.next() {
+ Some('(') => return self.update_with_charset('(', iter),
+ Some(')') => return self.update_with_charset(')', iter),
+ _ => { /* defer to update_with_unsupported */ }
+ }
+ }
}
+
+ self.update_with_unsupported(sequence.raw())
}
fn sgr_reset(&mut self) {
+ self.has_sgr_sequences = false;
+
self.foreground.clear();
self.background.clear();
self.underlined.clear();
@@ -121,6 +165,7 @@ impl Attributes {
.map(|p| p.parse::<u16>())
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
+ self.has_sgr_sequences = true;
while let Some(p) = iter.next() {
match p {
0 => self.sgr_reset(),
@@ -149,19 +194,23 @@ impl Attributes {
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_hyperlink(&mut self, sequence: &str) -> bool {
+ if sequence == "8;;" {
+ // Empty hyperlink ID and HREF -> end of hyperlink.
+ self.hyperlink.clear();
+ } else {
+ self.hyperlink.clear();
+ self.hyperlink.push_str(sequence);
+ }
+
+ true
+ }
+
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
@@ -179,13 +228,35 @@ impl Attributes {
_ => format!("\x1B[{}m", color),
}
}
+
+ /// Gets an ANSI escape sequence to reset all the known attributes.
+ pub fn to_reset_sequence(&self) -> String {
+ let mut buf = String::with_capacity(17);
+
+ // TODO: Enable me in a later pull request.
+ // if self.has_sgr_sequences {
+ // buf.push_str("\x1B[m");
+ // }
+
+ if !self.hyperlink.is_empty() {
+ buf.push_str("\x1B]8;;\x1B\\"); // Disable hyperlink.
+ }
+
+ // TODO: Enable me in a later pull request.
+ // if !self.charset.is_empty() {
+ // // https://espterm.github.io/docs/VT100%20escape%20codes.html
+ // buf.push_str("\x1B(B\x1B)B"); // setusg0 and setusg1
+ // }
+
+ buf
+ }
}
impl Display for Attributes {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
- "{}{}{}{}{}{}{}{}{}",
+ "{}{}{}{}{}{}{}{}{}{}",
self.foreground,
self.background,
self.underlined,
@@ -195,6 +266,7 @@ impl Display for Attributes {
self.underline,
self.italic,
self.strike,
+ self.hyperlink,
)
}
}
@@ -210,3 +282,612 @@ fn join(
.collect::<Vec<String>>()
.join(delimiter)
}
+
+/// A range of indices for a raw ANSI escape sequence.
+#[derive(Debug, PartialEq)]
+enum EscapeSequenceOffsets {
+ Text {
+ start: usize,
+ end: usize,
+ },
+ Unknown {
+ start: usize,
+ end: usize,
+ },
+ NF {
+ // https://en.wikipedia.org/wiki/ANSI_escape_code#nF_Escape_sequences
+ start_sequence: usize,
+ start: usize,
+ end: usize,
+ },
+ OSC {
+ // https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences
+ start_sequence: usize,
+ start_command: usize,
+ start_terminator: usize,
+ end: usize,
+ },
+ CSI {
+ // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
+ start_sequence: usize,
+ start_parameters: usize,
+ start_intermediates: usize,
+ start_final_byte: usize,
+ end: usize,
+ },
+}
+
+/// An iterator over the offests of ANSI/VT escape sequences within a string.
+///
+/// ## Example
+///
+/// ```ignore
+/// let iter = EscapeSequenceOffsetsIterator::new("\x1B[33mThis is yellow text.\x1B[m");
+/// ```
+struct EscapeSequenceOffsetsIterator<'a> {
+ text: &'a str,
+ chars: Peekable<CharIndices<'a>>,
+}
+
+impl<'a> EscapeSequenceOffsetsIterator<'a> {
+ pub fn new(text: &'a str) -> EscapeSequenceOffsetsIterator<'a> {
+ return EscapeSequenceOffsetsIterator {
+ text,
+ chars: text.char_indices().peekable(),
+ };
+ }
+
+ /// Takes values from the iterator while the predicate returns true.
+ /// If the predicate returns false, that value is left.
+ fn chars_take_while(&mut self, pred: impl Fn(char) -> bool) -> Option<(usize, usize)> {
+ if self.chars.peek().is_none() {
+ return None;
+ }
+
+ let start = self.chars.peek().unwrap().0;
+ let mut end: usize = start;
+ while let Some((i, c)) = self.chars.peek() {
+ if !pred(*c) {
+ break;
+ }
+
+ end = *i + c.len_utf8();
+ self.chars.next();
+ }
+
+ Some((start, end))
+ }
+
+ fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
+ match self.chars_take_while(|c| c != '\x1B') {
+ None => None,
+ Some((start, end)) => Some(EscapeSequenceOffsets::Text { start, end }),
+ }
+ }
+
+ fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
+ let (start_sequence, c) = self.chars.next().expect("to not be finished");
+ match self.chars.peek() {
+ None => Some(EscapeSequenceOffsets::Unknown {
+ start: start_sequence,
+ end: start_sequence + c.len_utf8(),
+ }),
+
+ Some((_, ']')) => self.next_osc(start_sequence),
+ Some((_, '[')) => self.next_csi(start_sequence),
+ Some((i, c)) => match c {
+ '\x20'..='\x2F' => self.next_nf(start_sequence),
+ c => Some(EscapeSequenceOffsets::Unknown {
+ start: start_sequence,
+ end: i + c.len_utf8(),
+ }),
+ },
+ }
+ }
+
+ fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
+ let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished");
+ debug_assert_eq!(osc_open_char, ']');
+
+ let mut start_terminator: usize;
+ let mut end_sequence: usize;
+
+ loop {
+ match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
+ None => {
+ start_terminator = self.text.len();
+ end_sequence = start_terminator;
+ break;
+ }
+
+ Some((_, end)) => {
+ start_terminator = end;
+ end_sequence = end;
+ }
+ }
+
+ match self.chars.next() {
+ Some((ti, '\x07')) => {
+ end_sequence = ti + '\x07'.len_utf8();
+ break;
+ }
+
+ Some((ti, '\x1B')) => {
+ match self.chars.next() {
+ Some((i, '\\')) => {
+ end_sequence = i + '\\'.len_utf8();
+ break;
+ }
+
+ None => {
+ end_sequence = ti + '\x1B'.len_utf8();
+ break;
+ }
+
+ _ => {
+ // Repeat, since `\\`(anything) isn't a valid ST.
+ }
+ }
+ }
+
+ None => {
+ // Prematurely ends.
+ break;
+ }
+
+ Some((_, tc)) => {
+ panic!("this should not be reached: char {:?}", tc)
+ }
+ }
+ }
+
+ Some(EscapeSequenceOffsets::OSC {
+ start_sequence,
+ start_command: osc_open_index + osc_open_char.len_utf8(),
+ start_terminator: start_terminator,
+ end: end_sequence,
+ })
+ }
+
+ fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
+ let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
+ debug_assert_eq!(csi_open_char, '[');
+
+ let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
+
+ // Keep iterating while within the range of `0x30-0x3F`.
+ let mut start_intermediates: usize = start_parameters;
+ if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
+ start_intermediates = end;
+ }
+
+ // Keep iterating while within the range of `0x20-0x2F`.
+ let mut start_final_byte: usize = start_intermediates;
+ if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
+ start_final_byte = end;
+ }
+
+ // Take the last char.
+ let end_of_sequence = match self.chars.next() {
+ None => start_final_byte,
+ Some((i, c)) => i + c.len_utf8(),
+ };
+
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence,
+ start_parameters,
+ start_intermediates,
+ start_final_byte,
+ end: end_of_sequence,
+ })
+ }
+
+ fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
+ let (nf_open_index, nf_open_char) = self.chars.next().expect("to not be finished");
+ debug_assert!(matches!(nf_open_char, '\x20'..='\x2F'));
+
+ let start: usize = nf_open_index;
+ let mut end: usize = start;
+
+ // Keep iterating while within the range of `0x20-0x2F`.
+ match self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
+ Some((_, i)) => end = i,
+ None => {
+ return Some(EscapeSequenceOffsets::NF {
+ start_sequence,
+ start,
+ end,
+ })
+ }
+ }
+
+ // Get the final byte.
+ match self.chars.next() {
+ Some((i, c)) => end = i + c.len_utf8(),
+ None => {}
+ }
+
+ Some(EscapeSequenceOffsets::NF {
+ start_sequence,
+ start,
+ end,
+ })
+ }
+}
+
+impl<'a> Iterator for EscapeSequenceOffsetsIterator<'a> {
+ type Item = EscapeSequenceOffsets;
+ fn next(&mut self) -> Option<Self::Item> {
+ match self.chars.peek() {
+ Some((_, '\x1B')) => self.next_sequence(),
+ Some((_, _)) => self.next_text(),
+ None => None,
+ }
+ }
+}
+
+/// An iterator over ANSI/VT escape sequences within a string.
+///
+/// ## Example
+///
+/// ```ignore
+/// let iter = EscapeSequenceIterator::new("\x1B[33mThis is yellow text.\x1B[m");
+/// ```
+pub struct EscapeSequenceIterator<'a> {
+ text: &'a str,
+ offset_iter: EscapeSequenceOffsetsIterator<'a>,
+}
+
+impl<'a> EscapeSequenceIterator<'a> {
+ pub fn new(text: &'a str) -> EscapeSequenceIterator<'a> {
+ return EscapeSequenceIterator {
+ text,
+ offset_iter: EscapeSequenceOffsetsIterator::new(text),
+ };
+ }
+}
+
+impl<'a> Iterator for EscapeSequenceIterator<'a> {
+ type Item = EscapeSequence<'a>;
+ fn next(&mut self) -> Option<Self::Item> {
+ use EscapeSequenceOffsets::*;
+ self.offset_iter.next().map(|offsets| match offsets {
+ Unknown { start, end } => EscapeSequence::Unknown(&self.text[start..end]),
+ Text { start, end } => EscapeSequence::Text(&self.text[start..end]),
+ NF {
+ start_sequence,
+ start,
+ end,
+ } => EscapeSequence::NF {
+ raw_sequence: &self.text[start_sequence..end],
+ nf_sequence: &self.text[start..end],
+ },
+ OSC {
+ start_sequence,
+ start_command,
+ start_terminator,
+ end,
+ } => EscapeSequence::OSC {
+ raw_sequence: &self.text[start_sequence..end],
+ command: &self.text[start_command..start_terminator],
+ terminator: &self.text[start_terminator..end],
+ },
+ CSI {
+ start_sequence,
+ start_parameters,
+ start_intermediates,
+ start_final_byte,
+ end,
+ } => EscapeSequence::CSI {
+ raw_sequence: &self.text[start_sequence..end],
+ parameters: &self.text[start_parameters..start_intermediates],
+ intermediates: &self.text[start_intermediates..start_final_byte],
+ final_byte: &self.text[start_final_byte..end],
+ },
+ })
+ }
+}
+
+/// A parsed ANSI/VT100 escape sequence.
+#[derive(Debug, PartialEq)]
+pub enum EscapeSequence<'a> {
+ Text(&'a str),
+ Unknown(&'a str),
+ NF {
+ raw_sequence: &'a str,
+ nf_sequence: &'a str,
+ },
+ OSC {
+ raw_sequence: &'a str,
+ command: &'a str,
+ terminator: &'a str,
+ },
+ CSI {
+ raw_sequence: &'a str,
+ parameters: &'a str,
+ intermediates: &'a str,
+ final_byte: &'a str,
+ },
+}
+
+impl<'a> EscapeSequence<'a> {
+ pub fn raw(&self) -> &'a str {
+ use EscapeSequence::*;
+ match *self {
+ Text(raw) => raw,
+ Unknown(raw) => raw,
+ NF { raw_sequence, .. } => raw_sequence,
+ OSC { raw_sequence, .. } => raw_sequence,
+ CSI { raw_sequence, .. } => raw_sequence,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::vscreen::{
+ EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets,
+ EscapeSequenceOffsetsIterator,
+ };
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_text() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("text");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_text_stops_at_esc() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[ming");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_osc_with_bel() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x07");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::OSC {
+ start_sequence: 0,
+ start_command: 2,
+ start_terminator: 5,
+ end: 6,
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_osc_with_st() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]abc\x1B\\");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::OSC {
+ start_sequence: 0,
+ start_command: 2,
+ start_terminator: 5,
+ end: 7,
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_osc_thats_broken() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B]ab");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::OSC {
+ start_sequence: 0,
+ start_command: 2,
+ start_terminator: 4,
+ end: 4,
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_csi() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[m");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 0,
+ start_parameters: 2,
+ start_intermediates: 2,
+ start_final_byte: 2,
+ end: 3
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1;34m");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 0,
+ start_parameters: 2,
+ start_intermediates: 6,
+ start_final_byte: 6,
+ end: 7
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_csi_with_intermediates() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[$m");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 0,
+ start_parameters: 2,
+ start_intermediates: 2,
+ start_final_byte: 3,
+ end: 4
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_csi_with_parameters_and_intermediates() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$m");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 0,
+ start_parameters: 2,
+ start_intermediates: 3,
+ start_final_byte: 4,
+ end: 5
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_csi_thats_broken() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 0,
+ start_parameters: 2,
+ start_intermediates: 2,
+ start_final_byte: 2,
+ end: 2
+ })
+ );
+
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 0,
+ start_parameters: 2,
+ start_intermediates: 3,
+ start_final_byte: 3,
+ end: 3
+ })
+ );
+
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B[1$");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 0,
+ start_parameters: 2,
+ start_intermediates: 3,
+ start_final_byte: 4,
+ end: 4
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_nf() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B($0");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::NF {
+ start_sequence: 0,
+ start: 1,
+ end: 4
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_parses_nf_thats_broken() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("\x1B(");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::NF {
+ start_sequence: 0,
+ start: 1,
+ end: 1
+ })
+ );
+ }
+
+ #[test]
+ fn test_escape_sequence_offsets_iterator_iterates() {
+ let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[33m\x1B]OSC\x07\x1B(0");
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::CSI {
+ start_sequence: 4,
+ start_parameters: 6,
+ start_intermediates: 8,
+ start_final_byte: 8,
+ end: 9
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::OSC {
+ start_sequence: 9,
+ start_command: 11,
+ start_terminator: 14,
+ end: 15
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequenceOffsets::NF {
+ start_sequence: 15,
+ start: 16,
+ end: 18
+ })
+ );
+ assert_eq!(iter.next(), None);
+ }
+
+ #[test]
+ fn test_escape_sequence_iterator_iterates() {
+ let mut iter = EscapeSequenceIterator::new("text\x1B[33m\x1B]OSC\x07\x1B]OSC\x1B\\\x1B(0");
+ assert_eq!(iter.next(), Some(EscapeSequence::Text("text")));
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequence::CSI {
+ raw_sequence: "\x1B[33m",
+ parameters: "33",
+ intermediates: "",
+ final_byte: "m",
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequence::OSC {
+ raw_sequence: "\x1B]OSC\x07",
+ command: "OSC",
+ terminator: "\x07",
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequence::OSC {
+ raw_sequence: "\x1B]OSC\x1B\\",
+ command: "OSC",
+ terminator: "\x1B\\",
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(EscapeSequence::NF {
+ raw_sequence: "\x1B(0",
+ nf_sequence: "(0",
+ })
+ );
+ assert_eq!(iter.next(), None);
+ }
+}
diff --git a/tests/examples/regression_tests/issue_2541.txt b/tests/examples/regression_tests/issue_2541.txt
new file mode 100644
index 00000000..1059b94e
--- /dev/null
+++ b/tests/examples/regression_tests/issue_2541.txt
@@ -0,0 +1 @@
+]8;;http://example.com\This is a l