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:
Ethan P 2024-02-09 22:20:30 -08:00 committed by GitHub
commit 5a2a20af42
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 821 additions and 53 deletions

View File

@ -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

View File

@ -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(())

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
]8;;http://example.com\This is a link]8;;\n

View File

@ -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]
fn can_print_file_named_cache() {
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]
fn ignored_suffix_arg() {
bat()