Merge branch 'master' into master

This commit is contained in:
David Peter 2024-02-23 21:48:44 +01:00 committed by GitHub
commit b718889ba2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1002 additions and 121 deletions

View File

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

66
Cargo.lock generated
View File

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

View File

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

View File

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

@ -1 +1 @@
Subproject commit b150d84534dd060afdcaf3f58977faeaf5917e56
Subproject commit 209559b72f7e8848c988828088231b3a4d8b6838

@ -1 +1 @@
Subproject commit e627f1cb223c1171ab0a6a48d166c87aeae2a1d5
Subproject commit 86d4ee7a1f884851a1d21d66249687f527fced32

View File

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

View File

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

View File

@ -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::*;

View File

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

View File

@ -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"),
})
}

View File

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

View File

@ -229,9 +229,33 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<
Ok(())
}
fn set_terminal_title_to(new_terminal_title: String) {
let osc_command_for_setting_terminal_title = "\x1b]0;";
let osc_end_command = "\x07";
print!(
"{}{}{}",
osc_command_for_setting_terminal_title, new_terminal_title, osc_end_command
);
io::stdout().flush().unwrap();
}
fn get_new_terminal_title(inputs: &Vec<Input>) -> String {
let mut new_terminal_title = "bat: ".to_string();
for (index, input) in inputs.iter().enumerate() {
new_terminal_title += input.description().title();
if index < inputs.len() - 1 {
new_terminal_title += ", ";
}
}
new_terminal_title
}
fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Result<bool> {
let assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
let controller = Controller::new(config, &assets);
if config.paging_mode != PagingMode::Never && config.set_terminal_title {
set_terminal_title_to(get_new_terminal_title(&inputs));
}
controller.run(inputs, None)
}

View File

@ -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"))]

View File

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

View File

@ -7,8 +7,6 @@ use nu_ansi_term::Style;
use bytesize::ByteSize;
use console::AnsiCodeIterator;
use syntect::easy::HighlightLines;
use syntect::highlighting::Color;
use syntect::highlighting::Theme;
@ -33,9 +31,23 @@ use crate::line_range::RangeCheckResult;
use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::vscreen::AnsiStyle;
use crate::vscreen::{AnsiStyle, EscapeSequence, EscapeSequenceIterator};
use crate::wrapping::WrappingMode;
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
raw_sequence: "\x1B[4m",
parameters: "4",
intermediates: "",
final_byte: "m",
};
const ANSI_UNDERLINE_DISABLE: EscapeSequence = EscapeSequence::CSI {
raw_sequence: "\x1B[24m",
parameters: "24",
intermediates: "",
final_byte: "m",
};
pub enum OutputHandle<'a> {
IoWrite(&'a mut dyn io::Write),
FmtWrite(&'a mut dyn fmt::Write),
@ -554,7 +566,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
if highlight_this_line && self.config.theme == "ansi" {
self.ansi_style.update("^[4m");
self.ansi_style.update(ANSI_UNDERLINE_ENABLE);
}
let background_color = self
@ -581,23 +593,17 @@ impl<'a> Printer for InteractivePrinter<'a> {
let italics = self.config.use_italic_text;
for &(style, region) in &regions {
let ansi_iterator = AnsiCodeIterator::new(region);
let ansi_iterator = EscapeSequenceIterator::new(region);
for chunk in ansi_iterator {
match chunk {
// ANSI escape passthrough.
(ansi, true) => {
self.ansi_style.update(ansi);
write!(handle, "{}", ansi)?;
}
// Regular text.
(text, false) => {
let text = &*self.preprocess(text, &mut cursor_total);
EscapeSequence::Text(text) => {
let text = self.preprocess(text, &mut cursor_total);
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
write!(
handle,
"{}",
"{}{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, text_trimmed),
@ -605,9 +611,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
colored_output,
italics,
background_color
)
),
self.ansi_style.to_reset_sequence(),
)?;
// Pad the rest of the line.
if text.len() != text_trimmed.len() {
if let Some(background_color) = background_color {
let ansi_style = Style {
@ -625,6 +633,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}
// ANSI escape passthrough.
_ => {
write!(handle, "{}", chunk.raw())?;
self.ansi_style.update(chunk);
}
}
}
}
@ -634,17 +648,11 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
} else {
for &(style, region) in &regions {
let ansi_iterator = AnsiCodeIterator::new(region);
let ansi_iterator = EscapeSequenceIterator::new(region);
for chunk in ansi_iterator {
match chunk {
// ANSI escape passthrough.
(ansi, true) => {
self.ansi_style.update(ansi);
write!(handle, "{}", ansi)?;
}
// Regular text.
(text, false) => {
EscapeSequence::Text(text) => {
let text = self.preprocess(
text.trim_end_matches(|c| c == '\r' || c == '\n'),
&mut cursor_total,
@ -687,7 +695,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
// It wraps.
write!(
handle,
"{}\n{}",
"{}{}\n{}",
as_terminal_escaped(
style,
&format!("{}{}", self.ansi_style, line_buf),
@ -696,6 +704,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.use_italic_text,
background_color
),
self.ansi_style.to_reset_sequence(),
panel_wrap.clone().unwrap()
)?;
@ -724,6 +733,12 @@ impl<'a> Printer for InteractivePrinter<'a> {
)
)?;
}
// ANSI escape passthrough.
_ => {
write!(handle, "{}", chunk.raw())?;
self.ansi_style.update(chunk);
}
}
}
}
@ -744,8 +759,8 @@ impl<'a> Printer for InteractivePrinter<'a> {
}
if highlight_this_line && self.config.theme == "ansi" {
self.ansi_style.update("^[24m");
write!(handle, "\x1B[24m")?;
write!(handle, "{}", ANSI_UNDERLINE_DISABLE.raw())?;
self.ansi_style.update(ANSI_UNDERLINE_DISABLE);
}
Ok(())

View File

@ -0,0 +1,2 @@
[mappings]
"INI" = ["**/.aws/credentials", "**/.aws/config"]

View File

@ -1,3 +1,3 @@
# JSON Lines is a simple variation of JSON #2535
[mappings]
"JSON" = ["*.jsonl"]
"JSON" = ["*.jsonl", "*.jsonc"]

View File

@ -1,4 +1,8 @@
use std::fmt::{Display, Formatter};
use std::{
fmt::{Display, Formatter},
iter::Peekable,
str::CharIndices,
};
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
pub struct AnsiStyle {
@ -10,7 +14,7 @@ impl AnsiStyle {
AnsiStyle { attributes: None }
}
pub fn update(&mut self, sequence: &str) -> bool {
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
match &mut self.attributes {
Some(a) => a.update(sequence),
None => {
@ -19,6 +23,13 @@ impl AnsiStyle {
}
}
}
pub fn to_reset_sequence(&self) -> String {
match self.attributes {
Some(ref a) => a.to_reset_sequence(),
None => String::new(),
}
}
}
impl Display for AnsiStyle {
@ -31,6 +42,8 @@ impl Display for AnsiStyle {
}
struct Attributes {
has_sgr_sequences: bool,
foreground: String,
background: String,
underlined: String,
@ -61,11 +74,20 @@ struct Attributes {
/// ON: ^[9m
/// OFF: ^[29m
strike: String,
/// The hyperlink sequence.
/// FORMAT: \x1B]8;{ID};{URL}\e\\
///
/// `\e\\` may be replaced with BEL `\x07`.
/// Setting both {ID} and {URL} to an empty string represents no hyperlink.
hyperlink: String,
}
impl Attributes {
pub fn new() -> Self {
Attributes {
has_sgr_sequences: false,
foreground: "".to_owned(),
background: "".to_owned(),
underlined: "".to_owned(),
@ -76,34 +98,56 @@ impl Attributes {
underline: "".to_owned(),
italic: "".to_owned(),
strike: "".to_owned(),
hyperlink: "".to_owned(),
}
}
/// Update the attributes with an escape sequence.
/// Returns `false` if the sequence is unsupported.
pub fn update(&mut self, sequence: &str) -> bool {
let mut chars = sequence.char_indices().skip(1);
if let Some((_, t)) = chars.next() {
match t {
'(' => self.update_with_charset('(', chars.map(|(_, c)| c)),
')' => self.update_with_charset(')', chars.map(|(_, c)| c)),
'[' => {
if let Some((i, last)) = chars.last() {
// SAFETY: Always starts with ^[ and ends with m.
self.update_with_csi(last, &sequence[2..i])
} else {
false
pub fn update(&mut self, sequence: EscapeSequence) -> bool {
use EscapeSequence::*;
match sequence {
Text(_) => return false,
Unknown(_) => { /* defer to update_with_unsupported */ }
OSC {
raw_sequence,
command,
..
} => {
if command.starts_with("8;") {
return self.update_with_hyperlink(raw_sequence);
}
/* defer to update_with_unsupported */
}
CSI {
final_byte,
parameters,
..
} => {
match final_byte {
"m" => return self.update_with_sgr(parameters),
_ => {
// NOTE(eth-p): We might want to ignore these, since they involve cursor or buffer manipulation.
/* defer to update_with_unsupported */
}
}
_ => self.update_with_unsupported(sequence),
}
} else {
false
NF { nf_sequence, .. } => {
let mut iter = nf_sequence.chars();
match iter.next() {
Some('(') => return self.update_with_charset('(', iter),
Some(')') => return self.update_with_charset(')', iter),
_ => { /* defer to update_with_unsupported */ }
}
}
}
self.update_with_unsupported(sequence.raw())
}
fn sgr_reset(&mut self) {
self.has_sgr_sequences = false;
self.foreground.clear();
self.background.clear();
self.underlined.clear();
@ -121,13 +165,14 @@ impl Attributes {
.map(|p| p.parse::<u16>())
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
self.has_sgr_sequences = true;
while let Some(p) = iter.next() {
match p {
0 => self.sgr_reset(),
1 => self.bold = format!("\x1B[{}m", parameters),
2 => self.dim = format!("\x1B[{}m", parameters),
3 => self.italic = format!("\x1B[{}m", parameters),
4 => self.underline = format!("\x1B[{}m", parameters),
1 => self.bold = "\x1B[1m".to_owned(),
2 => self.dim = "\x1B[2m".to_owned(),
3 => self.italic = "\x1B[3m".to_owned(),
4 => self.underline = "\x1B[4m".to_owned(),
23 => self.italic.clear(),
24 => self.underline.clear(),
22 => {
@ -138,7 +183,7 @@ impl Attributes {
40..=49 => self.background = Self::parse_color(p, &mut iter),
58..=59 => self.underlined = Self::parse_color(p, &mut iter),
90..=97 => self.foreground = Self::parse_color(p, &mut iter),
100..=107 => self.foreground = Self::parse_color(p, &mut iter),
100..=107 => self.background = Self::parse_color(p, &mut iter),
_ => {
// Unsupported SGR sequence.
// Be compatible and pretend one just wasn't was provided.
@ -149,19 +194,23 @@ impl Attributes {
true
}
fn update_with_csi(&mut self, finalizer: char, sequence: &str) -> bool {
if finalizer == 'm' {
self.update_with_sgr(sequence)
} else {
false
}
}
fn update_with_unsupported(&mut self, sequence: &str) -> bool {
self.unknown_buffer.push_str(sequence);
false
}
fn update_with_hyperlink(&mut self, sequence: &str) -> bool {
if sequence == "8;;" {
// Empty hyperlink ID and HREF -> end of hyperlink.
self.hyperlink.clear();
} else {
self.hyperlink.clear();
self.hyperlink.push_str(sequence);
}
true
}
fn update_with_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
true
@ -179,13 +228,35 @@ impl Attributes {
_ => format!("\x1B[{}m", color),
}
}
/// Gets an ANSI escape sequence to reset all the known attributes.
pub fn to_reset_sequence(&self) -> String {
let mut buf = String::with_capacity(17);
// TODO: Enable me in a later pull request.
// if self.has_sgr_sequences {
// buf.push_str("\x1B[m");
// }
if !self.hyperlink.is_empty() {
buf.push_str("\x1B]8;;\x1B\\"); // Disable hyperlink.
}
// TODO: Enable me in a later pull request.
// if !self.charset.is_empty() {
// // https://espterm.github.io/docs/VT100%20escape%20codes.html
// buf.push_str("\x1B(B\x1B)B"); // setusg0 and setusg1
// }
buf
}
}
impl Display for Attributes {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}{}{}{}{}{}{}",
"{}{}{}{}{}{}{}{}{}{}",
self.foreground,
self.background,
self.underlined,
@ -195,6 +266,7 @@ impl Display for Attributes {
self.underline,
self.italic,
self.strike,
self.hyperlink,
)
}
}
@ -210,3 +282,646 @@ fn join(
.collect::<Vec<String>>()
.join(delimiter)
}
/// A range of indices for a raw ANSI escape sequence.
#[derive(Debug, PartialEq)]
enum EscapeSequenceOffsets {
Text {
start: usize,
end: usize,
},
Unknown {
start: usize,
end: usize,
},
#[allow(clippy::upper_case_acronyms)]
NF {
// https://en.wikipedia.org/wiki/ANSI_escape_code#nF_Escape_sequences
start_sequence: usize,
start: usize,
end: usize,
},
#[allow(clippy::upper_case_acronyms)]
OSC {
// https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences
start_sequence: usize,
start_command: usize,
start_terminator: usize,
end: usize,
},
#[allow(clippy::upper_case_acronyms)]
CSI {
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
start_sequence: usize,
start_parameters: usize,
start_intermediates: usize,
start_final_byte: usize,
end: usize,
},
}
/// An iterator over the offests of ANSI/VT escape sequences within a string.
///
/// ## Example
///
/// ```ignore
/// let iter = EscapeSequenceOffsetsIterator::new("\x1B[33mThis is yellow text.\x1B[m");
/// ```
struct EscapeSequenceOffsetsIterator<'a> {
text: &'a str,
chars: Peekable<CharIndices<'a>>,
}
impl<'a> EscapeSequenceOffsetsIterator<'a> {
pub fn new(text: &'a str) -> EscapeSequenceOffsetsIterator<'a> {
return EscapeSequenceOffsetsIterator {
text,
chars: text.char_indices().peekable(),
};
}
/// Takes values from the iterator while the predicate returns true.
/// If the predicate returns false, that value is left.
fn chars_take_while(&mut self, pred: impl Fn(char) -> bool) -> Option<(usize, usize)> {
self.chars.peek()?;
let start = self.chars.peek().unwrap().0;
let mut end: usize = start;
while let Some((i, c)) = self.chars.peek() {
if !pred(*c) {
break;
}
end = *i + c.len_utf8();
self.chars.next();
}
Some((start, end))
}
fn next_text(&mut self) -> Option<EscapeSequenceOffsets> {
self.chars_take_while(|c| c != '\x1B')
.map(|(start, end)| EscapeSequenceOffsets::Text { start, end })
}
fn next_sequence(&mut self) -> Option<EscapeSequenceOffsets> {
let (start_sequence, c) = self.chars.next().expect("to not be finished");
match self.chars.peek() {
None => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: start_sequence + c.len_utf8(),
}),
Some((_, ']')) => self.next_osc(start_sequence),
Some((_, '[')) => self.next_csi(start_sequence),
Some((i, c)) => match c {
'\x20'..='\x2F' => self.next_nf(start_sequence),
c => Some(EscapeSequenceOffsets::Unknown {
start: start_sequence,
end: i + c.len_utf8(),
}),
},
}
}
fn next_osc(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (osc_open_index, osc_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(osc_open_char, ']');
let mut start_terminator: usize;
let mut end_sequence: usize;
loop {
match self.chars_take_while(|c| !matches!(c, '\x07' | '\x1B')) {
None => {
start_terminator = self.text.len();
end_sequence = start_terminator;
break;
}
Some((_, end)) => {
start_terminator = end;
end_sequence = end;
}
}
match self.chars.next() {
Some((ti, '\x07')) => {
end_sequence = ti + '\x07'.len_utf8();
break;
}
Some((ti, '\x1B')) => {
match self.chars.next() {
Some((i, '\\')) => {
end_sequence = i + '\\'.len_utf8();
break;
}
None => {
end_sequence = ti + '\x1B'.len_utf8();
break;
}
_ => {
// Repeat, since `\\`(anything) isn't a valid ST.
}
}
}
None => {
// Prematurely ends.
break;
}
Some((_, tc)) => {
panic!("this should not be reached: char {:?}", tc)
}
}
}
Some(EscapeSequenceOffsets::OSC {
start_sequence,
start_command: osc_open_index + osc_open_char.len_utf8(),
start_terminator,
end: end_sequence,
})
}
fn next_csi(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (csi_open_index, csi_open_char) = self.chars.next().expect("to not be finished");
debug_assert_eq!(csi_open_char, '[');
let start_parameters: usize = csi_open_index + csi_open_char.len_utf8();
// Keep iterating while within the range of `0x30-0x3F`.
let mut start_intermediates: usize = start_parameters;
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x30'..='\x3F')) {
start_intermediates = end;
}
// Keep iterating while within the range of `0x20-0x2F`.
let mut start_final_byte: usize = start_intermediates;
if let Some((_, end)) = self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
start_final_byte = end;
}
// Take the last char.
let end_of_sequence = match self.chars.next() {
None => start_final_byte,
Some((i, c)) => i + c.len_utf8(),
};
Some(EscapeSequenceOffsets::CSI {
start_sequence,
start_parameters,
start_intermediates,
start_final_byte,
end: end_of_sequence,
})
}
fn next_nf(&mut self, start_sequence: usize) -> Option<EscapeSequenceOffsets> {
let (nf_open_index, nf_open_char) = self.chars.next().expect("to not be finished");
debug_assert!(matches!(nf_open_char, '\x20'..='\x2F'));
let start: usize = nf_open_index;
let mut end: usize = start;
// Keep iterating while within the range of `0x20-0x2F`.
match self.chars_take_while(|c| matches!(c, '\x20'..='\x2F')) {
Some((_, i)) => end = i,
None => {
return Some(EscapeSequenceOffsets::NF {
start_sequence,
start,
end,
})
}
}
// Get the final byte.
if let Some((i, c)) = self.chars.next() {
end = i + c.len_utf8()
}
Some(EscapeSequenceOffsets::NF {
start_sequence,
start,
end,
})
}
}
impl<'a> Iterator for EscapeSequenceOffsetsIterator<'a> {
type Item = EscapeSequenceOffsets;
fn next(&mut self) -> Option<Self::Item> {
match self.chars.peek() {
Some((_, '\x1B')) => self.next_sequence(),
Some((_, _)) => self.next_text(),
None => None,
}
}
}
/// An iterator over ANSI/VT escape sequences within a string.
///
/// ## Example
///
/// ```ignore
/// let iter = EscapeSequenceIterator::new("\x1B[33mThis is yellow text.\x1B[m");
/// ```
pub struct EscapeSequenceIterator<'a> {
text: &'a str,
offset_iter: EscapeSequenceOffsetsIterator<'a>,
}
impl<'a> EscapeSequenceIterator<'a> {
pub fn new(text: &'a str) -> EscapeSequenceIterator<'a> {
return EscapeSequenceIterator {
text,
offset_iter: EscapeSequenceOffsetsIterator::new(text),
};
}
}
impl<'a> Iterator for EscapeSequenceIterator<'a> {
type Item = EscapeSequence<'a>;
fn next(&mut self) -> Option<Self::Item> {
use EscapeSequenceOffsets::*;
self.offset_iter.next().map(|offsets| match offsets {
Unknown { start, end } => EscapeSequence::Unknown(&self.text[start..end]),
Text { start, end } => EscapeSequence::Text(&self.text[start..end]),
NF {
start_sequence,
start,
end,
} => EscapeSequence::NF {
raw_sequence: &self.text[start_sequence..end],
nf_sequence: &self.text[start..end],
},
OSC {
start_sequence,
start_command,
start_terminator,
end,
} => EscapeSequence::OSC {
raw_sequence: &self.text[start_sequence..end],
command: &self.text[start_command..start_terminator],
terminator: &self.text[start_terminator..end],
},
CSI {
start_sequence,
start_parameters,
start_intermediates,
start_final_byte,
end,
} => EscapeSequence::CSI {
raw_sequence: &self.text[start_sequence..end],
parameters: &self.text[start_parameters..start_intermediates],
intermediates: &self.text[start_intermediates..start_final_byte],
final_byte: &self.text[start_final_byte..end],
},
})
}
}
/// A parsed ANSI/VT100 escape sequence.
#[derive(Debug, PartialEq)]
pub enum EscapeSequence<'a> {
Text(&'a str),
Unknown(&'a str),
#[allow(clippy::upper_case_acronyms)]
NF {
raw_sequence: &'a str,
nf_sequence: &'a str,
},
#[allow(clippy::upper_case_acronyms)]
OSC {
raw_sequence: &'a str,
command: &'a str,
terminator: &'a str,
},
#[allow(clippy::upper_case_acronyms)]
CSI {
raw_sequence: &'a str,
parameters: &'a str,
intermediates: &'a str,
final_byte: &'a str,
},
}
impl<'a> EscapeSequence<'a> {
pub fn raw(&self) -> &'a str {
use EscapeSequence::*;
match *self {
Text(raw) => raw,
Unknown(raw) => raw,
NF { raw_sequence, .. } => raw_sequence,
OSC { raw_sequence, .. } => raw_sequence,
CSI { raw_sequence, .. } => raw_sequence,
}
}
}
#[cfg(test)]
mod tests {
use crate::vscreen::{
EscapeSequence, EscapeSequenceIterator, EscapeSequenceOffsets,
EscapeSequenceOffsetsIterator,
};
#[test]
fn test_escape_sequence_offsets_iterator_parses_text() {
let mut iter = EscapeSequenceOffsetsIterator::new("text");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
);
}
#[test]
fn test_escape_sequence_offsets_iterator_parses_text_stops_at_esc() {
let mut iter = EscapeSequenceOffsetsIterator::new("text\x1B[ming");
assert_eq!(
iter.next(),
Some(EscapeSequenceOffsets::Text { start: 0, end: 4 })
);
}
#[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");
}
}

View File

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

View File

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

View File

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