From de79acb32e28aafa22ba2f61245e47c11b9fa6be Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Sat, 25 Dec 2021 00:53:35 +0000 Subject: [PATCH] Add --header-info option to show more information about file in header Fixes #1701 --- src/bin/bat/app.rs | 37 +++++++++++++++++ src/bin/bat/clap_app.rs | 27 +++++++++++++ src/config.rs | 4 ++ src/header.rs | 89 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/printer.rs | 63 ++++++++++++++++++++--------- 6 files changed, 202 insertions(+), 19 deletions(-) create mode 100644 src/header.rs diff --git a/src/bin/bat/app.rs b/src/bin/bat/app.rs index 0b17abce0e..b4b99b00dc 100644 --- a/src/bin/bat/app.rs +++ b/src/bin/bat/app.rs @@ -19,6 +19,7 @@ use bat::{ bat_warning, config::{Config, VisibleLines}, error::*, + header::{HeaderComponent, HeaderComponents}, input::Input, line_range::{HighlightedLineRanges, LineRange, LineRanges}, style::{StyleComponent, StyleComponents}, @@ -78,6 +79,7 @@ impl App { pub fn config(&self, inputs: &[Input]) -> Result { let style_components = self.style_components()?; + let header_components = self.header_components()?; let paging_mode = match self.matches.value_of("paging") { Some("always") => PagingMode::Always, @@ -229,6 +231,7 @@ impl App { ), }, style_components, + header_components, syntax_mapping, pager: self.matches.value_of("pager"), use_italic_text: self.matches.value_of("italic-text") == Some("always"), @@ -338,4 +341,38 @@ impl App { Ok(styled_components) } + + fn header_components(&self) -> Result { + let matches = &self.matches; + let header_components = HeaderComponents({ + let env_header_components: Option> = env::var("BAT_HEADER_INFO") + .ok() + .map(|header_str| { + header_str + .split(',') + .map(HeaderComponent::from_str) + .collect::>>() + }) + .transpose()?; + + matches + .values_of("header-info") + .map(|header| { + header + .map(|header| header.parse::()) + .filter_map(|header| header.ok()) + .collect::>() + }) + .or(env_header_components) + .unwrap_or_else(|| vec![HeaderComponent::Full]) + .into_iter() + .map(|header| header.components()) + .fold(HashSet::new(), |mut acc, components| { + acc.extend(components.iter().cloned()); + acc + }) + }); + + Ok(header_components) + } } diff --git a/src/bin/bat/clap_app.rs b/src/bin/bat/clap_app.rs index 94ddce55f0..7696bd8560 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -369,6 +369,33 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> { .help("Display all supported highlighting themes.") .long_help("Display a list of supported themes for syntax highlighting."), ) + .arg( + Arg::with_name("header-info") + .long("header-info") + .value_name("components") + .use_delimiter(true) + .takes_value(true) + .possible_values(&["full", "auto", "filename", "size", "last-modified", "permissions"]) + .help( + "Comma-separated list of header information elements to display \ + (full, filename, size, last-modified, permissions).", + ) + .long_help( + "Configure what information (filename, file size, last modification date, \ + permissions, ..) to display in the header.\ + The argument is a comma-separated list of \ + components to display (e.g. 'filename,size,last-modified') or all of them ('full'). \ + To set a default set of header information, add the \ + '--header-info=\"..\"' option to the configuration file or export the \ + BAT_HEADER_INFO environment variable (e.g.: export BAT_HEADER_INFO=\"..\").\n\n\ + Possible values:\n\n \ + * full: enables all available components (default).\n \ + * filename: displays the file name.\n \ + * size: displays the size of the file in human-readable format.\n \ + * last-modified: displays the last modification timestamp of the file.\n \ + * permissions: displays the file owner, group and mode.", + ), + ) .arg( Arg::with_name("style") .long("style") diff --git a/src/config.rs b/src/config.rs index 76eb3990cf..a1603f1445 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use crate::header::HeaderComponents; use crate::line_range::{HighlightedLineRanges, LineRanges}; #[cfg(feature = "paging")] use crate::paging::PagingMode; @@ -58,6 +59,9 @@ pub struct Config<'a> { /// Style elements (grid, line numbers, ...) pub style_components: StyleComponents, + /// Header elements (filename, size, ...) + pub header_components: HeaderComponents, + /// If and how text should be wrapped pub wrapping_mode: WrappingMode, diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000000..655d4b9854 --- /dev/null +++ b/src/header.rs @@ -0,0 +1,89 @@ +use std::collections::HashSet; +use std::fmt; +use std::str::FromStr; + +use crate::error::*; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] +pub enum HeaderComponent { + Filename, + Size, + Permissions, + LastModified, + Full, +} + +impl fmt::Display for HeaderComponent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write strictly the first element into the supplied output + // stream: `f`. Returns `fmt::Result` which indicates whether the + // operation succeeded or failed. Note that `write!` uses syntax which + // is very similar to `println!`. + let name = match self { + HeaderComponent::Filename => "filename", + HeaderComponent::Size => "size", + HeaderComponent::Permissions => "permissions", + HeaderComponent::LastModified => "last-modified", + HeaderComponent::Full => "full", + }; + + write!(f, "{}", name) + } +} + +impl HeaderComponent { + pub fn components(self) -> &'static [HeaderComponent] { + match self { + HeaderComponent::Filename => &[HeaderComponent::Filename], + HeaderComponent::Size => &[HeaderComponent::Size], + HeaderComponent::Permissions => &[HeaderComponent::Permissions], + HeaderComponent::LastModified => &[HeaderComponent::LastModified], + HeaderComponent::Full => &[ + HeaderComponent::Filename, + HeaderComponent::Size, + HeaderComponent::Permissions, + HeaderComponent::LastModified, + ], + } + } +} + +impl FromStr for HeaderComponent { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "filename" => Ok(HeaderComponent::Filename), + "size" => Ok(HeaderComponent::Size), + "permissions" => Ok(HeaderComponent::Permissions), + "last-modified" => Ok(HeaderComponent::LastModified), + "full" => Ok(HeaderComponent::Full), + _ => Err(format!("Unknown header-info '{}'", s).into()), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct HeaderComponents(pub HashSet); + +impl HeaderComponents { + pub fn new(components: &[HeaderComponent]) -> HeaderComponents { + HeaderComponents(components.iter().cloned().collect()) + } + + pub fn filename(&self) -> bool { + self.0.contains(&HeaderComponent::Filename) + } + + pub fn size(&self) -> bool { + self.0.contains(&HeaderComponent::Size) + } + + pub fn permissions(&self) -> bool { + self.0.contains(&HeaderComponent::Permissions) + } + + pub fn last_modified(&self) -> bool { + self.0.contains(&HeaderComponent::LastModified) + } +} diff --git a/src/lib.rs b/src/lib.rs index 37b1cd831f..3016665099 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub mod controller; mod decorations; mod diff; pub mod error; +pub mod header; pub mod input; mod less; pub mod line_range; diff --git a/src/printer.rs b/src/printer.rs index 6b8cbc3a64..404441b26d 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -26,6 +26,7 @@ use crate::decorations::{Decoration, GridBorderDecoration, LineNumberDecoration} #[cfg(feature = "git")] use crate::diff::LineChanges; use crate::error::*; +use crate::header::HeaderComponent; use crate::input::OpenedInput; use crate::line_range::RangeCheckResult; use crate::preprocessor::{expand_tabs, replace_nonprintable}; @@ -289,15 +290,6 @@ impl<'a> Printer for InteractivePrinter<'a> { if self.config.style_components.grid() { self.print_horizontal_line(handle, '┬')?; - - write!( - handle, - "{}{}", - " ".repeat(self.panel_width), - self.colors - .grid - .paint(if self.panel_width > 0 { "│ " } else { "" }), - )?; } else { // Only pad space between files, if we haven't already drawn a horizontal rule if add_header_padding && !self.config.style_components.rule() { @@ -316,16 +308,49 @@ impl<'a> Printer for InteractivePrinter<'a> { let description = &input.description; - writeln!( - handle, - "{}{}{}", - description - .kind() - .map(|kind| format!("{}: ", kind)) - .unwrap_or_else(|| "".into()), - self.colors.filename.paint(description.title()), - mode - )?; + self.config + .header_components + .0 + .iter() + .try_for_each(|component| { + if self.config.style_components.grid() { + write!( + handle, + "{}{}", + " ".repeat(self.panel_width), + self.colors + .grid + .paint(if self.panel_width > 0 { "│ " } else { "" }), + )?; + }; + + match component { + HeaderComponent::Filename => writeln!( + handle, + "{}{}{}", + description + .kind() + .map(|kind| format!("{}: ", kind)) + .unwrap_or_else(|| "".into()), + self.colors.filename.paint(description.title()), + mode + ), + + HeaderComponent::Size => writeln!(handle, "Size: {}", "3.3K"), + + HeaderComponent::Permissions => writeln!( + handle, + "Permissions: {}:{} {}", + "mahdi", "mahdi", "rw-rw-rw" + ), + + HeaderComponent::LastModified => { + writeln!(handle, "Last Modified At: {}", "2021 Dec 25 00:20") + } + + _ => Ok(()), + } + })?; if self.config.style_components.grid() { if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {