diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06db2cbb..a17726fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,10 +2,15 @@
## Features
+- Set terminal title to file names when Paging is not Paging::Never #2807 (@Oliver-Looney)
+
## Bugfixes
- 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)
+- Fix handling of inputs with combined ANSI color and attribute sequences, see #2185 and #2856 (@eth-p)
+- Fix panel width when line 10000 wraps, see #2854 (@eth-p)
## Other
@@ -20,12 +25,16 @@
- Use proper Architecture for Debian packages built for musl, see #2811 (@Enselic)
- Pull in fix for unsafe-libyaml security advisory, see #2812 (@dtolnay)
- Update git-version dependency to use Syn v2, see #2816 (@dtolnay)
+- Update git2 dependency to v0.18.2, see #2852 (@eth-p)
+- Apply clippy fixes #2864 (@cyqsimon)
## Syntaxes
- `cmd-help`: scope subcommands followed by other terms, and other misc improvements, see #2819 (@victor-gp)
- Upgrade JQ syntax, see #2820 (@dependabot[bot])
- Associate `xsh` files with `xonsh` syntax that is Python, see #2840 (@anki-code).
+- Added auto detect syntax for `.jsonc` #2795 (@mxaddict)
+- Added auto detect syntax for `.aws/{config,credentials}` #2795 (@mxaddict)
## Themes
@@ -36,6 +45,7 @@
- [BREAKING] `SyntaxMapping::{empty,builtin}` are removed; use `SyntaxMapping::new` instead
- [BREAKING] `SyntaxMapping::mappings` is replaced by `SyntaxMapping::{builtin,custom,all}_mappings`
- Make `Controller::run_with_error_handler`'s error handler `FnMut`, see #2831 (@rhysd)
+- Improve compile time by 20%, see #2815 (@dtolnay)
# v0.24.0
diff --git a/Cargo.lock b/Cargo.lock
index 4aaf7b32..ff674b9d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -129,7 +129,7 @@ dependencies = [
"globset",
"grep-cli",
"home",
- "indexmap 2.1.0",
+ "indexmap 2.2.2",
"itertools",
"nix",
"nu-ansi-term",
@@ -142,6 +142,7 @@ dependencies = [
"run_script",
"semver",
"serde",
+ "serde_derive",
"serde_with",
"serde_yaml",
"serial_test",
@@ -272,13 +273,14 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
name = "clircle"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e87cbed5354f17bd8ca8821a097fb62599787fe8f611743fad7ee156a0a600"
+checksum = "ec0b92245ea62a7a751db4b0e4a583f8978e508077ef6de24fcc0d0dc5311a8d"
dependencies = [
"cfg-if",
"libc",
"serde",
+ "serde_derive",
"winapi",
]
@@ -580,7 +582,7 @@ dependencies = [
"bstr",
"log",
"regex-automata",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
[[package]]
@@ -646,9 +648,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.1.0"
+version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520"
dependencies = [
"equivalent",
"hashbrown 0.14.1",
@@ -693,9 +695,9 @@ checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "libgit2-sys"
-version = "0.16.1+1.7.1"
+version = "0.16.2+1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2a2bb3680b094add03bb3732ec520ece34da31a8cd2d633d1389d0f0fb60d0c"
+checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
dependencies = [
"cc",
"libc",
@@ -1027,7 +1029,7 @@ dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
[[package]]
@@ -1038,15 +1040,9 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
-[[package]]
-name = "regex-syntax"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
-
[[package]]
name = "regex-syntax"
version = "0.8.2"
@@ -1113,9 +1109,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "semver"
-version = "1.0.20"
+version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
+checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
[[package]]
name = "serde"
@@ -1150,28 +1146,29 @@ dependencies = [
[[package]]
name = "serde_spanned"
-version = "0.6.4"
+version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
+checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
[[package]]
name = "serde_with"
-version = "3.4.0"
+version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
+checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
dependencies = [
"serde",
+ "serde_derive",
"serde_with_macros",
]
[[package]]
name = "serde_with_macros"
-version = "3.4.0"
+version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788"
+checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
dependencies = [
"darling",
"proc-macro2",
@@ -1185,7 +1182,7 @@ version = "0.9.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15e0ef66bf939a7c890a0bf6d5a733c70202225f9888a89ed5c62298b019129"
dependencies = [
- "indexmap 2.1.0",
+ "indexmap 2.2.2",
"itoa",
"ryu",
"serde",
@@ -1258,9 +1255,9 @@ dependencies = [
[[package]]
name = "syntect"
-version = "5.1.0"
+version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e02b4b303bf8d08bfeb0445cba5068a3d306b6baece1d5582171a9bf49188f91"
+checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1"
dependencies = [
"bincode",
"bitflags 1.3.2",
@@ -1270,8 +1267,9 @@ dependencies = [
"once_cell",
"onig",
"plist",
- "regex-syntax 0.7.5",
+ "regex-syntax",
"serde",
+ "serde_derive",
"serde_json",
"thiserror",
"walkdir",
@@ -1374,11 +1372,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "toml"
-version = "0.8.6"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ff9e3abce27ee2c9a37f9ad37238c1bdd4e789c84ba37df76aa4d528f5072cc"
+checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325"
dependencies = [
- "indexmap 2.1.0",
+ "indexmap 2.2.2",
"serde",
"serde_spanned",
"toml_datetime",
@@ -1396,11 +1394,11 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.20.7"
+version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
+checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
- "indexmap 2.1.0",
+ "indexmap 2.2.2",
"serde",
"serde_spanned",
"toml_datetime",
diff --git a/Cargo.toml b/Cargo.toml
index 3b7f10e6..05a2acb7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,11 +53,12 @@ content_inspector = "0.2.4"
shell-words = { version = "1.1.0", optional = true }
unicode-width = "0.1.11"
globset = "0.4"
-serde = { version = "1.0", features = ["derive"] }
+serde = "1.0"
+serde_derive = "1.0"
serde_yaml = "0.9.28"
semver = "1.0"
path_abs = { version = "0.5", default-features = false }
-clircle = "0.4"
+clircle = "0.5"
bugreport = { version = "0.5.0", optional = true }
etcetera = { version = "0.8.0", optional = true }
grep-cli = { version = "0.1.10", optional = true }
@@ -74,7 +75,7 @@ optional = true
default-features = false
[dependencies.syntect]
-version = "5.1.0"
+version = "5.2.0"
default-features = false
features = ["parsing"]
@@ -94,19 +95,21 @@ serial_test = { version = "2.0.0", default-features = false }
predicates = "3.0.4"
wait-timeout = "0.2.0"
tempfile = "3.8.1"
+serde = { version = "1.0", features = ["derive"] }
[target.'cfg(unix)'.dev-dependencies]
nix = { version = "0.26.4", default-features = false, features = ["term"] }
[build-dependencies]
anyhow = "1.0.78"
-indexmap = { version = "2.1.0", features = ["serde"] }
+indexmap = { version = "2.2.2", features = ["serde"] }
itertools = "0.11.0"
once_cell = "1.18"
regex = "1.10.2"
-serde = { version = "1.0", features = ["derive"] }
-serde_with = { version = "3.4.0", default-features = false, features = ["macros"] }
-toml = { version = "0.8.6", features = ["preserve_order"] }
+serde = "1.0"
+serde_derive = "1.0"
+serde_with = { version = "3.6.1", default-features = false, features = ["macros"] }
+toml = { version = "0.8.9", features = ["preserve_order"] }
walkdir = "2.4"
[build-dependencies.clap]
diff --git a/README.md b/README.md
index 352ae64d..57baf2b0 100644
--- a/README.md
+++ b/README.md
@@ -602,7 +602,8 @@ set, `less` is used by default. If you want to use a different pager, you can ei
`PAGER` variable or set the `BAT_PAGER` environment variable to override what is specified in
`PAGER`.
-**Note**: If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
+>[!NOTE]
+> If `PAGER` is `more` or `most`, `bat` will silently use `less` instead to ensure support for colors.
If you want to pass command-line arguments to the pager, you can also set them via the
`PAGER`/`BAT_PAGER` variables:
@@ -613,20 +614,37 @@ export BAT_PAGER="less -RF"
Instead of using environment variables, you can also use `bat`s [configuration file](https://github.com/sharkdp/bat#configuration-file) to configure the pager (`--pager` option).
-**Note**: By default, if the pager is set to `less` (and no command-line options are specified),
-`bat` will pass the following command line options to the pager: `-R`/`--RAW-CONTROL-CHARS`,
-`-F`/`--quit-if-one-screen` and `-X`/`--no-init`. The last option (`-X`) is only used for `less`
-versions older than 530.
-The `-R` option is needed to interpret ANSI colors correctly. The second option (`-F`) instructs
-less to exit immediately if the output size is smaller than the vertical size of the terminal.
-This is convenient for small files because you do not have to press `q` to quit the pager. The
-third option (`-X`) is needed to fix a bug with the `--quit-if-one-screen` feature in old versions
-of `less`. Unfortunately, it also breaks mouse-wheel support in `less`.
+### Using `less` as a pager
-If you want to enable mouse-wheel scrolling on older versions of `less`, you can pass just `-R` (as
-in the example above, this will disable the quit-if-one-screen feature). For less 530 or newer,
-it should work out of the box.
+When using `less` as a pager, `bat` will automatically pass extra options along to `less`
+to improve the experience. Specifically, `-R`/`--RAW-CONTROL-CHARS`, `-F`/`--quit-if-one-screen`,
+and under certain conditions, `-X`/`--no-init` and/or `-S`/`--chop-long-lines`.
+
+>[!IMPORTANT]
+> These options will not be added if:
+> - The pager is not named `less`.
+> - The `--pager` argument contains any command-line arguments (e.g. `--pager="less -R"`).
+> - The `BAT_PAGER` environment variable contains any command-line arguments (e.g. `export BAT_PAGER="less -R"`)
+>
+> The `--quit-if-one-screen` option will not be added when:
+> - The `--paging=always` argument is used.
+> - The `BAT_PAGING` environment is set to `always`.
+
+The `-R` option is needed to interpret ANSI colors correctly.
+
+The `-F` option instructs `less` to exit immediately if the output size is smaller than
+the vertical size of the terminal. This is convenient for small files because you do not
+have to press `q` to quit the pager.
+
+The `-X` option is needed to fix a bug with the `--quit-if-one-screen` feature in versions
+of `less` older than version 530. Unfortunately, it also breaks mouse-wheel support in `less`.
+If you want to enable mouse-wheel scrolling on older versions of `less` and do not mind losing
+the quit-if-one-screen feature, you can set the pager (via `--pager` or `BAT_PAGER`) to `less -R`.
+For `less` 530 or newer, it should work out of the box.
+
+The `-S` option is added when `bat`'s `-S`/`--chop-long-lines` option is used. This tells `less`
+to truncate any lines larger than the terminal width.
### Indentation
diff --git a/assets/syntaxes/02_Extra/cmd-help b/assets/syntaxes/02_Extra/cmd-help
index b150d845..209559b7 160000
--- a/assets/syntaxes/02_Extra/cmd-help
+++ b/assets/syntaxes/02_Extra/cmd-help
@@ -1 +1 @@
-Subproject commit b150d84534dd060afdcaf3f58977faeaf5917e56
+Subproject commit 209559b72f7e8848c988828088231b3a4d8b6838
diff --git a/assets/themes/zenburn b/assets/themes/zenburn
index e627f1cb..86d4ee7a 160000
--- a/assets/themes/zenburn
+++ b/assets/themes/zenburn
@@ -1 +1 @@
-Subproject commit e627f1cb223c1171ab0a6a48d166c87aeae2a1d5
+Subproject commit 86d4ee7a1f884851a1d21d66249687f527fced32
diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs
index c29b9225..959caea8 100644
--- a/build/syntax_mapping.rs
+++ b/build/syntax_mapping.rs
@@ -10,7 +10,7 @@ use indexmap::IndexMap;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
-use serde::Deserialize;
+use serde_derive::Deserialize;
use serde_with::DeserializeFromStr;
use walkdir::WalkDir;
diff --git a/doc/long-help.txt b/doc/long-help.txt
index 247120fb..3ac4a40f 100644
--- a/doc/long-help.txt
+++ b/doc/long-help.txt
@@ -160,6 +160,9 @@ Options:
--acknowledgements
Show acknowledgements.
+ --set-terminal-title
+ Sets terminal title to filenames when using a pager.
+
-h, --help
Print help (see a summary with '-h')
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) -> 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, 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 && 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 ®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(())
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
similarity index 65%
rename from src/syntax_mapping/builtins/common/50-jsonl.toml
rename to 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::())
.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- ) -> bool {
self.charset = format!("\x1B{}{}", kind, set.take(1).collect::());
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::>()
.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>,
+}
+
+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 {
+ self.chars_take_while(|c| c != '\x1B')
+ .map(|(start, end)| 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,
+ 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.
+ 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 {
+ 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 {
+ 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 })
+ );
+ }
+
+ #[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);
+ }
+
+ #[test]
+ fn test_sgr_attributes_do_not_leak_into_wrong_field() {
+ let mut attrs = crate::vscreen::Attributes::new();
+
+ // Bold, Dim, Italic, Underline, Foreground, Background
+ attrs.update(EscapeSequence::CSI {
+ raw_sequence: "\x1B[1;2;3;4;31;41m",
+ parameters: "1;2;3;4;31;41",
+ intermediates: "",
+ final_byte: "m",
+ });
+
+ assert_eq!(attrs.bold, "\x1B[1m");
+ assert_eq!(attrs.dim, "\x1B[2m");
+ assert_eq!(attrs.italic, "\x1B[3m");
+ assert_eq!(attrs.underline, "\x1B[4m");
+ assert_eq!(attrs.foreground, "\x1B[31m");
+ assert_eq!(attrs.background, "\x1B[41m");
+
+ // Bold, Bright Foreground, Bright Background
+ attrs.sgr_reset();
+ attrs.update(EscapeSequence::CSI {
+ raw_sequence: "\x1B[1;94;103m",
+ parameters: "1;94;103",
+ intermediates: "",
+ final_byte: "m",
+ });
+
+ assert_eq!(attrs.bold, "\x1B[1m");
+ assert_eq!(attrs.foreground, "\x1B[94m");
+ assert_eq!(attrs.background, "\x1B[103m");
+ }
+}
diff --git a/tests/examples/regression_tests/issue_2541.txt b/tests/examples/regression_tests/issue_2541.txt
new file mode 100644
index 00000000..1059b94e
--- /dev/null
+++ b/tests/examples/regression_tests/issue_2541.txt
@@ -0,0 +1 @@
+]8;;http://example.com\This is a link]8;;\n
\ No newline at end of file
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 437ae8e7..3612654b 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -936,6 +936,18 @@ fn env_var_bat_paging() {
});
}
+#[test]
+fn basic_set_terminal_title() {
+ bat()
+ .arg("--paging=always")
+ .arg("--set-terminal-title")
+ .arg("test.txt")
+ .assert()
+ .success()
+ .stdout("\u{1b}]0;bat: test.txt\x07hello world\n")
+ .stderr("");
+}
+
#[test]
fn diagnostic_sanity_check() {
bat()
@@ -1163,6 +1175,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 +1945,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()
diff --git a/tests/tester/mod.rs b/tests/tester/mod.rs
index 8ddea11f..c4e916a6 100644
--- a/tests/tester/mod.rs
+++ b/tests/tester/mod.rs
@@ -22,7 +22,7 @@ impl BatTester {
pub fn test_snapshot(&self, name: &str, style: &str) {
let output = Command::new(&self.exe)
.current_dir(self.temp_dir.path())
- .args(&[
+ .args([
"sample.rs",
"--no-config",
"--paging=never",