mirror of
https://github.com/sharkdp/bat
synced 2026-07-31 18:21:43 +00:00
Merge branch 'master' into makefile-syntax-for-justfiles
This commit is contained in:
+11
-1
@@ -590,7 +590,17 @@ impl App {
|
||||
|
||||
// Plain if `--plain` is specified at least once.
|
||||
if self.matches.get_count("plain") > 0 {
|
||||
return Some(StyleComponents(HashSet::from([StyleComponent::Plain])));
|
||||
let mut components = HashSet::from([StyleComponent::Plain]);
|
||||
// When --diff is active, preserve change markers and snip separators
|
||||
// so that diff output remains visually useful.
|
||||
if self.matches.try_contains_id("diff").unwrap_or_default()
|
||||
&& self.matches.get_flag("diff")
|
||||
{
|
||||
#[cfg(feature = "git")]
|
||||
components.insert(StyleComponent::Changes);
|
||||
components.insert(StyleComponent::Snip);
|
||||
}
|
||||
return Some(StyleComponents(components));
|
||||
}
|
||||
|
||||
// Default behavior.
|
||||
|
||||
@@ -240,6 +240,7 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
.arg(
|
||||
Arg::new("terminal-width")
|
||||
.long("terminal-width")
|
||||
.overrides_with("terminal-width")
|
||||
.value_name("width")
|
||||
.hide_short_help(true)
|
||||
.allow_hyphen_values(true)
|
||||
@@ -255,10 +256,13 @@ pub fn build_app(interactive_output: bool) -> Command {
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.help(
|
||||
.help("Explicitly set the width of the terminal instead of determining it automatically.")
|
||||
.long_help(
|
||||
"Explicitly set the width of the terminal instead of determining it \
|
||||
automatically. If prefixed with '+' or '-', the value will be treated \
|
||||
as an offset to the actual terminal width. See also: '--wrap'.",
|
||||
as an offset to the actual terminal width. This can also be configured \
|
||||
via the BAT_WIDTH environment variable (e.g. export BAT_WIDTH=\"100\"). \
|
||||
See also: '--wrap'.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
|
||||
@@ -153,6 +153,7 @@ fn get_args_from_str(content: &str) -> Result<Vec<OsString>, shell_words::ParseE
|
||||
pub fn get_args_from_env_vars() -> Vec<OsString> {
|
||||
[
|
||||
("--tabs", "BAT_TABS"),
|
||||
("--terminal-width", "BAT_WIDTH"),
|
||||
("--theme", bat::theme::env::BAT_THEME),
|
||||
("--theme-dark", bat::theme::env::BAT_THEME_DARK),
|
||||
("--theme-light", bat::theme::env::BAT_THEME_LIGHT),
|
||||
|
||||
+26
-1
@@ -285,7 +285,28 @@ fn run_controller(inputs: Vec<Input>, config: &Config, cache_dir: &Path) -> Resu
|
||||
|
||||
#[cfg(feature = "bugreport")]
|
||||
fn invoke_bugreport(app: &App, cache_dir: &Path) {
|
||||
use bugreport::{bugreport, collector::*, format::Markdown};
|
||||
use bugreport::{bugreport, collector::*, format::Markdown, report::ReportEntry};
|
||||
|
||||
struct ColorSchemeCollector;
|
||||
|
||||
impl Collector for ColorSchemeCollector {
|
||||
fn description(&self) -> &str {
|
||||
"Detected terminal color scheme"
|
||||
}
|
||||
|
||||
fn collect(
|
||||
&mut self,
|
||||
_: &bugreport::CrateInfo,
|
||||
) -> std::result::Result<ReportEntry, CollectionError> {
|
||||
let color_scheme = bat::theme::color_scheme(bat::theme::DetectColorScheme::Always);
|
||||
let text = match color_scheme {
|
||||
Some(bat::theme::ColorScheme::Dark) => "dark",
|
||||
Some(bat::theme::ColorScheme::Light) => "light",
|
||||
None => "not detected",
|
||||
};
|
||||
Ok(ReportEntry::Text(text.to_string()))
|
||||
}
|
||||
}
|
||||
let pager = bat::config::get_pager_executable(
|
||||
app.matches.get_one::<String>("pager").map(|s| s.as_str()),
|
||||
)
|
||||
@@ -307,6 +328,9 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
|
||||
"BAT_STYLE",
|
||||
"BAT_TABS",
|
||||
"BAT_THEME",
|
||||
"BAT_WIDTH",
|
||||
bat::theme::env::BAT_THEME_DARK,
|
||||
bat::theme::env::BAT_THEME_LIGHT,
|
||||
"COLORTERM",
|
||||
"LANG",
|
||||
"LC_ALL",
|
||||
@@ -326,6 +350,7 @@ fn invoke_bugreport(app: &App, cache_dir: &Path) {
|
||||
custom_assets_metadata,
|
||||
))
|
||||
.info(DirectoryEntries::new("Custom assets", cache_dir))
|
||||
.info(ColorSchemeCollector)
|
||||
.info(CompileTimeInformation::default());
|
||||
|
||||
#[cfg(feature = "paging")]
|
||||
|
||||
+72
-15
@@ -207,7 +207,7 @@ impl<'a> Input<'a> {
|
||||
kind: OpenedInputKind::StdIn,
|
||||
description,
|
||||
metadata: self.metadata,
|
||||
reader: InputReader::new(stdin),
|
||||
reader: InputReader::try_new(stdin)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -236,14 +236,14 @@ impl<'a> Input<'a> {
|
||||
file = input_identifier.into_inner().expect("The file was lost in the clircle::Identifier, this should not have happened...");
|
||||
}
|
||||
|
||||
InputReader::new(BufReader::new(file))
|
||||
InputReader::try_new(BufReader::new(file))?
|
||||
},
|
||||
}),
|
||||
InputKind::CustomReader(reader) => Ok(OpenedInput {
|
||||
description,
|
||||
kind: OpenedInputKind::CustomReader,
|
||||
metadata: self.metadata,
|
||||
reader: InputReader::new(BufReader::new(reader)),
|
||||
reader: InputReader::try_new(BufReader::new(reader))?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -257,28 +257,29 @@ pub(crate) struct InputReader<'a> {
|
||||
}
|
||||
|
||||
impl<'a> InputReader<'a> {
|
||||
pub(crate) fn new<R: BufRead + 'a>(mut reader: R) -> InputReader<'a> {
|
||||
let mut first_line = vec![];
|
||||
reader.read_until(b'\n', &mut first_line).ok();
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new<R: BufRead + 'a>(reader: R) -> InputReader<'a> {
|
||||
Self::try_new(reader).expect("reading the first line failed")
|
||||
}
|
||||
|
||||
let content_type = if first_line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(content_inspector::inspect(&first_line[..]))
|
||||
};
|
||||
pub(crate) fn try_new<R: BufRead + 'a>(mut reader: R) -> io::Result<InputReader<'a>> {
|
||||
let mut first_line = vec![];
|
||||
reader.read_until(b'\n', &mut first_line)?;
|
||||
|
||||
let content_type = inspect_content_type(&first_line);
|
||||
|
||||
if content_type == Some(ContentType::UTF_16LE) {
|
||||
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A).ok();
|
||||
read_utf16_line(&mut reader, &mut first_line, 0x00, 0x0A)?;
|
||||
} else if content_type == Some(ContentType::UTF_16BE) {
|
||||
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00).ok();
|
||||
read_utf16_line(&mut reader, &mut first_line, 0x0A, 0x00)?;
|
||||
}
|
||||
|
||||
InputReader {
|
||||
Ok(InputReader {
|
||||
inner: Box::new(reader),
|
||||
first_line,
|
||||
content_type,
|
||||
unbuffered: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
|
||||
@@ -319,6 +320,25 @@ impl<'a> InputReader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_content_type(first_line: &[u8]) -> Option<ContentType> {
|
||||
if first_line.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let content_type = content_inspector::inspect(first_line);
|
||||
if content_type == ContentType::UTF_8 && has_zip_signature(first_line) {
|
||||
Some(ContentType::BINARY)
|
||||
} else {
|
||||
Some(content_type)
|
||||
}
|
||||
}
|
||||
|
||||
fn has_zip_signature(bytes: &[u8]) -> bool {
|
||||
[b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08"]
|
||||
.into_iter()
|
||||
.any(|signature| bytes.starts_with(signature))
|
||||
}
|
||||
|
||||
fn read_utf16_line<R: BufRead>(
|
||||
reader: &mut R,
|
||||
buf: &mut Vec<u8>,
|
||||
@@ -374,6 +394,43 @@ fn basic() {
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_magic_headers_are_treated_as_binary() {
|
||||
for content in [b"PK\x03\x04hello", b"PK\x05\x06hello", b"PK\x07\x08hello"] {
|
||||
let reader = InputReader::new(&content[..]);
|
||||
assert_eq!(Some(ContentType::BINARY), reader.content_type);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_zip_pk_prefix_is_not_treated_as_binary() {
|
||||
assert_eq!(
|
||||
Some(ContentType::UTF_8),
|
||||
inspect_content_type(b"PK\x03\x03hello")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_open_returns_initial_read_errors() {
|
||||
struct FailingRead;
|
||||
|
||||
impl Read for FailingRead {
|
||||
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
|
||||
Err(io::Error::other("initial read failed"))
|
||||
}
|
||||
}
|
||||
|
||||
let input = Input::from_reader(Box::new(FailingRead));
|
||||
let result = input.open(io::empty(), None);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("initial read failed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16le() {
|
||||
let content = b"\xFF\xFE\x73\x00\x0A\x00\x64\x00";
|
||||
|
||||
+2
-2
@@ -155,7 +155,7 @@ impl LessOpenPreprocessor {
|
||||
|
||||
Ok(OpenedInput {
|
||||
kind,
|
||||
reader: InputReader::new(BufReader::new(
|
||||
reader: InputReader::try_new(BufReader::new(
|
||||
if matches!(self.kind, LessOpenKind::TempFile) {
|
||||
let lessopen_string = match String::from_utf8(lessopen_stdout) {
|
||||
Ok(string) => string,
|
||||
@@ -192,7 +192,7 @@ impl LessOpenPreprocessor {
|
||||
.map(|s| s.replacen("%s", &path_str, 1).replacen("%s", "-", 1)),
|
||||
}
|
||||
},
|
||||
)),
|
||||
))?,
|
||||
metadata: input.metadata,
|
||||
description: input.description,
|
||||
})
|
||||
|
||||
@@ -23,6 +23,18 @@ pub struct BuiltinPager {
|
||||
impl BuiltinPager {
|
||||
fn new() -> Self {
|
||||
let pager = minus::Pager::new();
|
||||
|
||||
let mut input_register = minus::input::HashedEventRegister::default();
|
||||
input_register.add_key_events(&["home"], |_, _| {
|
||||
minus::input::InputEvent::UpdateUpperMark(0)
|
||||
});
|
||||
input_register.add_key_events(&["end"], |_, _| {
|
||||
minus::input::InputEvent::UpdateUpperMark(usize::MAX)
|
||||
});
|
||||
pager
|
||||
.set_input_classifier(Box::new(input_register))
|
||||
.expect("failed to set input classifier on newly created pager");
|
||||
|
||||
let handle = {
|
||||
let pager = pager.clone();
|
||||
Some(spawn(move || {
|
||||
|
||||
+13
-5
@@ -37,6 +37,16 @@ use crate::wrapping::WrappingMode;
|
||||
use crate::BinaryBehavior;
|
||||
use crate::StripAnsiMode;
|
||||
|
||||
// Return the displayed width of a character.
|
||||
//
|
||||
// Control characters (0x00..=0x1F and 0x7F) are rendered by the terminal
|
||||
// in caret notation (e.g. ^@, ^A, ..., ^?), which occupies two columns.
|
||||
// UnicodeWidthChar::width() returns None for these, so we map them to 2
|
||||
// here instead of the previous default of 0.
|
||||
fn char_width(c: char) -> usize {
|
||||
c.width().unwrap_or(if c.is_control() { 2 } else { 0 })
|
||||
}
|
||||
|
||||
const ANSI_UNDERLINE_ENABLE: EscapeSequence = EscapeSequence::CSI {
|
||||
raw_sequence: "\x1B[4m",
|
||||
parameters: "4",
|
||||
@@ -793,7 +803,7 @@ impl Printer for InteractivePrinter<'_> {
|
||||
|
||||
for c in text.chars() {
|
||||
// calculate the displayed width for next character
|
||||
let cw = c.width().unwrap_or(0);
|
||||
let cw = char_width(c);
|
||||
current_width += cw;
|
||||
|
||||
// Track whitespace positions for word wrapping.
|
||||
@@ -868,10 +878,8 @@ impl Printer for InteractivePrinter<'_> {
|
||||
if let Some(rs) = rest_start {
|
||||
// Word wrap: carry remainder to next line.
|
||||
let remainder = line_buf[rs..].to_string();
|
||||
let rem_width: usize = remainder
|
||||
.chars()
|
||||
.map(|ch| ch.width().unwrap_or(0))
|
||||
.sum();
|
||||
let rem_width: usize =
|
||||
remainder.chars().map(char_width).sum();
|
||||
line_buf.clear();
|
||||
line_buf.push_str(&remainder);
|
||||
current_width = rem_width + cw;
|
||||
|
||||
+54
-3
@@ -17,9 +17,16 @@ use ignored_suffixes::IgnoredSuffixes;
|
||||
mod builtin;
|
||||
pub mod ignored_suffixes;
|
||||
|
||||
fn make_glob_matcher(from: &str) -> Result<GlobMatcher> {
|
||||
/// Whether a glob pattern should be matched case-sensitively or case-insensitively.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Case {
|
||||
Sensitive,
|
||||
Insensitive,
|
||||
}
|
||||
|
||||
fn make_glob_matcher(from: &str, case: Case) -> Result<GlobMatcher> {
|
||||
let matcher = GlobBuilder::new(from)
|
||||
.case_insensitive(true)
|
||||
.case_insensitive(matches!(case, Case::Insensitive))
|
||||
.literal_separator(true)
|
||||
.build()?
|
||||
.compile_matcher();
|
||||
@@ -97,7 +104,14 @@ impl<'a> SyntaxMapping<'a> {
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
|
||||
let matcher = make_glob_matcher(from)?;
|
||||
let matcher = make_glob_matcher(from, Case::Insensitive)?;
|
||||
self.custom_mappings.push((matcher, to));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Like [`Self::insert`], but the glob pattern is matched case-sensitively.
|
||||
pub fn insert_case_sensitive(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
|
||||
let matcher = make_glob_matcher(from, Case::Sensitive)?;
|
||||
self.custom_mappings.push((matcher, to));
|
||||
Ok(())
|
||||
}
|
||||
@@ -261,4 +275,41 @@ mod tests {
|
||||
Some(MappingTarget::MapTo("alpha"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_sensitive_custom_mappings_work() {
|
||||
let mut map = SyntaxMapping::new();
|
||||
map.insert_case_sensitive("MY_SPECIAL_FILE", MappingTarget::MapTo("Python"))
|
||||
.ok();
|
||||
|
||||
// Exact case matches
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/path/to/MY_SPECIAL_FILE"),
|
||||
Some(MappingTarget::MapTo("Python"))
|
||||
);
|
||||
// Different case should NOT match the case-sensitive rule
|
||||
assert_eq!(map.get_syntax_for("/path/to/my_special_file"), None);
|
||||
assert_eq!(map.get_syntax_for("/path/to/My_Special_File"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_mappings_build_is_case_sensitive() {
|
||||
let map = SyntaxMapping::new();
|
||||
|
||||
// "BUILD" (uppercase) should map to Python via case-sensitive builtin
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/path/to/BUILD"),
|
||||
Some(MappingTarget::MapTo("Python"))
|
||||
);
|
||||
// "build" (lowercase) should still map to MapToUnknown
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/path/to/build"),
|
||||
Some(MappingTarget::MapToUnknown)
|
||||
);
|
||||
// Mixed case should NOT match the Python rule
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/path/to/Build"),
|
||||
Some(MappingTarget::MapToUnknown)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::env;
|
||||
use globset::GlobMatcher;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::syntax_mapping::{make_glob_matcher, MappingTarget};
|
||||
use crate::syntax_mapping::{make_glob_matcher, Case, MappingTarget};
|
||||
|
||||
// Static syntax mappings generated from /src/syntax_mapping/builtins/ by the
|
||||
// build script (/build/syntax_mapping.rs).
|
||||
@@ -53,8 +53,8 @@ include!(concat!(
|
||||
/// A failure to compile is a fatal error.
|
||||
///
|
||||
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
|
||||
fn build_matcher_fixed(from: &str) -> GlobMatcher {
|
||||
make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile")
|
||||
fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher {
|
||||
make_glob_matcher(from, case).expect("A builtin fixed glob matcher failed to compile")
|
||||
}
|
||||
|
||||
/// Join a list of matcher segments to create a glob string, replacing all
|
||||
@@ -64,7 +64,7 @@ fn build_matcher_fixed(from: &str) -> GlobMatcher {
|
||||
/// to compile.
|
||||
///
|
||||
/// Used internally by `Lazy<Option<GlobMatcher>>`'s lazy evaluation closure.
|
||||
fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> {
|
||||
fn build_matcher_dynamic(segs: &[MatcherSegment], case: Case) -> Option<GlobMatcher> {
|
||||
// join segments
|
||||
let mut buf = String::new();
|
||||
for seg in segs {
|
||||
@@ -77,7 +77,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option<GlobMatcher> {
|
||||
}
|
||||
}
|
||||
// compile glob matcher
|
||||
let matcher = make_glob_matcher(&buf).ok()?;
|
||||
let matcher = make_glob_matcher(&buf, case).ok()?;
|
||||
Some(matcher)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,10 @@ syntax mappings defined by all TOML files, and embed them into the binary.
|
||||
|
||||
## File syntax
|
||||
|
||||
Each TOML file should contain a single section named `mappings`, with each of
|
||||
its keys being a language identifier (first column of `bat -L`; also referred to
|
||||
as "target").
|
||||
Each TOML file should contain a single section named `mappings`, with each of its keys being a language
|
||||
identifier (first column of `bat -L`; also referred to as "target").
|
||||
|
||||
The value of each key should be an array of strings, with each item being a glob
|
||||
matcher. We will call each of these items a "rule".
|
||||
The value of each key should be an array of "rules". The rules are expected to be objects with a `glob` string and a `case_sensitive` boolean. For simplification, a rule can be just a glob string, which is shorthand for the default case insensitive mode.
|
||||
|
||||
For example, if `foo-application` uses both TOML and YAML configuration files,
|
||||
we could write something like this:
|
||||
@@ -98,6 +96,15 @@ like this:
|
||||
]
|
||||
```
|
||||
|
||||
### Case sensitivity
|
||||
|
||||
By default, all glob patterns are matched case-insensitively. To match a pattern case-sensitively, use the object form of the rule with the `case_sensitive` option:
|
||||
|
||||
```toml
|
||||
[mappings]
|
||||
"Python" = [{ glob = "BUILD", case_sensitive = true }]
|
||||
```
|
||||
|
||||
## Ordering
|
||||
|
||||
At compile time, all TOML files applicable to the target are processed in
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[mappings]
|
||||
"Python" = [{ glob = "BUILD", case_sensitive = true }]
|
||||
@@ -1,2 +1,2 @@
|
||||
[mappings]
|
||||
"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets"]
|
||||
"XML" = ["*.csproj", "*.vbproj", "*.props", "*.targets", "*.slnx"]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[mappings]
|
||||
"INI" = [".boto", "**/gcloud/configurations/config_*"]
|
||||
"Git Ignore" = [".gcloudignore"]
|
||||
@@ -0,0 +1,2 @@
|
||||
[mappings]
|
||||
"Git Ignore" = [".?*ignore"]
|
||||
Reference in New Issue
Block a user