Merge branch 'sharkdp:master' into master

This commit is contained in:
John Higgins 2022-10-09 22:56:11 -07:00 committed by GitHub
commit 03216c9c18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 539 additions and 160 deletions

View File

@ -16,3 +16,9 @@ updates:
interval: monthly interval: monthly
time: "04:00" time: "04:00"
timezone: Europe/Berlin timezone: Europe/Berlin
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
time: "04:00"
timezone: Europe/Berlin

View File

@ -24,14 +24,14 @@ jobs:
default: true default: true
profile: minimal profile: minimal
components: rustfmt components: rustfmt
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- run: cargo fmt -- --check - run: cargo fmt -- --check
license_checks: license_checks:
name: License checks name: License checks
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
submodules: true # we especially want to perform license checks on submodules submodules: true # we especially want to perform license checks on submodules
- run: tests/scripts/license-checks.sh - run: tests/scripts/license-checks.sh
@ -43,7 +43,7 @@ jobs:
MSRV_FEATURES: --no-default-features --features minimal-application,bugreport,build-assets MSRV_FEATURES: --no-default-features --features minimal-application,bugreport,build-assets
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@ -68,7 +68,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Git checkout - name: Git checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
submodules: true # we need all syntax and theme submodules submodules: true # we need all syntax and theme submodules
- name: Install Rust toolchain - name: Install Rust toolchain
@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Git checkout - name: Git checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Prepare environment variables - name: Prepare environment variables
run: | run: |
echo "BAT_SYSTEM_CONFIG_PREFIX=$GITHUB_WORKSPACE/tests/examples/system_config" >> $GITHUB_ENV echo "BAT_SYSTEM_CONFIG_PREFIX=$GITHUB_WORKSPACE/tests/examples/system_config" >> $GITHUB_ENV
@ -139,7 +139,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Git checkout - name: Git checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install Rust toolchain - name: Install Rust toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -186,7 +186,7 @@ jobs:
- { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install prerequisites - name: Install prerequisites
shell: bash shell: bash

View File

@ -13,6 +13,13 @@
## `bat` as a library ## `bat` as a library
# v0.22.1
## Bugfixes
- Bring back pre-processing of ANSI escape characters to so that some common `bat` use cases starts working again. See #2308 (@Enselic)
# v0.22.0 # v0.22.0
## Features ## Features

36
Cargo.lock generated
View File

@ -74,7 +74,7 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]] [[package]]
name = "bat" name = "bat"
version = "0.22.0" version = "0.22.1"
dependencies = [ dependencies = [
"ansi_colours", "ansi_colours",
"ansi_term", "ansi_term",
@ -231,13 +231,13 @@ dependencies = [
[[package]] [[package]]
name = "console" name = "console"
version = "0.15.1" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c"
dependencies = [ dependencies = [
"encode_unicode", "encode_unicode",
"lazy_static",
"libc", "libc",
"once_cell",
"terminal_size", "terminal_size",
"unicode-width", "unicode-width",
"winapi", "winapi",
@ -699,9 +699,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.13.1" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]] [[package]]
name = "onig" name = "onig"
@ -964,24 +964,24 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.13" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.144" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1160,18 +1160,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.33" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.33" version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1226,9 +1226,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.9" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]] [[package]]
name = "url" name = "url"

View File

@ -6,7 +6,7 @@ homepage = "https://github.com/sharkdp/bat"
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
name = "bat" name = "bat"
repository = "https://github.com/sharkdp/bat" repository = "https://github.com/sharkdp/bat"
version = "0.22.0" version = "0.22.1"
exclude = ["assets/syntaxes/*", "assets/themes/*"] exclude = ["assets/syntaxes/*", "assets/themes/*"]
build = "build.rs" build = "build.rs"
edition = '2018' edition = '2018'
@ -44,15 +44,15 @@ atty = { version = "0.2.14", optional = true }
ansi_term = "^0.12.1" ansi_term = "^0.12.1"
ansi_colours = "^1.1" ansi_colours = "^1.1"
bincode = "1.0" bincode = "1.0"
console = "0.15.1" console = "0.15.2"
flate2 = "1.0" flate2 = "1.0"
once_cell = "1.13" once_cell = "1.15"
thiserror = "1.0" thiserror = "1.0"
wild = { version = "2.1", optional = true } wild = { version = "2.1", optional = true }
content_inspector = "0.2.4" content_inspector = "0.2.4"
encoding = "0.2" encoding = "0.2"
shell-words = { version = "1.1.0", optional = true } shell-words = { version = "1.1.0", optional = true }
unicode-width = "0.1.9" unicode-width = "0.1.10"
globset = "0.4" globset = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8" serde_yaml = "0.8"

View File

@ -10,6 +10,27 @@ __bat_init_completion()
_get_comp_words_by_ref "$@" cur prev words cword _get_comp_words_by_ref "$@" cur prev words cword
} }
__bat_escape_completions()
{
# Do not escape if completing a quoted value.
[[ $cur == [\"\']* ]] && return 0
# printf -v to an array index is available in bash >= 4.1.
# Use it if available, as -o filenames is semantically incorrect if
# we are not actually completing filenames, and it has side effects
# (e.g. adds trailing slash to candidates matching present dirs).
if ((
BASH_VERSINFO[0] > 4 || \
BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] > 0
)); then
local i
for i in ${!COMPREPLY[*]}; do
printf -v "COMPREPLY[i]" %q "${COMPREPLY[i]}"
done
else
compopt -o filenames
fi
}
_bat() { _bat() {
local cur prev words cword split=false local cur prev words cword split=false
if declare -F _init_completion >/dev/null 2>&1; then if declare -F _init_completion >/dev/null 2>&1; then
@ -27,7 +48,12 @@ _bat() {
;; ;;
esac esac
COMPREPLY=($(compgen -W " COMPREPLY=($(compgen -W "
--build --clear --source --target --blank --help --build
--clear
--source
--target
--blank
--help
" -- "$cur")) " -- "$cur"))
return 0 return 0
fi fi
@ -40,13 +66,26 @@ _bat() {
printf "%s\n" "$lang" printf "%s\n" "$lang"
done done
)" -- "$cur")) )" -- "$cur"))
compopt -o filenames # for escaping __bat_escape_completions
return 0 return 0
;; ;;
-H | --highlight-line | --diff-context | --tabs | --terminal-width | \ -H | --highlight-line | \
-m | --map-syntax | --style | --line-range | -h | --help | -V | \ --diff-context | \
--version | --diagnostic | --config-file | --config-dir | \ --tabs | \
--cache-dir | --generate-config-file) --terminal-width | \
-m | --map-syntax | \
--ignored-suffix | \
--list-themes | \
--line-range | \
-L | --list-languages | \
--diagnostic | \
--acknowledgements | \
-h | --help | \
-V | --version | \
--cache-dir | \
--config-dir | \
--config-file | \
--generate-config-file)
# argument required but no completion available, or option # argument required but no completion available, or option
# causes an exit # causes an exit
return 0 return 0
@ -74,22 +113,71 @@ _bat() {
--theme) --theme)
local IFS=$'\n' local IFS=$'\n'
COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur")) COMPREPLY=($(compgen -W "$("$1" --list-themes)" -- "$cur"))
compopt -o filenames # for escaping __bat_escape_completions
return 0 return 0
;; ;;
--style)
# shellcheck disable=SC2034
local -a styles=(
default
full
auto
plain
changes
header
header-filename
header-filesize
grid
rule
numbers
snip
)
# shellcheck disable=SC2016
if declare -F _comp_delimited >/dev/null 2>&1; then
# bash-completion > 2.11
_comp_delimited , -W '"${styles[@]}"'
else
COMPREPLY=($(compgen -W '${styles[@]}' -- "$cur"))
fi
return 0
esac esac
$split && return 0 $split && return 0
if [[ $cur == -* ]]; then if [[ $cur == -* ]]; then
# --unbuffered excluded intentionally (no-op)
COMPREPLY=($(compgen -W " COMPREPLY=($(compgen -W "
--show-all --plain --language --highlight-line --show-all
--file-name --diff --diff-context --tabs --wrap --plain
--terminal-width --number --color --italic-text --language
--decorations --paging --pager --map-syntax --theme --highlight-line
--list-themes --style --line-range --list-languages --file-name
--help --version --force-colorization --unbuffered --diff
--diagnostic --config-file --config-dir --cache-dir --diff-context
--tabs
--wrap
--terminal-width
--number
--color
--italic-text
--decorations
--force-colorization
--paging
--pager
--map-syntax
--ignored-suffix
--theme
--list-themes
--style
--line-range
--list-languages
--diagnostic
--acknowledgements
--help
--version
--cache-dir
--config-dir
--config-file
--generate-config-file --generate-config-file
" -- "$cur")) " -- "$cur"))
return 0 return 0

View File

@ -186,7 +186,7 @@ man 2 select
### On Ubuntu (`apt` を使用) ### On Ubuntu (`apt` を使用)
*... や他のDebianベースのLinuxディストリビューション* *... や他のDebianベースのLinuxディストリビューション*
Ubuntu Eoan 19.10 または Debian 不安定版 sid 以降の [the Ubuntu `bat` package](https://packages.ubuntu.com/eoan/bat) または [the Debian `bat` package](https://packages.debian.org/sid/bat) からインストールできます: [20.04 ("Focal") 以降の Ubuntu](https://packages.ubuntu.com/search?keywords=bat&exact=1) または [2021 年 8 月以降の Debian (Debian 11 - "Bullseye")](https://packages.debian.org/bullseye/bat) では `bat` パッケージが利用できます。
```bash ```bash
apt install bat apt install bat

View File

@ -46,6 +46,7 @@ pub(crate) mod printer;
pub mod style; pub mod style;
pub(crate) mod syntax_mapping; pub(crate) mod syntax_mapping;
mod terminal; mod terminal;
mod vscreen;
pub(crate) mod wrapping; pub(crate) mod wrapping;
pub use pretty_printer::{Input, PrettyPrinter, Syntax}; pub use pretty_printer::{Input, PrettyPrinter, Syntax};

View File

@ -1,28 +1,37 @@
use std::fmt::Write; use std::fmt::Write;
use console::AnsiCodeIterator;
/// Expand tabs like an ANSI-enabled expand(1). /// Expand tabs like an ANSI-enabled expand(1).
pub fn expand_tabs(mut text: &str, width: usize, cursor: &mut usize) -> String { pub fn expand_tabs(line: &str, width: usize, cursor: &mut usize) -> String {
let mut buffer = String::with_capacity(text.len() * 2); let mut buffer = String::with_capacity(line.len() * 2);
while let Some(index) = text.find('\t') { for chunk in AnsiCodeIterator::new(line) {
// Add previous text. match chunk {
if index > 0 { (text, true) => buffer.push_str(text),
*cursor += index; (mut text, false) => {
buffer.push_str(&text[0..index]); while let Some(index) = text.find('\t') {
// Add previous text.
if index > 0 {
*cursor += index;
buffer.push_str(&text[0..index]);
}
// Add tab.
let spaces = width - (*cursor % width);
*cursor += spaces;
buffer.push_str(&*" ".repeat(spaces));
// Next.
text = &text[index + 1..text.len()];
}
*cursor += text.len();
buffer.push_str(text);
}
} }
// Add tab.
let spaces = width - (*cursor % width);
*cursor += spaces;
buffer.push_str(&*" ".repeat(spaces));
// Next.
text = &text[index + 1..text.len()];
} }
*cursor += text.len();
buffer.push_str(text);
buffer buffer
} }

View File

@ -6,6 +6,8 @@ use ansi_term::Style;
use bytesize::ByteSize; use bytesize::ByteSize;
use console::AnsiCodeIterator;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::Color; use syntect::highlighting::Color;
use syntect::highlighting::Theme; use syntect::highlighting::Theme;
@ -31,6 +33,7 @@ use crate::line_range::RangeCheckResult;
use crate::preprocessor::{expand_tabs, replace_nonprintable}; use crate::preprocessor::{expand_tabs, replace_nonprintable};
use crate::style::StyleComponent; use crate::style::StyleComponent;
use crate::terminal::{as_terminal_escaped, to_ansi_color}; use crate::terminal::{as_terminal_escaped, to_ansi_color};
use crate::vscreen::AnsiStyle;
use crate::wrapping::WrappingMode; use crate::wrapping::WrappingMode;
pub(crate) trait Printer { pub(crate) trait Printer {
@ -119,6 +122,7 @@ pub(crate) struct InteractivePrinter<'a> {
config: &'a Config<'a>, config: &'a Config<'a>,
decorations: Vec<Box<dyn Decoration>>, decorations: Vec<Box<dyn Decoration>>,
panel_width: usize, panel_width: usize,
ansi_style: AnsiStyle,
content_type: Option<ContentType>, content_type: Option<ContentType>,
#[cfg(feature = "git")] #[cfg(feature = "git")]
pub line_changes: &'a Option<LineChanges>, pub line_changes: &'a Option<LineChanges>,
@ -202,6 +206,7 @@ impl<'a> InteractivePrinter<'a> {
config, config,
decorations, decorations,
content_type: input.reader.content_type, content_type: input.reader.content_type,
ansi_style: AnsiStyle::new(),
#[cfg(feature = "git")] #[cfg(feature = "git")]
line_changes, line_changes,
highlighter_from_set, highlighter_from_set,
@ -480,7 +485,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange; self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
if highlight_this_line && self.config.theme == "ansi" { if highlight_this_line && self.config.theme == "ansi" {
write!(handle, "\x1B[4m")?; self.ansi_style.update("^[4m");
} }
let background_color = self let background_color = self
@ -507,37 +512,51 @@ impl<'a> Printer for InteractivePrinter<'a> {
let italics = self.config.use_italic_text; let italics = self.config.use_italic_text;
for &(style, region) in &regions { for &(style, region) in &regions {
let text = &*self.preprocess(region, &mut cursor_total); let ansi_iterator = AnsiCodeIterator::new(region);
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n'); for chunk in ansi_iterator {
match chunk {
// ANSI escape passthrough.
(ansi, true) => {
self.ansi_style.update(ansi);
write!(handle, "{}", ansi)?;
}
write!( // Regular text.
handle, (text, false) => {
"{}", let text = &*self.preprocess(text, &mut cursor_total);
as_terminal_escaped( let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
style,
text_trimmed,
true_color,
colored_output,
italics,
background_color
)
)?;
if text.len() != text_trimmed.len() { write!(
if let Some(background_color) = background_color { handle,
let ansi_style = Style { "{}",
background: to_ansi_color(background_color, true_color), as_terminal_escaped(
..Default::default() style,
}; &format!("{}{}", self.ansi_style, text_trimmed),
true_color,
colored_output,
italics,
background_color
)
)?;
let width = if cursor_total <= cursor_max { if text.len() != text_trimmed.len() {
cursor_max - cursor_total + 1 if let Some(background_color) = background_color {
} else { let ansi_style = Style {
0 background: to_ansi_color(background_color, true_color),
}; ..Default::default()
write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?; };
let width = if cursor_total <= cursor_max {
cursor_max - cursor_total + 1
} else {
0
};
write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
}
write!(handle, "{}", &text[text_trimmed.len()..])?;
}
}
} }
write!(handle, "{}", &text[text_trimmed.len()..])?;
} }
} }
@ -546,82 +565,98 @@ impl<'a> Printer for InteractivePrinter<'a> {
} }
} else { } else {
for &(style, region) in &regions { for &(style, region) in &regions {
let text = self.preprocess( let ansi_iterator = AnsiCodeIterator::new(region);
region.trim_end_matches(|c| c == '\r' || c == '\n'), for chunk in ansi_iterator {
&mut cursor_total, match chunk {
); // ANSI escape passthrough.
(ansi, true) => {
let mut max_width = cursor_max - cursor; self.ansi_style.update(ansi);
write!(handle, "{}", ansi)?;
// line buffer (avoid calling write! for every character)
let mut line_buf = String::with_capacity(max_width * 4);
// Displayed width of line_buf
let mut current_width = 0;
for c in text.chars() {
// calculate the displayed width for next character
let cw = c.width().unwrap_or(0);
current_width += cw;
// if next character cannot be printed on this line,
// flush the buffer.
if current_width > max_width {
// Generate wrap padding if not already generated.
if panel_wrap.is_none() {
panel_wrap = if self.panel_width > 0 {
Some(format!(
"{} ",
self.decorations
.iter()
.map(|d| d.generate(line_number, true, self).text)
.collect::<Vec<String>>()
.join(" ")
))
} else {
Some("".to_string())
}
} }
// It wraps. // Regular text.
write!( (text, false) => {
handle, let text = self.preprocess(
"{}\n{}", text.trim_end_matches(|c| c == '\r' || c == '\n'),
as_terminal_escaped( &mut cursor_total,
style, );
&line_buf,
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
background_color
),
panel_wrap.clone().unwrap()
)?;
cursor = 0; let mut max_width = cursor_max - cursor;
max_width = cursor_max;
line_buf.clear(); // line buffer (avoid calling write! for every character)
current_width = cw; let mut line_buf = String::with_capacity(max_width * 4);
// Displayed width of line_buf
let mut current_width = 0;
for c in text.chars() {
// calculate the displayed width for next character
let cw = c.width().unwrap_or(0);
current_width += cw;
// if next character cannot be printed on this line,
// flush the buffer.
if current_width > max_width {
// Generate wrap padding if not already generated.
if panel_wrap.is_none() {
panel_wrap = if self.panel_width > 0 {
Some(format!(
"{} ",
self.decorations
.iter()
.map(|d| d
.generate(line_number, true, self)
.text)
.collect::<Vec<String>>()
.join(" ")
))
} else {
Some("".to_string())
}
}
// It wraps.
write!(
handle,
"{}\n{}",
as_terminal_escaped(
style,
&*format!("{}{}", self.ansi_style, line_buf),
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
background_color
),
panel_wrap.clone().unwrap()
)?;
cursor = 0;
max_width = cursor_max;
line_buf.clear();
current_width = cw;
}
line_buf.push(c);
}
// flush the buffer
cursor += current_width;
write!(
handle,
"{}",
as_terminal_escaped(
style,
&*format!("{}{}", self.ansi_style, line_buf),
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
background_color
)
)?;
}
} }
line_buf.push(c);
} }
// flush the buffer
cursor += current_width;
write!(
handle,
"{}",
as_terminal_escaped(
style,
&line_buf,
self.config.true_color,
self.config.colored_output,
self.config.use_italic_text,
background_color
)
)?;
} }
if let Some(background_color) = background_color { if let Some(background_color) = background_color {
@ -640,6 +675,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
} }
if highlight_this_line && self.config.theme == "ansi" { if highlight_this_line && self.config.theme == "ansi" {
self.ansi_style.update("^[24m");
write!(handle, "\x1B[24m")?; write!(handle, "\x1B[24m")?;
} }

212
src/vscreen.rs Normal file
View File

@ -0,0 +1,212 @@
use std::fmt::{Display, Formatter};
// Wrapper to avoid unnecessary branching when input doesn't have ANSI escape sequences.
pub struct AnsiStyle {
attributes: Option<Attributes>,
}
impl AnsiStyle {
pub fn new() -> Self {
AnsiStyle { attributes: None }
}
pub fn update(&mut self, sequence: &str) -> bool {
match &mut self.attributes {
Some(a) => a.update(sequence),
None => {
self.attributes = Some(Attributes::new());
self.attributes.as_mut().unwrap().update(sequence)
}
}
}
}
impl Display for AnsiStyle {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.attributes {
Some(ref a) => a.fmt(f),
None => Ok(()),
}
}
}
struct Attributes {
foreground: String,
background: String,
underlined: String,
/// The character set to use.
/// REGEX: `\^[()][AB0-3]`
charset: String,
/// A buffer for unknown sequences.
unknown_buffer: String,
/// ON: ^[1m
/// OFF: ^[22m
bold: String,
/// ON: ^[2m
/// OFF: ^[22m
dim: String,
/// ON: ^[4m
/// OFF: ^[24m
underline: String,
/// ON: ^[3m
/// OFF: ^[23m
italic: String,
/// ON: ^[9m
/// OFF: ^[29m
strike: String,
}
impl Attributes {
pub fn new() -> Self {
Attributes {
foreground: "".to_owned(),
background: "".to_owned(),
underlined: "".to_owned(),
charset: "".to_owned(),
unknown_buffer: "".to_owned(),
bold: "".to_owned(),
dim: "".to_owned(),
underline: "".to_owned(),
italic: "".to_owned(),
strike: "".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
}
}
_ => self.update_with_unsupported(sequence),
}
} else {
false
}
}
fn sgr_reset(&mut self) {
self.foreground.clear();
self.background.clear();
self.underlined.clear();
self.bold.clear();
self.dim.clear();
self.underline.clear();
self.italic.clear();
self.strike.clear();
}
fn update_with_sgr(&mut self, parameters: &str) -> bool {
let mut iter = parameters
.split(';')
.map(|p| if p.is_empty() { "0" } else { p })
.map(|p| p.parse::<u16>())
.map(|p| p.unwrap_or(0)); // Treat errors as 0.
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),
23 => self.italic.clear(),
24 => self.underline.clear(),
22 => {
self.bold.clear();
self.dim.clear();
}
30..=39 => self.foreground = Self::parse_color(p, &mut iter),
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),
_ => {
// Unsupported SGR sequence.
// Be compatible and pretend one just wasn't was provided.
}
}
}
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_charset(&mut self, kind: char, set: impl Iterator<Item = char>) -> bool {
self.charset = format!("\x1B{}{}", kind, set.take(1).collect::<String>());
true
}
fn parse_color(color: u16, parameters: &mut dyn Iterator<Item = u16>) -> String {
match color % 10 {
8 => match parameters.next() {
Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)),
Some(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
Some(c) => format!("\x1B[{};{}m", color, c),
_ => "".to_owned(),
},
9 => "".to_owned(),
_ => format!("\x1B[{}m", color),
}
}
}
impl Display for Attributes {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}{}{}{}{}{}{}",
self.foreground,
self.background,
self.underlined,
self.charset,
self.bold,
self.dim,
self.underline,
self.italic,
self.strike,
)
}
}
fn join(
delimiter: &str,
limit: usize,
iterator: &mut dyn Iterator<Item = impl ToString>,
) -> String {
iterator
.take(limit)
.map(|i| i.to_string())
.collect::<Vec<String>>()
.join(delimiter)
}

View File

@ -1442,6 +1442,26 @@ fn ansi_highlight_underline() {
.stderr(""); .stderr("");
} }
// Ensure that ANSI passthrough is emitted properly for both wrapping and non-wrapping printer.
// See https://github.com/sharkdp/bat/issues/2307 for what common use case this test tests.
#[test]
fn ansi_passthrough_emit() {
for wrapping in &["never", "character"] {
bat()
.arg("--paging=never")
.arg("--color=never")
.arg("--terminal-width=80")
.arg(format!("--wrap={}", wrapping))
.arg("--decorations=always")
.arg("--style=plain")
.write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n")
.assert()
.success()
.stdout("\x1B[33m\x1B[33mColor\n\x1B[33mColor \x1B[m\nPlain\n")
.stderr("");
}
}
#[test] #[test]
fn ignored_suffix_arg() { fn ignored_suffix_arg() {
bat() bat()