summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDavid Peter <sharkdp@users.noreply.github.com>2024-02-23 21:48:44 +0100
committerGitHub <noreply@github.com>2024-02-23 21:48:44 +0100
commitb718889ba2a0fb734ad5ec55beab94849e6951f0 (patch)
treeba183170791d48195f9113d3db88e40eb626a6db /src
parent695cf1f387a970e2706c908d9597bfad85eb97ee (diff)
parent708c74f6af45ba8a56bab68d34e5aa12e1f44b25 (diff)
Merge branch 'master' into master
Diffstat (limited to 'src')
-rw-r--r--src/assets/assets_metadata.rs2
-rw-r--r--src/assets/lazy_theme_set.rs3
-rw-r--r--src/bin/bat/app.rs1
-rw-r--r--src/bin/bat/clap_app.rs7
-rw-r--r--src/bin/bat/main.rs24
-rw-r--r--src/config.rs3
-rw-r--r--src/decorations.rs2
-rw-r--r--src/printer.rs67
-rw-r--r--src/syntax_mapping/builtins/common/50-aws-credentials.toml2
-rw-r--r--src/syntax_mapping/builtins/common/50-json.toml (renamed from src/syntax_mapping/builtins/common/50-jsonl.toml)2
-rw-r--r--src/vscreen.rs779
11 files changed, 829 insertions, 63 deletions
diff --git a/src/assets/assets_metadata.rs b/src/assets/assets_metadata.rs
index 700c4c3b..cfc7a9e0 100644
--- a/src/assets/assets_metadata.rs
+++ b/src/assets/assets_metadata.rs
@@ -3,7 +3,7 @@ use std::path::Path;
use std::time::SystemTime;
use semver::Version;
-use serde::{Deserialize, Serialize};
+use serde_derive::{Deserialize, Serialize};
use crate::error::*;
diff --git a/src/assets/lazy_theme_set.rs b/src/assets/lazy_theme_set.rs
index bf749154..fcc3eb46 100644
--- a/src/assets/lazy_theme_set.rs
+++ b/src/assets/lazy_theme_set.rs
@@ -3,8 +3,7 @@ use super::*;
use std::collections::BTreeMap;
use std::convert::TryFrom;
-use serde::Deserialize;
-use serde::Serialize;
+use serde_derive::{Deserialize, Serialize};
use once_cell::unsync::OnceCell;
diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs
index a2c09770..8843d53b 100644
--- a/src/bin/bat/app.rs
+++ b/src/bin/bat/app.rs
@@ -289,6 +289,7 @@ impl App {
use_custom_assets: !self.matches.get_flag("no-custom-assets"),
#[cfg(feature = "lessopen")]
use_lessopen: self.matches.get_flag("lessopen"),
+ set_terminal_title: self.matches.get_flag("set-terminal-title"),
})
}
diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs
index e8222a1d..6ceed784 100644
--- a/src/bin/bat/clap_app.rs
+++ b/src/bin/bat/clap_app.rs
@@ -567,6 +567,13 @@ pub fn build_app(interactive_output: bool) -> Command {
.action(ArgAction::SetTrue)
.hide_short_help(true)
.help("Show acknowledgements."),
+ )
+ .arg(
+ Arg::new("set-terminal-title")
+ .long("set-terminal-title")
+ .action(ArgAction::SetTrue)
+ .hide_short_help(true)
+ .help("Sets terminal title to filenames when using a pager."),
);
// Check if the current directory contains a file name cache. Otherwise,
diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs
index f48abdc1..d877bb9b 100644
--- a/src/bin/bat/main.rs
+++ b/src/bin/bat/main.rs
@@ -229,9 +229,33 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<
Ok(())
}
+fn set_terminal_title_to(new_terminal_title: String) {
+ let osc_command_for_setting_terminal_title = "\x1b]0;";
+ let osc_end_command = "\x07";
+ print!(
+ "{}{}{}",
+ osc_command_for_setting_terminal_title, new_terminal_title, osc_end_command
+ );
+ io::stdout().flush().unwrap();
+}
+
+fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
+ let mut new_terminal_title = "bat: ".to_string();
+ for (index, input) in inputs.iter().enumerate() {
+ new_terminal_title += input.description().title();
+ if index < inputs.len() - 1 {
+ new_terminal_title += ", ";
+ }
+ }
+ new_terminal_title
+}
+
fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Result<bool> {
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
let controller = Controller::new(config, &assets);
+ if config.paging_mode != PagingMode::Never && config.set_terminal_title {
+ set_terminal_title_to(get_new_terminal_title(&inputs));
+ }
controller.run(inputs, None)
}
diff --git a/src/config.rs b/src/config.rs
index 83acc7df..c5cc2abd 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -94,6 +94,9 @@ pub struct Config<'a> {
// Whether or not to use $LESSOPEN if set
#[cfg(feature = "lessopen")]
pub use_lessopen: bool,
+
+ // Weather or not to set terminal title when using a pager
+ pub set_terminal_title: bool,
}
#[cfg(all(feature = "minimal-application", feature = "paging"))]
diff --git a/src/decorations.rs b/src/decorations.rs
index d3ed9b34..85d8103a 100644
--- a/src/decorations.rs
+++ b/src/decorations.rs
@@ -46,7 +46,7 @@ impl Decoration for LineNumberDecoration {
_printer: &InteractivePrinter,
) -> DecorationText {
if continuation {
- if line_number > self.cached_wrap_invalid_at {
+ if line_number >= self.cached_wrap_invalid_at {
let new_width = self.cached_wrap.width + 1;
return DecorationText {
text: self.color.paint(" ".repeat(new_width)).to_string(),
diff --git a/src/printer.rs b/src/printer.rs
index 257cc766..f413fdc3 100644
--- a/src/printer.rs
+++ b/src/printer.rs
@@ -7,8 +7,6 @@ use nu_ansi_term::Style;
use bytesize::ByteSize;
-use console::AnsiCodeIterator;
-
use syntect::easy::HighlightLines;
use syntect::highlighting::Color;
use syntect::highlighting::Theme;
@@ -33,9 +31,23 @@ use crate::line_range::RangeCheckResult;
use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color};
-use crate::vscreen::AnsiStyle;
+use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
use crate::wrapping::WrappingMode;
+const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
+ raw_sequence: "\x1B[4m",
+ parameters: "4",
+ intermediates: "",
+ final_byte: "m",
+};
+
+const ANSI_UNDERLINE_DISABLE: EscapeSequence = EscapeSequence::CSI {
+ raw_sequence: "\x1B[24m",
+ parameters: "24",
+ intermediates: "",
+ final_byte: "m",
+};
+
pub enum OutputHandle<'a> {
IoWrite(&'a mut dyn io::Write),
FmtWrite(&'a mut dyn fmt::Write),
@@ -554,7 +566,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
if highlight_this_line && self.config.theme == "ansi" {
- self.ansi_style.update("^[4m");
+ self.ansi_style.update(ANSI_UNDERLINE_ENABLE);
}
let background_color = self
@@ -581,23 +593,17 @@ impl<'a> Printer for InteractivePrinter<'a> {
let italics = self.config.use_italic_text;
for &(style, region) in &regions {
- let ansi_iterator = AnsiCodeIterator::new(region);
+ let ansi_iterator = EscapeSequenceIterator::new(region);
for chunk in ansi_iterator {
match chunk {
- // ANSI escape passthrough.
- (ansi, true) => {
- self.ansi_style.update(ansi);
- write!(handle, "{}", ansi)?;
- }
-
// Regular text.
- (text, false) => {
- let text = &*self.preprocess(text, &mut cursor_total);
+ EscapeSequence::Text(text) => {
+ let text = self.preprocess(text, &mut cursor_total);
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
write!(
handle,
- "{}",
+ "{}{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, text_trimmed),
@@ -605,9 +611,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
colored_output,
italics,
background_color
- )
+ ),
+ self.ansi_style.to_reset_sequence(),
)?;
+ // Pad the rest of the line.
if text.len() != text_trimmed.len() {
if let Some(background_color) = background_color {
let ansi_style = Style {
@@ -625,6 +633,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}
+
+ // ANSI escape passthrough.
+ _ => {
+ write!(handle, "{}", chunk.raw())?;
+ self.ansi_style.update(chunk);
+ }
}
}
}
@@ -634,17 +648,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
} else {
for &(style, region) in &regions {
- let ansi_iterator = AnsiCodeIterator::new(region);
+ let ansi_iterator = EscapeSequenceIterator::new(region);
for chunk in ansi_iterator {
match chunk {
- // ANSI escape passthrough.
- (ansi, true) => {
- self.ansi_style.update(ansi);
- write!(handle, "{}", ansi)?;
- }
-
// Regular text.
- (text, false) => {
+ EscapeSequence::Text(text) => {
let text = self.preprocess(
text.trim_end_matches(|c| c == '\r' || c == '\n'),
&mut cursor_total,
@@ -687,7 +695,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
// It wraps.
write!(
handle,
- "{}\n{}",
+ "{}{}\n{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, line_buf),
@@ -696,6 +704,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.use_italic_text,
background_color
),
+ self.ansi_style.to_reset_sequence(),
panel_wrap.clone().unwrap()
)?;
@@ -724,6 +733,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
)
)?;
}
+
+ // ANSI escape passthrough.
+ _ => {
+ write!(handle, "{}", chunk.raw())?;
+ self.ansi_style.update(chunk);
+ }
}
}
}
@@ -744,8 +759,8 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
if highlight_this_line && self.config.theme == "ansi" {
- self.ansi_style.update("^[24m");
- write!(handle, "\x1B[24m")?;
+ write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?;
+ self.ansi_style.update(ANSI_UNDERLINE_DISABLE);
}
Ok(())
diff --git a/src/syntax_mapping/builtins/common/50-aws-credentials.toml b/src/syntax_mapping/builtins/common/50-aws-credentials.toml
new file mode 100644
index 00000000..a16e6e8f
--- /dev/null
+++ b/src/syntax_mapping/builtins/common/50-aws-credentials.toml
@@ -0,0 +1,2 @@
+[mappings]
+"INI" = ["**/.aws/credentials", "**/.aws/config"]
diff --git a/src/syntax_mapping/builtins/common/50-jsonl.toml b/src/syntax_mapping/builtins/common/50-json.toml
index 4b70a4d0..e604868a 100644
--- a/src/syntax_mapping/builtins/common/50-jsonl.toml
+++ b/src/syntax_mapping/builtins/common/50-json.toml
@@ -1,3 +1,3 @@
# JSON Lines is a simple variation of JSON #2535
[mappings]
-"JSON" = ["*.jsonl"]
+"JSON" = ["*.jsonl", "*.jsonc"]
diff --git a/src/vscreen.rs b/src/vscreen.rs
index ea5d4da6..f7ba3f91 100644
--- a/src/vscreen.rs
+++ b/src/vscreen.rs
@@ -1,4 +1,8 @@
-use std::fmt::{Display, Formatter};
+use std::{
+ fmt::{Display, Formatter},
+ iter::Peekable,
+ str::CharIndices,
+};
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
pub struct AnsiStyle {
@@ -10,7 +14,7 @@ impl AnsiStyle {
AnsiStyle { attributes: None }
}
- pub fn update(&mut self, sequence: &str) -> bool {
+ pub fn update(&mut self, sequence: EscapeSequence) -> bool {
match &mut self.attributes {
Some(a) => a.update(sequence),
None => {
@@ -19,6 +23,13 @@ impl AnsiStyle {
}
}
}
+
+ pub fn to_reset_sequence(&self) -> String {
+ match self.attributes {
+ Some(ref 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,13 +165,14 @@ 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(),
- 1 => self.bold = format!("\x1B[{}m", parameters),
- 2 => self.dim = format!("\x1B[{}m", parameters),
- 3 => self.italic = format!("\x1B[{}m", parameters),
- 4 => self.underline = format!("\x1B[{}m", parameters),
+ 1 => self.bold = "\x1B[1m".to_owned(),
+ 2 => self.dim = "\x1B[2m".to_owned(),
+ 3 => self.italic = "\x1B[3m".to_owned(),
+ 4 => self.underline = "\x1B[4m".to_owned(),
23 => self.italic.clear(),
24 => self.underline.clear(),
22 => {
@@ -138,7 +183,7 @@ impl Attributes {
40..=49 => self.background = Self::parse_color(p, &mut iter),
58..=59 => self.underlined = Self::parse_color(p, &mut iter),
90..=97 => self.foreground = Self::parse_color(p, &mut iter),
- 100..=107 => self.foreground = Self::parse_color(p, &mut iter),
+ 100..=107 => self.background = Self::parse_color(p, &mut iter),
_ => {
// Unsupported SGR sequence.
// Be compatible and pretend one just wasn't was provided.
@@ -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,646 @@ 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,
+ },
+ #[allow(clippy::upper_case_acronyms)]
+ NF {
+ // https://en.wikipedia.org/wiki/ANSI_escape_code#nF_Escape_sequences
+ start_sequence: usize,
+ start: usize,
+ end: usize,
+ },
+ #[allow(clippy::upper_case_acronyms)]
+ 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,
+ },
+ #[allow(clippy::upper_case_acronyms)]
+ 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)> {
+ self.chars.peek()?;
+
+ 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> {
+ self.chars_take_while(|c| c != '\x1B')
+ .map(|(start, end)| 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,
+ 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.
+ if let Some((i, c)) = self.chars.next() {
+ end = i + c.len_utf8()
+ }
+
+ 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),
+ #[allow(clippy::upper_case_acronyms)]
+ NF {
+ raw_sequence: &'a str,
+ nf_sequence: &'a str,
+ },
+ #[allow(clippy::upper_case_acronyms)]
+ OSC {
+ raw_sequence: &'a str,
+ command: &'a str,
+ terminator: &'a str,
+ },
+ #[allow(clippy::upper_case_acronyms)]
+ 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 })