mirror of
				https://github.com/sharkdp/bat.git
				synced 2025-10-30 22:54:07 +00:00 
			
		
		
		
	Merge pull request #2544 from eth-p/fix-2541
Treat OSC ANSI Sequences as Invisible Text & Add OSC 8 Support
This commit is contained in:
		| @@ -6,6 +6,7 @@ | |||||||
|  |  | ||||||
| - Fix long file name wrapping in header, see #2835 (@FilipRazek) | - Fix long file name wrapping in header, see #2835 (@FilipRazek) | ||||||
| - Fix `NO_COLOR` support, see #2767 (@acuteenvy) | - Fix `NO_COLOR` support, see #2767 (@acuteenvy) | ||||||
|  | - Fix handling of inputs with OSC ANSI escape sequences, see #2541 and #2544 (@eth-p) | ||||||
|  |  | ||||||
| ## Other | ## Other | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,8 +7,6 @@ use nu_ansi_term::Style; | |||||||
|  |  | ||||||
| use bytesize::ByteSize; | use bytesize::ByteSize; | ||||||
|  |  | ||||||
| use console::AnsiCodeIterator; |  | ||||||
|  |  | ||||||
| use syntect::easy::HighlightLines; | use syntect::easy::HighlightLines; | ||||||
| use syntect::highlighting::Color; | use syntect::highlighting::Color; | ||||||
| use syntect::highlighting::Theme; | use syntect::highlighting::Theme; | ||||||
| @@ -33,9 +31,23 @@ use crate::line_range::RangeCheckResult; | |||||||
| use crate::preprocessor::{expand_tabs, replace_nonprintable}; | use crate::preprocessor::{expand_tabs, replace_nonprintable}; | ||||||
| use crate::style::StyleComponent; | use crate::style::StyleComponent; | ||||||
| use crate::terminal::{as_terminal_escaped, to_ansi_color}; | use crate::terminal::{as_terminal_escaped, to_ansi_color}; | ||||||
| use crate::vscreen::AnsiStyle; | use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator}; | ||||||
| use crate::wrapping::WrappingMode; | 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> { | pub enum OutputHandle<'a> { | ||||||
|     IoWrite(&'a mut dyn io::Write), |     IoWrite(&'a mut dyn io::Write), | ||||||
|     FmtWrite(&'a mut dyn fmt::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; |             self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange; | ||||||
|  |  | ||||||
|         if highlight_this_line && self.config.theme == "ansi" { |         if highlight_this_line && self.config.theme == "ansi" { | ||||||
|             self.ansi_style.update("^[4m"); |             self.ansi_style.update(ANSI_UNDERLINE_ENABLE); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let background_color = self |         let background_color = self | ||||||
| @@ -581,23 +593,17 @@ impl<'a> Printer for InteractivePrinter<'a> { | |||||||
|             let italics = self.config.use_italic_text; |             let italics = self.config.use_italic_text; | ||||||
|  |  | ||||||
|             for &(style, region) in ®ions { |             for &(style, region) in ®ions { | ||||||
|                 let ansi_iterator = AnsiCodeIterator::new(region); |                 let ansi_iterator = EscapeSequenceIterator::new(region); | ||||||
|                 for chunk in ansi_iterator { |                 for chunk in ansi_iterator { | ||||||
|                     match chunk { |                     match chunk { | ||||||
|                         // ANSI escape passthrough. |  | ||||||
|                         (ansi, true) => { |  | ||||||
|                             self.ansi_style.update(ansi); |  | ||||||
|                             write!(handle, "{}", ansi)?; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         // Regular text. |                         // Regular text. | ||||||
|                         (text, false) => { |                         EscapeSequence::Text(text) => { | ||||||
|                             let text = &*self.preprocess(text, &mut cursor_total); |                             let text = self.preprocess(text, &mut cursor_total); | ||||||
|                             let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n'); |                             let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n'); | ||||||
|  |  | ||||||
|                             write!( |                             write!( | ||||||
|                                 handle, |                                 handle, | ||||||
|                                 "{}", |                                 "{}{}", | ||||||
|                                 as_terminal_escaped( |                                 as_terminal_escaped( | ||||||
|                                     style, |                                     style, | ||||||
|                                     &format!("{}{}", self.ansi_style, text_trimmed), |                                     &format!("{}{}", self.ansi_style, text_trimmed), | ||||||
| @@ -605,9 +611,11 @@ impl<'a> Printer for InteractivePrinter<'a> { | |||||||
|                                     colored_output, |                                     colored_output, | ||||||
|                                     italics, |                                     italics, | ||||||
|                                     background_color |                                     background_color | ||||||
|                                 ) |                                 ), | ||||||
|  |                                 self.ansi_style.to_reset_sequence(), | ||||||
|                             )?; |                             )?; | ||||||
|  |  | ||||||
|  |                             // Pad the rest of the line. | ||||||
|                             if text.len() != text_trimmed.len() { |                             if text.len() != text_trimmed.len() { | ||||||
|                                 if let Some(background_color) = background_color { |                                 if let Some(background_color) = background_color { | ||||||
|                                     let ansi_style = Style { |                                     let ansi_style = Style { | ||||||
| @@ -625,6 +633,12 @@ impl<'a> Printer for InteractivePrinter<'a> { | |||||||
|                                 write!(handle, "{}", &text[text_trimmed.len()..])?; |                                 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 { |         } else { | ||||||
|             for &(style, region) in ®ions { |             for &(style, region) in ®ions { | ||||||
|                 let ansi_iterator = AnsiCodeIterator::new(region); |                 let ansi_iterator = EscapeSequenceIterator::new(region); | ||||||
|                 for chunk in ansi_iterator { |                 for chunk in ansi_iterator { | ||||||
|                     match chunk { |                     match chunk { | ||||||
|                         // ANSI escape passthrough. |  | ||||||
|                         (ansi, true) => { |  | ||||||
|                             self.ansi_style.update(ansi); |  | ||||||
|                             write!(handle, "{}", ansi)?; |  | ||||||
|                         } |  | ||||||
|  |  | ||||||
|                         // Regular text. |                         // Regular text. | ||||||
|                         (text, false) => { |                         EscapeSequence::Text(text) => { | ||||||
|                             let text = self.preprocess( |                             let text = self.preprocess( | ||||||
|                                 text.trim_end_matches(|c| c == '\r' || c == '\n'), |                                 text.trim_end_matches(|c| c == '\r' || c == '\n'), | ||||||
|                                 &mut cursor_total, |                                 &mut cursor_total, | ||||||
| @@ -687,7 +695,7 @@ impl<'a> Printer for InteractivePrinter<'a> { | |||||||
|                                     // It wraps. |                                     // It wraps. | ||||||
|                                     write!( |                                     write!( | ||||||
|                                         handle, |                                         handle, | ||||||
|                                         "{}\n{}", |                                         "{}{}\n{}", | ||||||
|                                         as_terminal_escaped( |                                         as_terminal_escaped( | ||||||
|                                             style, |                                             style, | ||||||
|                                             &format!("{}{}", self.ansi_style, line_buf), |                                             &format!("{}{}", self.ansi_style, line_buf), | ||||||
| @@ -696,6 +704,7 @@ impl<'a> Printer for InteractivePrinter<'a> { | |||||||
|                                             self.config.use_italic_text, |                                             self.config.use_italic_text, | ||||||
|                                             background_color |                                             background_color | ||||||
|                                         ), |                                         ), | ||||||
|  |                                         self.ansi_style.to_reset_sequence(), | ||||||
|                                         panel_wrap.clone().unwrap() |                                         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" { |         if highlight_this_line && self.config.theme == "ansi" { | ||||||
|             self.ansi_style.update("^[24m"); |             write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?; | ||||||
|             write!(handle, "\x1B[24m")?; |             self.ansi_style.update(ANSI_UNDERLINE_DISABLE); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         Ok(()) |         Ok(()) | ||||||
|   | |||||||
							
								
								
									
										735
									
								
								src/vscreen.rs
									
									
									
									
									
								
							
							
						
						
									
										735
									
								
								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. | // Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences. | ||||||
| pub struct AnsiStyle { | pub struct AnsiStyle { | ||||||
| @@ -10,7 +14,7 @@ impl AnsiStyle { | |||||||
|         AnsiStyle { attributes: None } |         AnsiStyle { attributes: None } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn update(&mut self, sequence: &str) -> bool { |     pub fn update(&mut self, sequence: EscapeSequence) -> bool { | ||||||
|         match &mut self.attributes { |         match &mut self.attributes { | ||||||
|             Some(a) => a.update(sequence), |             Some(a) => a.update(sequence), | ||||||
|             None => { |             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 { | impl Display for AnsiStyle { | ||||||
| @@ -31,6 +42,8 @@ impl Display for AnsiStyle { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct Attributes { | struct Attributes { | ||||||
|  |     has_sgr_sequences: bool, | ||||||
|  |  | ||||||
|     foreground: String, |     foreground: String, | ||||||
|     background: String, |     background: String, | ||||||
|     underlined: String, |     underlined: String, | ||||||
| @@ -61,11 +74,20 @@ struct Attributes { | |||||||
|     /// ON:  ^[9m |     /// ON:  ^[9m | ||||||
|     /// OFF: ^[29m |     /// OFF: ^[29m | ||||||
|     strike: String, |     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 { | impl Attributes { | ||||||
|     pub fn new() -> Self { |     pub fn new() -> Self { | ||||||
|         Attributes { |         Attributes { | ||||||
|  |             has_sgr_sequences: false, | ||||||
|  |  | ||||||
|             foreground: "".to_owned(), |             foreground: "".to_owned(), | ||||||
|             background: "".to_owned(), |             background: "".to_owned(), | ||||||
|             underlined: "".to_owned(), |             underlined: "".to_owned(), | ||||||
| @@ -76,34 +98,56 @@ impl Attributes { | |||||||
|             underline: "".to_owned(), |             underline: "".to_owned(), | ||||||
|             italic: "".to_owned(), |             italic: "".to_owned(), | ||||||
|             strike: "".to_owned(), |             strike: "".to_owned(), | ||||||
|  |             hyperlink: "".to_owned(), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /// Update the attributes with an escape sequence. |     /// Update the attributes with an escape sequence. | ||||||
|     /// Returns `false` if the sequence is unsupported. |     /// Returns `false` if the sequence is unsupported. | ||||||
|     pub fn update(&mut self, sequence: &str) -> bool { |     pub fn update(&mut self, sequence: EscapeSequence) -> bool { | ||||||
|         let mut chars = sequence.char_indices().skip(1); |         use EscapeSequence::*; | ||||||
|  |         match sequence { | ||||||
|         if let Some((_, t)) = chars.next() { |             Text(_) => return false, | ||||||
|             match t { |             Unknown(_) => { /* defer to update_with_unsupported */ } | ||||||
|                 '(' => self.update_with_charset('(', chars.map(|(_, c)| c)), |             OSC { | ||||||
|                 ')' => self.update_with_charset(')', chars.map(|(_, c)| c)), |                 raw_sequence, | ||||||
|                 '[' => { |                 command, | ||||||
|                     if let Some((i, last)) = chars.last() { |                 .. | ||||||
|                         // SAFETY: Always starts with ^[ and ends with m. |             } => { | ||||||
|                         self.update_with_csi(last, &sequence[2..i]) |                 if command.starts_with("8;") { | ||||||
|                     } else { |                     return self.update_with_hyperlink(raw_sequence); | ||||||
|                         false |                 } | ||||||
|  |                 /* 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 { |             NF { nf_sequence, .. } => { | ||||||
|             false |                 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) { |     fn sgr_reset(&mut self) { | ||||||
|  |         self.has_sgr_sequences = false; | ||||||
|  |  | ||||||
|         self.foreground.clear(); |         self.foreground.clear(); | ||||||
|         self.background.clear(); |         self.background.clear(); | ||||||
|         self.underlined.clear(); |         self.underlined.clear(); | ||||||
| @@ -121,6 +165,7 @@ impl Attributes { | |||||||
|             .map(|p| p.parse::<u16>()) |             .map(|p| p.parse::<u16>()) | ||||||
|             .map(|p| p.unwrap_or(0)); // Treat errors as 0. |             .map(|p| p.unwrap_or(0)); // Treat errors as 0. | ||||||
|  |  | ||||||
|  |         self.has_sgr_sequences = true; | ||||||
|         while let Some(p) = iter.next() { |         while let Some(p) = iter.next() { | ||||||
|             match p { |             match p { | ||||||
|                 0 => self.sgr_reset(), |                 0 => self.sgr_reset(), | ||||||
| @@ -149,19 +194,23 @@ impl Attributes { | |||||||
|         true |         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 { |     fn update_with_unsupported(&mut self, sequence: &str) -> bool { | ||||||
|         self.unknown_buffer.push_str(sequence); |         self.unknown_buffer.push_str(sequence); | ||||||
|         false |         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 { |     fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool { | ||||||
|         self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>()); |         self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>()); | ||||||
|         true |         true | ||||||
| @@ -179,13 +228,35 @@ impl Attributes { | |||||||
|             _ => format!("\x1B[{}m", color), |             _ => 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 { | impl Display for Attributes { | ||||||
|     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { | ||||||
|         write!( |         write!( | ||||||
|             f, |             f, | ||||||
|             "{}{}{}{}{}{}{}{}{}", |             "{}{}{}{}{}{}{}{}{}{}", | ||||||
|             self.foreground, |             self.foreground, | ||||||
|             self.background, |             self.background, | ||||||
|             self.underlined, |             self.underlined, | ||||||
| @@ -195,6 +266,7 @@ impl Display for Attributes { | |||||||
|             self.underline, |             self.underline, | ||||||
|             self.italic, |             self.italic, | ||||||
|             self.strike, |             self.strike, | ||||||
|  |             self.hyperlink, | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @@ -210,3 +282,612 @@ fn join( | |||||||
|         .collect::<Vec<String>>() |         .collect::<Vec<String>>() | ||||||
|         .join(delimiter) |         .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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								tests/examples/regression_tests/issue_2541.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/examples/regression_tests/issue_2541.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | ]8;;http://example.com\This is a link]8;;\n | ||||||
| @@ -1163,6 +1163,20 @@ fn bom_stripped_when_no_color_and_not_loop_through() { | |||||||
|         ); |         ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Regression test for https://github.com/sharkdp/bat/issues/2541 | ||||||
|  | #[test] | ||||||
|  | fn no_broken_osc_emit_with_line_wrapping() { | ||||||
|  |     bat() | ||||||
|  |         .arg("--color=always") | ||||||
|  |         .arg("--decorations=never") | ||||||
|  |         .arg("--wrap=character") | ||||||
|  |         .arg("--terminal-width=40") | ||||||
|  |         .arg("regression_tests/issue_2541.txt") | ||||||
|  |         .assert() | ||||||
|  |         .success() | ||||||
|  |         .stdout(predicate::function(|s: &str| s.lines().count() == 1)); | ||||||
|  | } | ||||||
|  |  | ||||||
| #[test] | #[test] | ||||||
| fn can_print_file_named_cache() { | fn can_print_file_named_cache() { | ||||||
|     bat_with_config() |     bat_with_config() | ||||||
| @@ -1919,6 +1933,62 @@ fn ansi_passthrough_emit() { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines. | ||||||
|  | // This also helps ensure that escape sequences are counted as part of the visible characters when wrapping. | ||||||
|  | #[test] | ||||||
|  | fn ansi_sgr_emitted_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...............Also color.\n") | ||||||
|  |         .assert() | ||||||
|  |         .success() | ||||||
|  |         .stdout("\x1B[33m\x1B[33mColor...............\n\x1B[33mAlso color.\n") | ||||||
|  |         // FIXME:              ~~~~~~~~ should not be emitted twice. | ||||||
|  |         .stderr(""); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Ensure that a simple ANSI sequence passthrough is emitted properly on wrapped lines. | ||||||
|  | // This also helps ensure that escape sequences are counted as part of the visible characters when wrapping. | ||||||
|  | #[test] | ||||||
|  | fn ansi_hyperlink_emitted_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]8;;http://example.com/\x1B\\Hyperlinks..........Wrap across lines.\n") | ||||||
|  |         .assert() | ||||||
|  |         .success() | ||||||
|  |         .stdout("\x1B]8;;http://example.com/\x1B\\\x1B]8;;http://example.com/\x1B\\Hyperlinks..........\x1B]8;;\x1B\\\n\x1B]8;;http://example.com/\x1B\\Wrap across lines.\n") | ||||||
|  |         // FIXME:                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ should not be emitted twice. | ||||||
|  |         .stderr(""); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Ensure that multiple ANSI sequence SGR attributes are combined when emitted on wrapped lines. | ||||||
|  | #[test] | ||||||
|  | fn ansi_sgr_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[1mBold.........Also bold and color.\n") | ||||||
|  |             .assert() | ||||||
|  |             .success() | ||||||
|  |             .stdout("\x1B[33m\x1B[33mColor. \x1B[1m\x1B[33m\x1B[1mBold.........\n\x1B[33m\x1B[1mAlso bold and color.\n") | ||||||
|  |             // FIXME:              ~~~~~~~~       ~~~~~~~~~~~~~~~ should not be emitted twice. | ||||||
|  |             .stderr(""); | ||||||
|  | } | ||||||
|  |  | ||||||
| #[test] | #[test] | ||||||
| fn ignored_suffix_arg() { | fn ignored_suffix_arg() { | ||||||
|     bat() |     bat() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user