From 2e103ee6b3703e120025d43f04fe9a6d7ae92b5e Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sun, 10 Dec 2023 16:39:34 +0000 Subject: able to set terminal title to hardcoded value --- src/pager.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/pager.rs b/src/pager.rs index 5b707779..bdd5eb7f 100644 --- a/src/pager.rs +++ b/src/pager.rs @@ -1,5 +1,6 @@ use shell_words::ParseError; -use std::env; +use std::{env, io}; +use std::io::Write; /// If we use a pager, this enum tells us from where we were told to use it. #[derive(Debug, PartialEq)] @@ -36,6 +37,16 @@ pub(crate) enum PagerKind { Unknown, } +fn set_terminal_title(title: &str) { + print!("\x1b]2;{}\x07", title); + io::stdout().flush().unwrap(); +} + +fn restore_terminal_title() { + print!("\x1b]2;\x07"); + io::stdout().flush().unwrap(); +} + impl PagerKind { fn from_bin(bin: &str) -> PagerKind { use std::path::Path; @@ -102,6 +113,7 @@ pub(crate) fn get_pager(config_pager: Option<&str>) -> Result, Par }; let parts = shell_words::split(cmd)?; + set_terminal_title("test"); match parts.split_first() { Some((bin, args)) => { let kind = PagerKind::from_bin(bin); -- cgit v1.2.3 From 4863d428dd7004b471960fdf1c6b30417d94383d Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sun, 10 Dec 2023 16:44:47 +0000 Subject: title is being reset on quit, so no need to restore terminal title --- src/pager.rs | 5 ----- 1 file changed, 5 deletions(-) (limited to 'src') diff --git a/src/pager.rs b/src/pager.rs index bdd5eb7f..052cdbff 100644 --- a/src/pager.rs +++ b/src/pager.rs @@ -42,11 +42,6 @@ fn set_terminal_title(title: &str) { io::stdout().flush().unwrap(); } -fn restore_terminal_title() { - print!("\x1b]2;\x07"); - io::stdout().flush().unwrap(); -} - impl PagerKind { fn from_bin(bin: &str) -> PagerKind { use std::path::Path; -- cgit v1.2.3 From b9b554248daba2134d4355ebc5cd4373ad265abe Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sun, 10 Dec 2023 17:17:19 +0000 Subject: successfully setting the terminal title to bat's input's names --- src/bin/bat/main.rs | 10 ++++++++++ src/controller.rs | 1 + src/input.rs | 4 ++-- src/pager.rs | 9 +-------- 4 files changed, 14 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 43e9d288..6e5b71be 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -227,9 +227,19 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< Ok(()) } +fn set_terminal_title_to_inputs_names(inputs: &Vec) { + let mut input_names = "bat: ".to_string(); + for input in inputs.iter() { + input_names = input_names + &input.description.name.to_string() + ", " + } + print!("\x1b]2;{}\x07", input_names); + io::stdout().flush().unwrap(); +} + fn run_controller(inputs: Vec, config: &Config, cache_dir: &Path) -> Result { let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?; let controller = Controller::new(config, &assets); + set_terminal_title_to_inputs_names(&inputs); controller.run(inputs, None) } diff --git a/src/controller.rs b/src/controller.rs index f378cbc6..6333ebb1 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -88,6 +88,7 @@ impl<'b> Controller<'b> { clircle::Identifier::stdout() }; + let mut writer = match output_buffer { Some(buf) => OutputHandle::FmtWrite(buf), None => OutputHandle::IoWrite(output_type.handle()?), diff --git a/src/input.rs b/src/input.rs index ccab98bf..724c5e15 100644 --- a/src/input.rs +++ b/src/input.rs @@ -13,7 +13,7 @@ use crate::error::*; /// This tells bat how to refer to the input. #[derive(Clone)] pub struct InputDescription { - pub(crate) name: String, + pub name: String, /// The input title. /// This replaces the name if provided. @@ -94,7 +94,7 @@ pub(crate) struct InputMetadata { pub struct Input<'a> { pub(crate) kind: InputKind<'a>, pub(crate) metadata: InputMetadata, - pub(crate) description: InputDescription, + pub description: InputDescription, } pub(crate) enum OpenedInputKind { diff --git a/src/pager.rs b/src/pager.rs index 052cdbff..d627e903 100644 --- a/src/pager.rs +++ b/src/pager.rs @@ -1,6 +1,5 @@ use shell_words::ParseError; -use std::{env, io}; -use std::io::Write; +use std::{env}; /// If we use a pager, this enum tells us from where we were told to use it. #[derive(Debug, PartialEq)] @@ -37,11 +36,6 @@ pub(crate) enum PagerKind { Unknown, } -fn set_terminal_title(title: &str) { - print!("\x1b]2;{}\x07", title); - io::stdout().flush().unwrap(); -} - impl PagerKind { fn from_bin(bin: &str) -> PagerKind { use std::path::Path; @@ -108,7 +102,6 @@ pub(crate) fn get_pager(config_pager: Option<&str>) -> Result, Par }; let parts = shell_words::split(cmd)?; - set_terminal_title("test"); match parts.split_first() { Some((bin, args)) => { let kind = PagerKind::from_bin(bin); -- cgit v1.2.3 From 069318b139eaf88c3fdc0819e42953fb63cc7592 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sun, 10 Dec 2023 17:20:42 +0000 Subject: fixed formatting of terminal title --- src/bin/bat/main.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 6e5b71be..fb6abd11 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -229,8 +229,11 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< fn set_terminal_title_to_inputs_names(inputs: &Vec) { let mut input_names = "bat: ".to_string(); - for input in inputs.iter() { - input_names = input_names + &input.description.name.to_string() + ", " + for (index, input) in inputs.iter().enumerate() { + input_names += &input.description.name.to_string(); + if index < inputs.len() - 1 { + input_names += ", "; + } } print!("\x1b]2;{}\x07", input_names); io::stdout().flush().unwrap(); -- cgit v1.2.3 From 6ad800e43a18c5e4274ec63cc022c014506f6397 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sun, 10 Dec 2023 17:24:49 +0000 Subject: tidied commits --- src/controller.rs | 1 - src/pager.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/controller.rs b/src/controller.rs index 6333ebb1..f378cbc6 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -88,7 +88,6 @@ impl<'b> Controller<'b> { clircle::Identifier::stdout() }; - let mut writer = match output_buffer { Some(buf) => OutputHandle::FmtWrite(buf), None => OutputHandle::IoWrite(output_type.handle()?), diff --git a/src/pager.rs b/src/pager.rs index d627e903..5b707779 100644 --- a/src/pager.rs +++ b/src/pager.rs @@ -1,5 +1,5 @@ use shell_words::ParseError; -use std::{env}; +use std::env; /// If we use a pager, this enum tells us from where we were told to use it. #[derive(Debug, PartialEq)] -- cgit v1.2.3 From 12b74dfb4ec0575664482c714e55e28d3493415d Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Mon, 18 Dec 2023 16:59:12 +0000 Subject: terminal title is only set when pager is being used --- src/bin/bat/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index fb6abd11..97ea48f8 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -242,7 +242,9 @@ fn set_terminal_title_to_inputs_names(inputs: &Vec) { fn run_controller(inputs: Vec, config: &Config, cache_dir: &Path) -> Result { let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?; let controller = Controller::new(config, &assets); - set_terminal_title_to_inputs_names(&inputs); + if config.paging_mode != PagingMode::Never { + set_terminal_title_to_inputs_names(&inputs); + } controller.run(inputs, None) } -- cgit v1.2.3 From 57016f4e044db09161140b50e55a65b35fb3e363 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sun, 31 Dec 2023 22:15:00 +0000 Subject: small refactoring of set terminal title function --- src/bin/bat/main.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 97ea48f8..8998ad7b 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -227,23 +227,27 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< Ok(()) } -fn set_terminal_title_to_inputs_names(inputs: &Vec) { - let mut input_names = "bat: ".to_string(); +fn set_terminal_title_to(new_terminal_title: String) { + print!("\x1b]2;{}\x07", new_terminal_title); + io::stdout().flush().unwrap(); +} + +fn get_new_terminal_title(inputs: &Vec) -> String { + let mut new_terminal_title = "bat: ".to_string(); for (index, input) in inputs.iter().enumerate() { - input_names += &input.description.name.to_string(); + new_terminal_title += &input.description.name.to_string(); if index < inputs.len() - 1 { - input_names += ", "; + new_terminal_title += ", "; } } - print!("\x1b]2;{}\x07", input_names); - io::stdout().flush().unwrap(); + new_terminal_title } fn run_controller(inputs: Vec, config: &Config, cache_dir: &Path) -> Result { 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 { - set_terminal_title_to_inputs_names(&inputs); + set_terminal_title_to(get_new_terminal_title(&inputs)); } controller.run(inputs, None) } -- cgit v1.2.3 From 3b0ade9cb8ade5aa03ca20a250372aba37a1b24f Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sun, 31 Dec 2023 22:24:44 +0000 Subject: slightly changed set terminal command to match docs & broke print line into multiple variables --- src/bin/bat/main.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 8998ad7b..4a8ff421 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -228,7 +228,12 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result< } fn set_terminal_title_to(new_terminal_title: String) { - print!("\x1b]2;{}\x07", new_terminal_title); + 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(); } -- cgit v1.2.3 From 9239b125b148af1833d85be972fc27d14895e7a6 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sat, 27 Jan 2024 14:14:40 +0000 Subject: added a flag to config for setting terminal title --- src/bin/bat/app.rs | 1 + src/bin/bat/clap_app.rs | 6 ++++++ src/config.rs | 3 +++ 3 files changed, 10 insertions(+) (limited to 'src') diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 09430623..ef22e93d 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -287,6 +287,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..ba463996 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -168,6 +168,12 @@ pub fn build_app(interactive_output: bool) -> Command { "Include N lines of context around added/removed/modified lines when using '--diff'.", ), ) + .arg( + Arg::new("set_terminal_title") + .long("set_terminal_title") + .action(ArgAction::SetTrue) + .help("Sets terminal title when using a pager") + .long_help("Sets terminal title to filenames when using a pager."),) } app = app.arg( 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"))] -- cgit v1.2.3 From b33e33fe260f44074f4bcac21a0798530b377228 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sat, 27 Jan 2024 14:17:25 +0000 Subject: terminal title is only set if user opts in with --set_terminal_title flag --- src/bin/bat/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index 4a8ff421..a21009f0 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -251,7 +251,7 @@ fn get_new_terminal_title(inputs: &Vec) -> String { fn run_controller(inputs: Vec, config: &Config, cache_dir: &Path) -> Result { 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 { + if config.paging_mode != PagingMode::Never && config.set_terminal_title { set_terminal_title_to(get_new_terminal_title(&inputs)); } controller.run(inputs, None) -- cgit v1.2.3 From 60e32cf8237abf96b3d6dc78de35b8e904b015f1 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Sat, 27 Jan 2024 14:46:13 +0000 Subject: removed set_terminal_title arg from clap_app.rs since other boolean args aren't in clap_app.rs --- src/bin/bat/clap_app.rs | 6 ------ 1 file changed, 6 deletions(-) (limited to 'src') diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index ba463996..e8222a1d 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -168,12 +168,6 @@ pub fn build_app(interactive_output: bool) -> Command { "Include N lines of context around added/removed/modified lines when using '--diff'.", ), ) - .arg( - Arg::new("set_terminal_title") - .long("set_terminal_title") - .action(ArgAction::SetTrue) - .help("Sets terminal title when using a pager") - .long_help("Sets terminal title to filenames when using a pager."),) } app = app.arg( -- cgit v1.2.3 From 7f12989127839e07d3dcf5d9f3b154d0e2d7c661 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Mon, 29 Jan 2024 09:47:41 +0000 Subject: added set_terminal_title arg to clap_app.rs to fix ci errors --- src/bin/bat/clap_app.rs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src') diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index e8222a1d..3f83bf63 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, -- cgit v1.2.3 From 7ce010d9edf6d82f7013af2c711308f38fc680d6 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Thu, 8 Feb 2024 21:33:03 +0000 Subject: Using hypens instead of underscores for set-terminal-title command --- src/bin/bat/app.rs | 2 +- src/bin/bat/clap_app.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index ef22e93d..8ec3caa5 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -287,7 +287,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"), + 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 3f83bf63..6ceed784 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -569,8 +569,8 @@ pub fn build_app(interactive_output: bool) -> Command { .help("Show acknowledgements."), ) .arg( - Arg::new("set_terminal_title") - .long("set_terminal_title") + 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."), -- cgit v1.2.3 From 02077db53e36b157e1a12496b8315c77514de0a0 Mon Sep 17 00:00:00 2001 From: Oliver looney Date: Thu, 8 Feb 2024 21:41:20 +0000 Subject: undid unnecessary api visibility changes --- src/bin/bat/main.rs | 2 +- src/input.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/bin/bat/main.rs b/src/bin/bat/main.rs index a21009f0..afc0d59b 100644 --- a/src/bin/bat/main.rs +++ b/src/bin/bat/main.rs @@ -240,7 +240,7 @@ fn set_terminal_title_to(new_terminal_title: String) { fn get_new_terminal_title(inputs: &Vec) -> String { let mut new_terminal_title = "bat: ".to_string(); for (index, input) in inputs.iter().enumerate() { - new_terminal_title += &input.description.name.to_string(); + new_terminal_title += input.description().title(); if index < inputs.len() - 1 { new_terminal_title += ", "; } diff --git a/src/input.rs b/src/input.rs index 724c5e15..ccab98bf 100644 --- a/src/input.rs +++ b/src/input.rs @@ -13,7 +13,7 @@ use crate::error::*; /// This tells bat how to refer to the input. #[derive(Clone)] pub struct InputDescription { - pub name: String, + pub(crate) name: String, /// The input title. /// This replaces the name if provided. @@ -94,7 +94,7 @@ pub(crate) struct InputMetadata { pub struct Input<'a> { pub(crate) kind: InputKind<'a>, pub(crate) metadata: InputMetadata, - pub description: InputDescription, + pub(crate) description: InputDescription, } pub(crate) enum OpenedInputKind { -- cgit v1.2.3 From 1a54c9bf6ddb1daf649e193c1262c28d86ddcc43 Mon Sep 17 00:00:00 2001 From: David Tolnay Date: Thu, 28 Dec 2023 14:38:35 -0800 Subject: Eliminate dependency on serde's "derive" feature --- src/assets/assets_metadata.rs | 2 +- src/assets/lazy_theme_set.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src') 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; -- cgit v1.2.3 From 414403b062ac644ee2b569870f66de49999f9dd3 Mon Sep 17 00:00:00 2001 From: Ethan P Date: Sun, 16 Apr 2023 19:59:52 -0700 Subject: Add EscapeSequenceOffsetsIterator This can be used to extract a subset of ANSI escape sequences from a string of text. I have big plans for this eventually, but for now, it'll be used to strip OSC before printing. --- src/vscreen.rs | 476 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 475 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/vscreen.rs b/src/vscreen.rs index ea5d4da6..ecf2bd3c 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 { @@ -210,3 +214,473 @@ fn join( .collect::>() .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>, +} + +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 { + match self.chars_take_while(|c| c != '\x1B') { + None => None, + Some((start, end)) => Some(EscapeSequenceOffsets::Text { start, end }), + } + } + + fn next_sequence(&mut self) -> Option { + 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 { + 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 { + 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 { + 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 { + match self.chars.peek() { + Some((_, '\x1B')) => self.next_sequence(), + Some((_, _)) => self.next_text(), + None => None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::vscreen::{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); + } +} -- cgit v1.2.3 From 054421268fff33003b892eb7bcef1a831c6c308d Mon Sep 17 00:00:00 2001 From: Ethan P Date: Sun, 16 Apr 2023 20:18:40 -0700 Subject: Strip OSC sequences before printing This commit strips OSC (Operating System Command) sequences before printing lines. Eventually when time permits, I want to add back support for printing OSC sequences (and improve it to treat hyperlinks like an attribute). Until then, this should help prevent garbled output :) --- src/printer.rs | 8 +++++--- src/vscreen.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/printer.rs b/src/printer.rs index 257cc766..45fd5336 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -33,7 +33,7 @@ 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::{strip_problematic_sequences, AnsiStyle}; use crate::wrapping::WrappingMode; pub enum OutputHandle<'a> { @@ -581,7 +581,8 @@ 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 text = strip_problematic_sequences(region); + let ansi_iterator = AnsiCodeIterator::new(&text); for chunk in ansi_iterator { match chunk { // ANSI escape passthrough. @@ -634,7 +635,8 @@ impl<'a> Printer for InteractivePrinter<'a> { } } else { for &(style, region) in ®ions { - let ansi_iterator = AnsiCodeIterator::new(region); + let text = strip_problematic_sequences(region); + let ansi_iterator = AnsiCodeIterator::new(&text); for chunk in ansi_iterator { match chunk { // ANSI escape passthrough. diff --git a/src/vscreen.rs b/src/vscreen.rs index ecf2bd3c..ccd0bfe8 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -458,9 +458,54 @@ impl<'a> Iterator for EscapeSequenceOffsetsIterator<'a> { } } +/// Strips problematic ANSI escape sequences from a string. +/// +/// Ideally, this will be replaced with something that uses [[Attributes]] to create a table of char offsets +/// -> absolute styles and style deltas. Something like that would let us simplify the printer (and support +/// re-printing OSC hyperlink commands). +pub fn strip_problematic_sequences(text: &str) -> String { + use EscapeSequenceOffsets::*; + + let mut buffer = String::with_capacity(text.len()); + for seq in EscapeSequenceOffsetsIterator::new(text) { + match seq { + Text { start, end } => buffer.push_str(&text[start..end]), + Unknown { start, end } => buffer.push_str(&text[start..end]), + + NF { + start_sequence: start, + start: _, + end, + } => buffer.push_str(&text[start..end]), + + CSI { + start_sequence: start, + start_parameters: _, + start_intermediates: _, + start_final_byte: _, + end, + } => buffer.push_str(&text[start..end]), + + OSC { + start_sequence: _, + start_command: _, + start_terminator: _, + end: _, + } => { + // TODO(eth-p): Support re-printing hyperlinks. + // In the meantime, strip these. + } + } + } + + buffer +} + #[cfg(test)] mod tests { - use crate::vscreen::{EscapeSequenceOffsets, EscapeSequenceOffsetsIterator}; + use crate::vscreen::{ + strip_problematic_sequences, EscapeSequenceOffsets, EscapeSequenceOffsetsIterator, + }; #[test] fn test_escape_sequence_offsets_iterator_parses_text() { @@ -683,4 +728,12 @@ mod tests { ); assert_eq!(iter.next(), None); } + + #[test] + fn test_strip_problematic_sequences() { + assert_eq!( + strip_problematic_sequences("text\x1B[33m\x1B]OSC\x1B\\\x1B(0"), + "text\x1B[33m\x1B(0" + ); + } } -- cgit v1.2.3 From 6b9b085be3893682e75e3538dbee0d7bd6772b86 Mon Sep 17 00:00:00 2001 From: Ethan P Date: Mon, 17 Apr 2023 16:35:32 -0700 Subject: Add EscapeSequenceIterator This is an iterator for escape sequences, using EscapeSequenceOffsetsIterator for the underlying parsing of individual escape sequences. --- src/vscreen.rs | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 138 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/vscreen.rs b/src/vscreen.rs index ccd0bfe8..ce7188d7 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -458,6 +458,68 @@ impl<'a> Iterator for EscapeSequenceOffsetsIterator<'a> { } } +/// 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 { + 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], + }, + }) + } +} + /// Strips problematic ANSI escape sequences from a string. /// /// Ideally, this will be replaced with something that uses [[Attributes]] to create a table of char offsets @@ -501,10 +563,46 @@ pub fn strip_problematic_sequences(text: &str) -> String { buffer } +/// 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::{ - strip_problematic_sequences, EscapeSequenceOffsets, EscapeSequenceOffsetsIterator, + strip_problematic_sequences, EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets, + EscapeSequenceOffsetsIterator, }; #[test] @@ -736,4 +834,43 @@ mod tests { "text\x1B[33m\x1B(0" ); } + + #[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); + } } -- cgit v1.2.3 From 165c495e7582e1251802cfc3c05b2fc28151cee8 Mon Sep 17 00:00:00 2001 From: Ethan P Date: Mon, 17 Apr 2023 17:02:42 -0700 Subject: Replace AnsiCodeIterator in printer.rs This uses the new EscapeSequenceIterator, saving us a preprocessing step for each line. --- src/printer.rs | 58 ++++++++++++++++++------------- src/vscreen.rs | 105 +++++++++++++++------------------------------------------ 2 files changed, 62 insertions(+), 101 deletions(-) (limited to 'src') diff --git a/src/printer.rs b/src/printer.rs index 45fd5336..6d495777 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::{strip_problematic_sequences, 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,18 +593,11 @@ impl<'a> Printer for InteractivePrinter<'a> { let italics = self.config.use_italic_text; for &(style, region) in ®ions { - let text = strip_problematic_sequences(region); - let ansi_iterator = AnsiCodeIterator::new(&text); + 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, &mut cursor_total); let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n'); @@ -626,6 +631,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); + } } } } @@ -635,18 +646,11 @@ impl<'a> Printer for InteractivePrinter<'a> { } } else { for &(style, region) in ®ions { - let text = strip_problematic_sequences(region); - let ansi_iterator = AnsiCodeIterator::new(&text); + 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, @@ -726,6 +730,12 @@ impl<'a> Printer for InteractivePrinter<'a> { ) )?; } + + // ANSI escape passthrough. + _ => { + write!(handle, "{}", chunk.raw())?; + self.ansi_style.update(chunk); + } } } } @@ -746,8 +756,8 @@ impl<'a> Printer for InteractivePrinter<'a> { } if highlight_this_line && self.config.theme == "ansi" { - self.ansi_style.update("^[24m"); - write!(handle, "\x1B[24m")?; + write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?; + self.ansi_style.update(ANSI_UNDERLINE_DISABLE); } Ok(()) diff --git a/src/vscreen.rs b/src/vscreen.rs index ce7188d7..ea1f02b4 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -14,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 => { @@ -85,26 +85,36 @@ impl Attributes { /// 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 { .. } => return false, + 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) { @@ -153,14 +163,6 @@ 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 @@ -520,49 +522,6 @@ impl<'a> Iterator for EscapeSequenceIterator<'a> { } } -/// Strips problematic ANSI escape sequences from a string. -/// -/// Ideally, this will be replaced with something that uses [[Attributes]] to create a table of char offsets -/// -> absolute styles and style deltas. Something like that would let us simplify the printer (and support -/// re-printing OSC hyperlink commands). -pub fn strip_problematic_sequences(text: &str) -> String { - use EscapeSequenceOffsets::*; - - let mut buffer = String::with_capacity(text.len()); - for seq in EscapeSequenceOffsetsIterator::new(text) { - match seq { - Text { start, end } => buffer.push_str(&text[start..end]), - Unknown { start, end } => buffer.push_str(&text[start..end]), - - NF { - start_sequence: start, - start: _, - end, - } => buffer.push_str(&text[start..end]), - - CSI { - start_sequence: start, - start_parameters: _, - start_intermediates: _, - start_final_byte: _, - end, - } => buffer.push_str(&text[start..end]), - - OSC { - start_sequence: _, - start_command: _, - start_terminator: _, - end: _, - } => { - // TODO(eth-p): Support re-printing hyperlinks. - // In the meantime, strip these. - } - } - } - - buffer -} - /// A parsed ANSI/VT100 escape sequence. #[derive(Debug, PartialEq)] pub enum EscapeSequence<'a> { @@ -601,7 +560,7 @@ impl<'a> EscapeSequence<'a> { #[cfg(test)] mod tests { use crate::vscreen::{ - strip_problematic_sequences, EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets, + EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets, EscapeSequenceOffsetsIterator, }; @@ -827,14 +786,6 @@ mod tests { assert_eq!(iter.next(), None); } - #[test] - fn test_strip_problematic_sequences() { - assert_eq!( - strip_problematic_sequences("text\x1B[33m\x1B]OSC\x1B\\\x1B(0"), - "text\x1B[33m\x1B(0" - ); - } - #[test] fn test_escape_sequence_iterator_iterates() { let mut iter = EscapeSequenceIterator::new("text\x1B[33m\x1B]OSC\x07\x1B]OSC\x1B\\\x1B(0"); -- cgit v1.2.3 From 6549e26f5d3923078b482d30bc4ed90b2f5710c9 Mon Sep 17 00:00:00 2001 From: Ethan P Date: Mon, 17 Apr 2023 19:07:58 -0700 Subject: Re-emit hyperlinks when wrapping lines --- src/vscreen.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/vscreen.rs b/src/vscreen.rs index ea1f02b4..7e2b8cd1 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -65,6 +65,13 @@ struct Attributes { /// ON: ^[9m /// OFF: ^[29m strike: String, + + /// The hyperlink sequence. + /// FORMAT: \x1B]8;;\e\\ + /// + /// `\e\\` may be replaced with BEL `\x07`. + /// Setting both and to an empty string represents no hyperlink. + hyperlink: String, } impl Attributes { @@ -80,6 +87,7 @@ impl Attributes { underline: "".to_owned(), italic: "".to_owned(), strike: "".to_owned(), + hyperlink: "".to_owned(), } } @@ -90,7 +98,16 @@ impl Attributes { match sequence { Text(_) => return false, Unknown(_) => { /* defer to update_with_unsupported */ } - OSC { .. } => return false, + 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, @@ -168,6 +185,18 @@ impl Attributes { 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) -> bool { self.charset = format!("\x1B{}{}", kind, set.take(1).collect::()); true @@ -191,7 +220,7 @@ impl Display for Attributes { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{}{}{}{}{}{}{}{}{}", + "{}{}{}{}{}{}{}{}{}{}", self.foreground, self.background, self.underlined, @@ -201,6 +230,7 @@ impl Display for Attributes { self.underline, self.italic, self.strike, + self.hyperlink, ) } } -- cgit v1.2.3 From 1023399c5e20edac8710e9f19dc72b6a95fd257d Mon Sep 17 00:00:00 2001 From: Ethan P Date: Mon, 17 Apr 2023 19:19:49 -0700 Subject: Remove hyperlink when wrapping lines --- src/printer.rs | 11 +++++++---- src/vscreen.rs | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/printer.rs b/src/printer.rs index 6d495777..f413fdc3 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -598,12 +598,12 @@ impl<'a> Printer for InteractivePrinter<'a> { match chunk { // Regular text. 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'); write!( handle, - "{}", + "{}{}", as_terminal_escaped( style, &format!("{}{}", self.ansi_style, text_trimmed), @@ -611,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 { @@ -693,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), @@ -702,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() )?; diff --git a/src/vscreen.rs b/src/vscreen.rs index 7e2b8cd1..c902d42b 100644 --- a/src/vscreen.rs +++ b/src/vscreen.rs @@ -23,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 { @@ -35,6 +42,8 @@ impl Display for AnsiStyle { } struct Attributes { + has_sgr_sequences: bool, + foreground: String, background: String, underlined: String, @@ -67,16 +76,18 @@ struct Attributes { strike: String, /// The hyperlink sequence. - /// FORMAT: \x1B]8;;\e\\ + /// FORMAT: \x1B]8;{ID};{URL}\e\\ /// /// `\e\\` may be replaced with BEL `\x07`. - /// Setting both and to an empty string represents no hyperlink. + /// 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(), @@ -135,6 +146,8 @@ impl Attributes { } fn sgr_reset(&mut self) { + self.has_sgr_sequences = false; + self.foreground.clear(); self.background.clear(); self.underlined.clear(); @@ -152,6 +165,7 @@ impl Attributes { .map(|p| p.parse::()) .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(), @@ -214,6 +228,28 @@ impl Attributes { _ => format!("\x1B[{}m", color), }