1
0
mirror of https://github.com/sharkdp/bat.git synced 2025-03-15 15:18:45 +00:00

Merge branch 'sharkdp:master' into master

This commit is contained in:
nazdridoy 2024-02-25 17:51:37 +06:00 committed by GitHub
commit 31db954393
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 373 additions and 78 deletions

View File

@ -29,5 +29,5 @@ jobs:
ADDED=$(git diff -U0 "origin/${PR_BASE}" HEAD -- CHANGELOG.md | grep -P '^\+[^\+].+$') ADDED=$(git diff -U0 "origin/${PR_BASE}" HEAD -- CHANGELOG.md | grep -P '^\+[^\+].+$')
echo "Added lines in CHANGELOG.md:" echo "Added lines in CHANGELOG.md:"
echo "$ADDED" echo "$ADDED"
echo "Grepping for PR info:" echo "Grepping for PR info (see CONTRIBUTING.md):"
grep "#${PR_NUMBER}\\b.*@${PR_SUBMITTER}\\b" <<< "$ADDED" grep "#${PR_NUMBER}\\b.*@${PR_SUBMITTER}\\b" <<< "$ADDED"

View File

@ -2,6 +2,11 @@
## Features ## Features
- Set terminal title to file names when Paging is not Paging::Never #2807 (@Oliver-Looney)
- `bat --squeeze-blank`/`bat -s` will now squeeze consecutive empty lines, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `bat --squeeze-limit` to set the maximum number of empty consecutive when using `--squeeze-blank`, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
- `PrettyPrinter::squeeze_empty_lines` to support line squeezing for bat as a library, see #1441 (@eth-p) and #2665 (@einfachIrgendwer0815)
## Bugfixes ## Bugfixes
- Fix long file name wrapping in header, see #2835 (@FilipRazek) - Fix long file name wrapping in header, see #2835 (@FilipRazek)
@ -24,12 +29,19 @@
- Pull in fix for unsafe-libyaml security advisory, see #2812 (@dtolnay) - Pull in fix for unsafe-libyaml security advisory, see #2812 (@dtolnay)
- Update git-version dependency to use Syn v2, see #2816 (@dtolnay) - Update git-version dependency to use Syn v2, see #2816 (@dtolnay)
- Update git2 dependency to v0.18.2, see #2852 (@eth-p) - Update git2 dependency to v0.18.2, see #2852 (@eth-p)
- Improve performance when color output disabled, see #2397 and #2857 (@eth-p)
- Relax syntax mapping rule restrictions to allow brace expansion #2865 (@cyqsimon)
- Apply clippy fixes #2864 (@cyqsimon) - Apply clippy fixes #2864 (@cyqsimon)
## Syntaxes ## Syntaxes
- `cmd-help`: scope subcommands followed by other terms, and other misc improvements, see #2819 (@victor-gp) - `cmd-help`: scope subcommands followed by other terms, and other misc improvements, see #2819 (@victor-gp)
- Upgrade JQ syntax, see #2820 (@dependabot[bot]) - Upgrade JQ syntax, see #2820 (@dependabot[bot])
- Add syntax mapping for quadman quadlets #2866 (@cyqsimon)
- Map containers .conf files to TOML syntax #2867 (@cyqsimon)
- 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 ## Themes

View File

@ -6,21 +6,42 @@ Thank you for considering to contribute to `bat`!
## Add an entry to the changelog ## Add an entry to the changelog
If your contribution changes the behavior of `bat` (as opposed to a typo-fix Keeping the [`CHANGELOG.md`](CHANGELOG.md) file up-to-date makes the release
in the documentation), please update the [`CHANGELOG.md`](CHANGELOG.md) file process much easier and therefore helps to get your changes into a new `bat`
and describe your changes. This makes the release process much easier and release faster. However, not every change to the repository requires a
therefore helps to get your changes into a new `bat` release faster. changelog entry. Below are a few examples of that.
Please update the changelog if your contribution contains changes regarding
any of the following:
- the behavior of `bat`
- syntax mappings
- syntax definitions
- themes
- the build system, linting, or CI workflows
A changelog entry is not necessary when:
- updating documentation
- fixing typos
>[!NOTE]
> For PRs, a CI workflow verifies that a suitable changelog entry is
> added. If such an entry is missing, the workflow will fail. If your
> changes do not need an entry to the changelog (see above), that
> workflow failure can be disregarded.
### Changelog entry format
The top of the `CHANGELOG` contains a *"unreleased"* section with a few The top of the `CHANGELOG` contains a *"unreleased"* section with a few
subsections (Features, Bugfixes, …). Please add your entry to the subsection subsections (Features, Bugfixes, …). Please add your entry to the subsection
that best describes your change. that best describes your change.
Entries follow this format: Entries must follow this format:
``` ```
- Short description of what has been changed, see #123 (@user) - Short description of what has been changed, see #123 (@user)
``` ```
Here, `#123` is the number of the original issue and/or your pull request. Please replace `#123` with the number of your pull request (not issue) and
Please replace `@user` by your GitHub username. `@user` by your GitHub username.
## Development ## Development

View File

@ -53,14 +53,16 @@ struct Matcher(Vec<MatcherSegment>);
/// ///
/// Note that this implementation is rather strict: it will greedily interpret /// Note that this implementation is rather strict: it will greedily interpret
/// every valid environment variable replacement as such, then immediately /// every valid environment variable replacement as such, then immediately
/// hard-error if it finds a '$', '{', or '}' anywhere in the remaining text /// hard-error if it finds a '$' anywhere in the remaining text segments.
/// segments.
/// ///
/// The reason for this strictness is I currently cannot think of a valid reason /// The reason for this strictness is I currently cannot think of a valid reason
/// why you would ever need '$', '{', or '}' as plaintext in a glob pattern. /// why you would ever need '$' as plaintext in a glob pattern. Therefore any
/// Therefore any such occurrences are likely human errors. /// such occurrences are likely human errors.
/// ///
/// If we later discover some edge cases, it's okay to make it more permissive. /// If we later discover some edge cases, it's okay to make it more permissive.
///
/// Revision history:
/// - 2024-02-20: allow `{` and `}` (glob brace expansion)
impl FromStr for Matcher { impl FromStr for Matcher {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -106,7 +108,7 @@ impl FromStr for Matcher {
if non_empty_segments if non_empty_segments
.iter() .iter()
.filter_map(Seg::text) .filter_map(Seg::text)
.any(|t| t.contains(['$', '{', '}'])) .any(|t| t.contains('$'))
{ {
bail!(r#"Invalid matcher: "{s}""#); bail!(r#"Invalid matcher: "{s}""#);
} }

View File

@ -116,6 +116,12 @@ Options:
--list-themes --list-themes
Display a list of supported themes for syntax highlighting. Display a list of supported themes for syntax highlighting.
-s, --squeeze-blank
Squeeze consecutive empty lines into a single empty line.
--squeeze-limit <squeeze-limit>
Set the maximum number of consecutive empty lines to be printed.
--style <components> --style <components>
Configure which elements (line numbers, file headers, grid borders, Git modifications, ..) Configure which elements (line numbers, file headers, grid borders, Git modifications, ..)
to display in addition to the file contents. The argument is a comma-separated list of to display in addition to the file contents. The argument is a comma-separated list of
@ -123,6 +129,9 @@ Options:
set a default style, add the '--style=".."' option to the configuration file or export the set a default style, add the '--style=".."' option to the configuration file or export the
BAT_STYLE environment variable (e.g.: export BAT_STYLE=".."). BAT_STYLE environment variable (e.g.: export BAT_STYLE="..").
By default, the following components are enabled:
changes, grid, header-filename, numbers, snip
Possible values: Possible values:
* default: enables recommended style components (default). * default: enables recommended style components (default).
@ -160,6 +169,9 @@ Options:
--acknowledgements --acknowledgements
Show acknowledgements. Show acknowledgements.
--set-terminal-title
Sets terminal title to filenames when using a pager.
-h, --help -h, --help
Print help (see a summary with '-h') Print help (see a summary with '-h')

View File

@ -43,6 +43,8 @@ Options:
Set the color theme for syntax highlighting. Set the color theme for syntax highlighting.
--list-themes --list-themes
Display all supported highlighting themes. Display all supported highlighting themes.
-s, --squeeze-blank
Squeeze consecutive empty lines.
--style <components> --style <components>
Comma-separated list of style elements to display (*default*, auto, full, plain, changes, Comma-separated list of style elements to display (*default*, auto, full, plain, changes,
header, header-filename, header-filesize, grid, rule, numbers, snip). header, header-filename, header-filesize, grid, rule, numbers, snip).

View File

@ -13,6 +13,6 @@ fn main() {
println!("Themes:"); println!("Themes:");
for theme in printer.themes() { for theme in printer.themes() {
println!("- {}", theme); println!("- {theme}");
} }
} }

View File

@ -380,7 +380,7 @@ fn asset_from_contents<T: serde::de::DeserializeOwned>(
} else { } else {
bincode::deserialize_from(contents) bincode::deserialize_from(contents)
} }
.map_err(|_| format!("Could not parse {}", description).into()) .map_err(|_| format!("Could not parse {description}").into())
} }
fn asset_from_cache<T: serde::de::DeserializeOwned>( fn asset_from_cache<T: serde::de::DeserializeOwned>(
@ -396,7 +396,7 @@ fn asset_from_cache<T: serde::de::DeserializeOwned>(
) )
})?; })?;
asset_from_contents(&contents[..], description, compressed) asset_from_contents(&contents[..], description, compressed)
.map_err(|_| format!("Could not parse cached {}", description).into()) .map_err(|_| format!("Could not parse cached {description}").into())
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -466,7 +466,7 @@ mod tests {
let file_path = self.temp_dir.path().join(file_name); let file_path = self.temp_dir.path().join(file_name);
{ {
let mut temp_file = File::create(&file_path).unwrap(); let mut temp_file = File::create(&file_path).unwrap();
writeln!(temp_file, "{}", first_line).unwrap(); writeln!(temp_file, "{first_line}").unwrap();
} }
let input = Input::ordinary_file(&file_path); let input = Input::ordinary_file(&file_path);
@ -514,8 +514,7 @@ mod tests {
if !consistent { if !consistent {
eprintln!( eprintln!(
"Inconsistent syntax detection:\nFor File: {}\nFor Reader: {}", "Inconsistent syntax detection:\nFor File: {as_file}\nFor Reader: {as_reader}"
as_file, as_reader
) )
} }

View File

@ -93,7 +93,7 @@ fn print_unlinked_contexts(syntax_set: &SyntaxSet) {
if !missing_contexts.is_empty() { if !missing_contexts.is_empty() {
println!("Some referenced contexts could not be found!"); println!("Some referenced contexts could not be found!");
for context in missing_contexts { for context in missing_contexts {
println!("- {}", context); println!("- {context}");
} }
} }
} }
@ -152,7 +152,7 @@ pub(crate) fn asset_to_contents<T: serde::Serialize>(
} else { } else {
bincode::serialize_into(&mut contents, asset) bincode::serialize_into(&mut contents, asset)
} }
.map_err(|_| format!("Could not serialize {}", description))?; .map_err(|_| format!("Could not serialize {description}"))?;
Ok(contents) Ok(contents)
} }

View File

@ -80,7 +80,7 @@ fn handle_license(path: &Path) -> Result<Option<String>> {
} else if license_not_needed_in_acknowledgements(&license_text) { } else if license_not_needed_in_acknowledgements(&license_text) {
Ok(None) Ok(None)
} else { } else {
Err(format!("ERROR: License is of unknown type: {:?}", path).into()) Err(format!("ERROR: License is of unknown type: {path:?}").into())
} }
} }
@ -125,7 +125,7 @@ fn append_to_acknowledgements(
relative_path: &str, relative_path: &str,
license_text: &str, license_text: &str,
) { ) {
write!(acknowledgements, "## {}\n\n{}", relative_path, license_text).ok(); write!(acknowledgements, "## {relative_path}\n\n{license_text}").ok();
// Make sure the last char is a newline to not mess up formatting later // Make sure the last char is a newline to not mess up formatting later
if acknowledgements if acknowledgements

View File

@ -88,7 +88,7 @@ impl TryFrom<ThemeSet> for LazyThemeSet {
let lazy_theme = LazyTheme { let lazy_theme = LazyTheme {
serialized: crate::assets::build_assets::asset_to_contents( serialized: crate::assets::build_assets::asset_to_contents(
&theme, &theme,
&format!("theme {}", name), &format!("theme {name}"),
COMPRESS_LAZY_THEMES, COMPRESS_LAZY_THEMES,
)?, )?,
deserialized: OnceCell::new(), deserialized: OnceCell::new(),

View File

@ -289,6 +289,17 @@ impl App {
use_custom_assets: !self.matches.get_flag("no-custom-assets"), use_custom_assets: !self.matches.get_flag("no-custom-assets"),
#[cfg(feature = "lessopen")] #[cfg(feature = "lessopen")]
use_lessopen: self.matches.get_flag("lessopen"), use_lessopen: self.matches.get_flag("lessopen"),
set_terminal_title: self.matches.get_flag("set-terminal-title"),
squeeze_lines: if self.matches.get_flag("squeeze-blank") {
Some(
self.matches
.get_one::<usize>("squeeze-limit")
.map(|limit| limit.to_owned())
.unwrap_or(1),
)
} else {
None
},
}) })
} }

View File

@ -44,7 +44,7 @@ pub fn assets_from_cache_or_binary(
} }
fn clear_asset(path: PathBuf, description: &str) { fn clear_asset(path: PathBuf, description: &str) {
print!("Clearing {} ... ", description); print!("Clearing {description} ... ");
match fs::remove_file(&path) { match fs::remove_file(&path) {
Err(err) if err.kind() == io::ErrorKind::NotFound => { Err(err) if err.kind() == io::ErrorKind::NotFound => {
println!("skipped (not present)"); println!("skipped (not present)");

View File

@ -387,6 +387,21 @@ pub fn build_app(interactive_output: bool) -> Command {
.help("Display all supported highlighting themes.") .help("Display all supported highlighting themes.")
.long_help("Display a list of supported themes for syntax highlighting."), .long_help("Display a list of supported themes for syntax highlighting."),
) )
.arg(
Arg::new("squeeze-blank")
.long("squeeze-blank")
.short('s')
.action(ArgAction::SetTrue)
.help("Squeeze consecutive empty lines.")
.long_help("Squeeze consecutive empty lines into a single empty line.")
)
.arg(
Arg::new("squeeze-limit")
.long("squeeze-limit")
.value_parser(|s: &str| s.parse::<usize>().map_err(|_| "Requires a non-negative number".to_owned()))
.long_help("Set the maximum number of consecutive empty lines to be printed.")
.hide_short_help(true)
)
.arg( .arg(
Arg::new("style") Arg::new("style")
.long("style") .long("style")
@ -415,7 +430,7 @@ pub fn build_app(interactive_output: bool) -> Command {
}); });
if let Some(invalid) = invalid_vals.next() { if let Some(invalid) = invalid_vals.next() {
Err(format!("Unknown style, '{}'", invalid)) Err(format!("Unknown style, '{invalid}'"))
} else { } else {
Ok(val.to_owned()) Ok(val.to_owned())
} }
@ -432,6 +447,8 @@ pub fn build_app(interactive_output: bool) -> Command {
pre-defined style ('full'). To set a default style, add the \ pre-defined style ('full'). To set a default style, add the \
'--style=\"..\"' option to the configuration file or export the \ '--style=\"..\"' option to the configuration file or export the \
BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\").\n\n\ BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\").\n\n\
By default, the following components are enabled:\n \
changes, grid, header-filename, numbers, snip\n\n\
Possible values:\n\n \ Possible values:\n\n \
* default: enables recommended style components (default).\n \ * default: enables recommended style components (default).\n \
* full: enables all available components.\n \ * full: enables all available components.\n \
@ -567,6 +584,13 @@ pub fn build_app(interactive_output: bool) -> Command {
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
.hide_short_help(true) .hide_short_help(true)
.help("Show acknowledgements."), .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, // Check if the current directory contains a file name cache. Otherwise,

View File

@ -222,16 +222,37 @@ pub fn list_themes(cfg: &Config, config_dir: &Path, cache_dir: &Path) -> Result<
)?; )?;
} else { } else {
for theme in assets.themes() { for theme in assets.themes() {
writeln!(stdout, "{}", theme)?; writeln!(stdout, "{theme}")?;
} }
} }
Ok(()) 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> { 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 assets = assets_from_cache_or_binary(config.use_custom_assets, cache_dir)?;
let controller = Controller::new(config, &assets); 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) controller.run(inputs, None)
} }

View File

@ -94,6 +94,12 @@ pub struct Config<'a> {
// Whether or not to use $LESSOPEN if set // Whether or not to use $LESSOPEN if set
#[cfg(feature = "lessopen")] #[cfg(feature = "lessopen")]
pub use_lessopen: bool, pub use_lessopen: bool,
// Weather or not to set terminal title when using a pager
pub set_terminal_title: bool,
/// The maximum number of consecutive empty lines to display
pub squeeze_lines: Option<usize>,
} }
#[cfg(all(feature = "minimal-application", feature = "paging"))] #[cfg(all(feature = "minimal-application", feature = "paging"))]

View File

@ -56,7 +56,7 @@ impl Decoration for LineNumberDecoration {
self.cached_wrap.clone() self.cached_wrap.clone()
} else { } else {
let plain: String = format!("{:4}", line_number); let plain: String = format!("{line_number:4}");
DecorationText { DecorationText {
width: plain.len(), width: plain.len(),
text: self.color.paint(plain).to_string(), text: self.color.paint(plain).to_string(),

View File

@ -197,7 +197,7 @@ impl<'a> Input<'a> {
InputKind::StdIn => { InputKind::StdIn => {
if let Some(stdout) = stdout_identifier { if let Some(stdout) = stdout_identifier {
let input_identifier = Identifier::try_from(clircle::Stdio::Stdin) let input_identifier = Identifier::try_from(clircle::Stdio::Stdin)
.map_err(|e| format!("Stdin: Error identifying file: {}", e))?; .map_err(|e| format!("Stdin: Error identifying file: {e}"))?;
if stdout.surely_conflicts_with(&input_identifier) { if stdout.surely_conflicts_with(&input_identifier) {
return Err("IO circle detected. The input from stdin is also an output. Aborting to avoid infinite loop.".into()); return Err("IO circle detected. The input from stdin is also an output. Aborting to avoid infinite loop.".into());
} }

View File

@ -230,6 +230,12 @@ impl<'a> PrettyPrinter<'a> {
self self
} }
/// Specify the maximum number of consecutive empty lines to print.
pub fn squeeze_empty_lines(&mut self, maximum: Option<usize>) -> &mut Self {
self.config.squeeze_lines = maximum;
self
}
/// Specify the highlighting theme /// Specify the highlighting theme
pub fn theme(&mut self, theme: impl AsRef<str>) -> &mut Self { pub fn theme(&mut self, theme: impl AsRef<str>) -> &mut Self {
self.config.theme = theme.as_ref().to_owned(); self.config.theme = theme.as_ref().to_owned();

View File

@ -9,6 +9,7 @@ use bytesize::ByteSize;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::Color; use syntect::highlighting::Color;
use syntect::highlighting::FontStyle;
use syntect::highlighting::Theme; use syntect::highlighting::Theme;
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
@ -48,6 +49,22 @@ const ANSI_UNDERLINE_DISABLE: EscapeSequence = EscapeSequence::CSI {
final_byte: "m", final_byte: "m",
}; };
const EMPTY_SYNTECT_STYLE: syntect::highlighting::Style = syntect::highlighting::Style {
foreground: Color {
r: 127,
g: 127,
b: 127,
a: 255,
},
background: Color {
r: 127,
g: 127,
b: 127,
a: 255,
},
font_style: FontStyle::empty(),
};
pub enum OutputHandle<'a> { pub enum OutputHandle<'a> {
IoWrite(&'a mut dyn io::Write), IoWrite(&'a mut dyn io::Write),
FmtWrite(&'a mut dyn fmt::Write), FmtWrite(&'a mut dyn fmt::Write),
@ -84,11 +101,15 @@ pub(crate) trait Printer {
pub struct SimplePrinter<'a> { pub struct SimplePrinter<'a> {
config: &'a Config<'a>, config: &'a Config<'a>,
consecutive_empty_lines: usize,
} }
impl<'a> SimplePrinter<'a> { impl<'a> SimplePrinter<'a> {
pub fn new(config: &'a Config) -> Self { pub fn new(config: &'a Config) -> Self {
SimplePrinter { config } SimplePrinter {
config,
consecutive_empty_lines: 0,
}
} }
} }
@ -117,6 +138,21 @@ impl<'a> Printer for SimplePrinter<'a> {
_line_number: usize, _line_number: usize,
line_buffer: &[u8], line_buffer: &[u8],
) -> Result<()> { ) -> Result<()> {
// Skip squeezed lines.
if let Some(squeeze_limit) = self.config.squeeze_lines {
if String::from_utf8_lossy(line_buffer)
.trim_end_matches(|c| c == '\r' || c == '\n')
.is_empty()
{
self.consecutive_empty_lines += 1;
if self.consecutive_empty_lines > squeeze_limit {
return Ok(());
}
} else {
self.consecutive_empty_lines = 0;
}
}
if !out_of_range { if !out_of_range {
if self.config.show_nonprintable { if self.config.show_nonprintable {
let line = replace_nonprintable( let line = replace_nonprintable(
@ -124,7 +160,7 @@ impl<'a> Printer for SimplePrinter<'a> {
self.config.tab_width, self.config.tab_width,
self.config.nonprintable_notation, self.config.nonprintable_notation,
); );
write!(handle, "{}", line)?; write!(handle, "{line}")?;
} else { } else {
match handle { match handle {
OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?, OutputHandle::IoWrite(handle) => handle.write_all(line_buffer)?,
@ -170,6 +206,7 @@ pub(crate) struct InteractivePrinter<'a> {
pub line_changes: &'a Option<LineChanges>, pub line_changes: &'a Option<LineChanges>,
highlighter_from_set: Option<HighlighterFromSet<'a>>, highlighter_from_set: Option<HighlighterFromSet<'a>>,
background_color_highlight: Option<Color>, background_color_highlight: Option<Color>,
consecutive_empty_lines: usize,
} }
impl<'a> InteractivePrinter<'a> { impl<'a> InteractivePrinter<'a> {
@ -222,11 +259,13 @@ impl<'a> InteractivePrinter<'a> {
panel_width = 0; panel_width = 0;
} }
let highlighter_from_set = if input // Get the highlighter for the output.
let is_printing_binary = input
.reader .reader
.content_type .content_type
.map_or(false, |c| c.is_binary() && !config.show_nonprintable) .map_or(false, |c| c.is_binary() && !config.show_nonprintable);
{
let highlighter_from_set = if is_printing_binary || config.colored_output == false {
None None
} else { } else {
// Determine the type of syntax for highlighting // Determine the type of syntax for highlighting
@ -253,6 +292,7 @@ impl<'a> InteractivePrinter<'a> {
line_changes, line_changes,
highlighter_from_set, highlighter_from_set,
background_color_highlight, background_color_highlight,
consecutive_empty_lines: 0,
}) })
} }
@ -293,7 +333,7 @@ impl<'a> InteractivePrinter<'a> {
" ".repeat(self.panel_width - 1 - text_truncated.len()) " ".repeat(self.panel_width - 1 - text_truncated.len())
); );
if self.config.style_components.grid() { if self.config.style_components.grid() {
format!("{}", text_filled) format!("{text_filled}")
} else { } else {
text_filled text_filled
} }
@ -328,7 +368,7 @@ impl<'a> InteractivePrinter<'a> {
content: &str, content: &str,
) -> Result<()> { ) -> Result<()> {
self.print_header_component_indent(handle)?; self.print_header_component_indent(handle)?;
writeln!(handle, "{}", content) writeln!(handle, "{content}")
} }
fn print_header_multiline_component( fn print_header_multiline_component(
@ -346,6 +386,31 @@ impl<'a> InteractivePrinter<'a> {
self.print_header_component_with_indent(handle, content) self.print_header_component_with_indent(handle, content)
} }
fn highlight_regions_for_line<'b>(
&mut self,
line: &'b str,
) -> Result<Vec<(syntect::highlighting::Style, &'b str)>> {
let highlighter_from_set = match self.highlighter_from_set {
Some(ref mut highlighter_from_set) => highlighter_from_set,
_ => return Ok(vec![(EMPTY_SYNTECT_STYLE, line)]),
};
// skip syntax highlighting on long lines
let too_long = line.len() > 1024 * 16;
let for_highlighting: &str = if too_long { "\n" } else { &line };
let mut highlighted_line = highlighter_from_set
.highlighter
.highlight_line(for_highlighting, highlighter_from_set.syntax_set)?;
if too_long {
highlighted_line[0].1 = &line;
}
Ok(highlighted_line)
}
fn preprocess(&self, text: &str, cursor: &mut usize) -> String { fn preprocess(&self, text: &str, cursor: &mut usize) -> String {
if self.config.tab_width > 0 { if self.config.tab_width > 0 {
return expand_tabs(text, self.config.tab_width, cursor); return expand_tabs(text, self.config.tab_width, cursor);
@ -429,7 +494,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
"{}{}{}", "{}{}{}",
description description
.kind() .kind()
.map(|kind| format!("{}: ", kind)) .map(|kind| format!("{kind}: "))
.unwrap_or_else(|| "".into()), .unwrap_or_else(|| "".into()),
self.colors.header_value.paint(description.title()), self.colors.header_value.paint(description.title()),
mode mode
@ -487,7 +552,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
"{}", "{}",
self.colors self.colors
.grid .grid
.paint(format!("{}{}{}{}", panel, snip_left, title, snip_right)) .paint(format!("{panel}{snip_left}{title}{snip_right}"))
)?; )?;
Ok(()) Ok(())
@ -528,34 +593,23 @@ impl<'a> Printer for InteractivePrinter<'a> {
} }
}; };
let regions = { let regions = self.highlight_regions_for_line(&line)?;
let highlighter_from_set = match self.highlighter_from_set {
Some(ref mut highlighter_from_set) => highlighter_from_set,
_ => {
return Ok(());
}
};
// skip syntax highlighting on long lines
let too_long = line.len() > 1024 * 16;
let for_highlighting: &str = if too_long { "\n" } else { &line };
let mut highlighted_line = highlighter_from_set
.highlighter
.highlight_line(for_highlighting, highlighter_from_set.syntax_set)?;
if too_long {
highlighted_line[0].1 = &line;
}
highlighted_line
};
if out_of_range { if out_of_range {
return Ok(()); return Ok(());
} }
// Skip squeezed lines.
if let Some(squeeze_limit) = self.config.squeeze_lines {
if line.trim_end_matches(|c| c == '\r' || c == '\n').is_empty() {
self.consecutive_empty_lines += 1;
if self.consecutive_empty_lines > squeeze_limit {
return Ok(());
}
} else {
self.consecutive_empty_lines = 0;
}
}
let mut cursor: usize = 0; let mut cursor: usize = 0;
let mut cursor_max: usize = self.config.term_width; let mut cursor_max: usize = self.config.term_width;
let mut cursor_total: usize = 0; let mut cursor_total: usize = 0;

View File

@ -80,7 +80,7 @@ impl FromStr for StyleComponent {
"full" => Ok(StyleComponent::Full), "full" => Ok(StyleComponent::Full),
"default" => Ok(StyleComponent::Default), "default" => Ok(StyleComponent::Default),
"plain" => Ok(StyleComponent::Plain), "plain" => Ok(StyleComponent::Plain),
_ => Err(format!("Unknown style '{}'", s).into()), _ => Err(format!("Unknown style '{s}'").into()),
} }
} }
} }

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 # JSON Lines is a simple variation of JSON #2535
[mappings] [mappings]
"JSON" = ["*.jsonl"] "JSON" = ["*.jsonl", "*.jsonc"]

View File

@ -0,0 +1,3 @@
# Xonsh shell (https://xon.sh/)
[mappings]
"Python" = ["*.xsh", "*.xonshrc"]

View File

@ -0,0 +1,8 @@
# see https://github.com/containers/image/tree/main/docs
[mappings]
"TOML" = [
"/usr/share/containers/**/*.conf",
"/etc/containers/**/*.conf",
"${HOME}/.config/containers/**/*.conf",
"${XDG_CONFIG_HOME}/containers/**/*.conf",
]

View File

@ -0,0 +1,7 @@
# see `man quadlet`
[mappings]
"INI" = [
"**/containers/systemd/*.{container,volume,network,kube,image}",
"**/containers/systemd/users/*.{container,volume,network,kube,image}",
"**/containers/systemd/users/*/*.{container,volume,network,kube,image}",
]

View File

@ -73,7 +73,7 @@ fn internal_suffixes() {
let file_names = ignored_suffixes let file_names = ignored_suffixes
.values .values
.iter() .iter()
.map(|suffix| format!("test.json{}", suffix)); .map(|suffix| format!("test.json{suffix}"));
for file_name_str in file_names { for file_name_str in file_names {
let file_name = OsStr::new(&file_name_str); let file_name = OsStr::new(&file_name_str);
let expected_stripped_file_name = OsStr::new("test.json"); let expected_stripped_file_name = OsStr::new("test.json");
@ -95,7 +95,7 @@ fn external_suffixes() {
let file_names = ignored_suffixes let file_names = ignored_suffixes
.values .values
.iter() .iter()
.map(|suffix| format!("test.json{}", suffix)); .map(|suffix| format!("test.json{suffix}"));
for file_name_str in file_names { for file_name_str in file_names {
let file_name = OsStr::new(&file_name_str); let file_name = OsStr::new(&file_name_str);
let expected_stripped_file_name = OsStr::new("test.json"); let expected_stripped_file_name = OsStr::new("test.json");

View File

@ -221,11 +221,11 @@ impl Attributes {
8 => match parameters.next() { 8 => match parameters.next() {
Some(5) /* 256-color */ => format!("\x1B[{};5;{}m", color, join(";", 1, parameters)), 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(2) /* 24-bit color */ => format!("\x1B[{};2;{}m", color, join(";", 3, parameters)),
Some(c) => format!("\x1B[{};{}m", color, c), Some(c) => format!("\x1B[{color};{c}m"),
_ => "".to_owned(), _ => "".to_owned(),
}, },
9 => "".to_owned(), 9 => "".to_owned(),
_ => format!("\x1B[{}m", color), _ => format!("\x1B[{color}m"),
} }
} }
@ -435,7 +435,7 @@ impl<'a> EscapeSequenceOffsetsIterator<'a> {
} }
Some((_, tc)) => { Some((_, tc)) => {
panic!("this should not be reached: char {:?}", tc) panic!("this should not be reached: char {tc:?}")
} }
} }
} }

30
tests/examples/empty_lines.txt vendored Normal file
View File

@ -0,0 +1,30 @@
line 1
line 5
line 20
line 21
line 24
line 26
line 30

View File

@ -208,6 +208,70 @@ fn line_range_multiple() {
.stdout("line 1\nline 2\nline 4\n"); .stdout("line 1\nline 2\nline 4\n");
} }
#[test]
fn squeeze_blank() {
bat()
.arg("empty_lines.txt")
.arg("--squeeze-blank")
.assert()
.success()
.stdout("line 1\n\nline 5\n\nline 20\nline 21\n\nline 24\n\nline 26\n\nline 30\n");
}
#[test]
fn squeeze_blank_line_numbers() {
bat()
.arg("empty_lines.txt")
.arg("--squeeze-blank")
.arg("--decorations=always")
.arg("--number")
.assert()
.success()
.stdout(" 1 line 1\n 2 \n 5 line 5\n 6 \n 20 line 20\n 21 line 21\n 22 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 30 line 30\n");
}
#[test]
fn squeeze_limit() {
bat()
.arg("empty_lines.txt")
.arg("--squeeze-blank")
.arg("--squeeze-limit=2")
.assert()
.success()
.stdout("line 1\n\n\nline 5\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\nline 30\n");
bat()
.arg("empty_lines.txt")
.arg("--squeeze-blank")
.arg("--squeeze-limit=5")
.assert()
.success()
.stdout("line 1\n\n\n\nline 5\n\n\n\n\n\nline 20\nline 21\n\n\nline 24\n\nline 26\n\n\n\nline 30\n");
}
#[test]
fn squeeze_limit_line_numbers() {
bat()
.arg("empty_lines.txt")
.arg("--squeeze-blank")
.arg("--squeeze-limit=2")
.arg("--decorations=always")
.arg("--number")
.assert()
.success()
.stdout(" 1 line 1\n 2 \n 3 \n 5 line 5\n 6 \n 7 \n 20 line 20\n 21 line 21\n 22 \n 23 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 28 \n 30 line 30\n");
bat()
.arg("empty_lines.txt")
.arg("--squeeze-blank")
.arg("--squeeze-limit=5")
.arg("--decorations=always")
.arg("--number")
.assert()
.success()
.stdout(" 1 line 1\n 2 \n 3 \n 4 \n 5 line 5\n 6 \n 7 \n 8 \n 9 \n 10 \n 20 line 20\n 21 line 21\n 22 \n 23 \n 24 line 24\n 25 \n 26 line 26\n 27 \n 28 \n 29 \n 30 line 30\n");
}
#[test] #[test]
#[cfg_attr(any(not(feature = "git"), target_os = "windows"), ignore)] #[cfg_attr(any(not(feature = "git"), target_os = "windows"), ignore)]
fn short_help() { fn short_help() {
@ -936,6 +1000,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] #[test]
fn diagnostic_sanity_check() { fn diagnostic_sanity_check() {
bat() bat()
@ -1682,7 +1758,7 @@ fn do_not_panic_regression_tests() {
] { ] {
bat() bat()
.arg("--color=always") .arg("--color=always")
.arg(&format!("regression_tests/{}", filename)) .arg(&format!("regression_tests/{filename}"))
.assert() .assert()
.success(); .success();
} }
@ -1695,7 +1771,7 @@ fn do_not_detect_different_syntax_for_stdin_and_files() {
let cmd_for_file = bat() let cmd_for_file = bat()
.arg("--color=always") .arg("--color=always")
.arg("--map-syntax=*.js:Markdown") .arg("--map-syntax=*.js:Markdown")
.arg(&format!("--file-name={}", file)) .arg(&format!("--file-name={file}"))
.arg("--style=plain") .arg("--style=plain")
.arg(file) .arg(file)
.assert() .assert()
@ -1705,7 +1781,7 @@ fn do_not_detect_different_syntax_for_stdin_and_files() {
.arg("--color=always") .arg("--color=always")
.arg("--map-syntax=*.js:Markdown") .arg("--map-syntax=*.js:Markdown")
.arg("--style=plain") .arg("--style=plain")
.arg(&format!("--file-name={}", file)) .arg(&format!("--file-name={file}"))
.pipe_stdin(Path::new(EXAMPLES_DIR).join(file)) .pipe_stdin(Path::new(EXAMPLES_DIR).join(file))
.unwrap() .unwrap()
.assert() .assert()
@ -1724,7 +1800,7 @@ fn no_first_line_fallback_when_mapping_to_invalid_syntax() {
bat() bat()
.arg("--color=always") .arg("--color=always")
.arg("--map-syntax=*.invalid-syntax:InvalidSyntax") .arg("--map-syntax=*.invalid-syntax:InvalidSyntax")
.arg(&format!("--file-name={}", file)) .arg(&format!("--file-name={file}"))
.arg("--style=plain") .arg("--style=plain")
.arg(file) .arg(file)
.assert() .assert()
@ -1922,7 +1998,7 @@ fn ansi_passthrough_emit() {
.arg("--paging=never") .arg("--paging=never")
.arg("--color=never") .arg("--color=never")
.arg("--terminal-width=80") .arg("--terminal-width=80")
.arg(format!("--wrap={}", wrapping)) .arg(format!("--wrap={wrapping}"))
.arg("--decorations=always") .arg("--decorations=always")
.arg("--style=plain") .arg("--style=plain")
.write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n") .write_stdin("\x1B[33mColor\nColor \x1B[m\nPlain\n")

View File

@ -30,8 +30,7 @@ fn no_duplicate_extensions() {
for extension in &syntax.file_extensions { for extension in &syntax.file_extensions {
assert!( assert!(
KNOWN_EXCEPTIONS.contains(&extension.as_str()) || extensions.insert(extension), KNOWN_EXCEPTIONS.contains(&extension.as_str()) || extensions.insert(extension),
"File extension / pattern \"{}\" appears twice in the syntax set", "File extension / pattern \"{extension}\" appears twice in the syntax set"
extension
); );
} }
} }

View File

@ -29,7 +29,7 @@ impl BatTester {
"--color=never", "--color=never",
"--decorations=always", "--decorations=always",
"--terminal-width=80", "--terminal-width=80",
&format!("--style={}", style), &format!("--style={style}"),
]) ])
.output() .output()
.expect("bat failed"); .expect("bat failed");
@ -40,7 +40,7 @@ impl BatTester {
.replace("tests/snapshots/", ""); .replace("tests/snapshots/", "");
let mut expected = String::new(); let mut expected = String::new();
let mut file = File::open(format!("tests/snapshots/output/{}.snapshot.txt", name)) let mut file = File::open(format!("tests/snapshots/output/{name}.snapshot.txt"))
.expect("snapshot file missing"); .expect("snapshot file missing");
file.read_to_string(&mut expected) file.read_to_string(&mut expected)
.expect("could not read snapshot file"); .expect("could not read snapshot file");