mirror of
https://github.com/sharkdp/bat.git
synced 2024-11-24 16:53:55 +01: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:
commit
5a2a20af42
@ -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
|
||||
|
||||
|
@ -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 ®ions {
|
||||
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 ®ions {
|
||||
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(())
|
||||
|
741
src/vscreen.rs
741
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);
|
||||
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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
_ => self.update_with_unsupported(sequence),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
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]
|
||||
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()
|
||||
|
Loading…
Reference in New Issue
Block a user