From 10a56695c25c5965f4f2c77a46d1c0e542617da6 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 --- Cargo.lock | 11 ++++ Cargo.toml | 2 + assets/syntaxes/02_Extra/MediaWiki | 2 +- assets/syntaxes/02_Extra/ssh-config | 2 +- src/bin/bat/app.rs | 37 ++++++++++++ src/bin/bat/clap_app.rs | 27 +++++++++ src/config.rs | 4 ++ src/header.rs | 89 +++++++++++++++++++++++++++++ src/input.rs | 32 ++++++++++- src/lib.rs | 1 + src/printer.rs | 89 +++++++++++++++++++++++------ 11 files changed, 274 insertions(+), 22 deletions(-) create mode 100644 src/header.rs diff --git a/Cargo.lock b/Cargo.lock index 4d8fecb172..5bb9244c32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,7 @@ dependencies = [ "atty", "bincode", "bugreport", + "bytesize", "clap", "clircle", "console", @@ -105,6 +106,7 @@ dependencies = [ "syntect", "tempfile", "thiserror", + "time", "unicode-width", "wait-timeout", "walkdir", @@ -169,6 +171,15 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.0.72" diff --git a/Cargo.toml b/Cargo.toml index 639e9d9656..0e63141e1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,8 @@ dirs-next = { version = "2.0.0", optional = true } grep-cli = { version = "0.1.6", optional = true } regex = { version = "1.0", optional = true } walkdir = { version = "2.0", optional = true } +time = { version = "0.3.5", features = ["formatting"] } +bytesize = {version = "1.1.0", features = ["serde"]} [dependencies.git2] version = "0.13" diff --git a/assets/syntaxes/02_Extra/MediaWiki b/assets/syntaxes/02_Extra/MediaWiki index 81bf97cace..843a483e8b 160000 --- a/assets/syntaxes/02_Extra/MediaWiki +++ b/assets/syntaxes/02_Extra/MediaWiki @@ -1 +1 @@ -Subproject commit 81bf97cace59bedcb1668e7830b85c36e014428e +Subproject commit 843a483e8b9f18c4ba914f29a788db5b3a87ed11 diff --git a/assets/syntaxes/02_Extra/ssh-config b/assets/syntaxes/02_Extra/ssh-config index e1012e9f13..201816b609 160000 --- a/assets/syntaxes/02_Extra/ssh-config +++ b/assets/syntaxes/02_Extra/ssh-config @@ -1 +1 @@ -Subproject commit e1012e9f13c6073f559b14206df2ede35720e884 +Subproject commit 201816b609abf7ccf583f7e888f6dc4121410d70 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 e0460f7482..834c6b6237 100644 --- a/src/bin/bat/clap_app.rs +++ b/src/bin/bat/clap_app.rs @@ -381,6 +381,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/input.rs b/src/input.rs index ffaca0ae0b..0aebb9b4c6 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,7 +1,10 @@ use std::convert::TryFrom; +use std::fs; use std::fs::File; use std::io::{self, BufRead, BufReader, Read}; +use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; +use std::time::SystemTime; use clircle::{Clircle, Identifier}; use content_inspector::{self, ContentType}; @@ -84,9 +87,17 @@ impl<'a> InputKind<'a> { } } +#[derive(Clone)] +pub(crate) struct InputPermissions { + pub(crate) mode: u32, +} + #[derive(Clone, Default)] pub(crate) struct InputMetadata { pub(crate) user_provided_name: Option, + pub(crate) size: Option, + pub(crate) permissions: Option, + pub(crate) modified: Option, } pub struct Input<'a> { @@ -130,9 +141,28 @@ impl<'a> Input<'a> { fn _ordinary_file(path: &Path) -> Self { let kind = InputKind::OrdinaryFile(path.to_path_buf()); + let metadata = match fs::metadata(path.to_path_buf()) { + Ok(meta) => { + let size = meta.len(); + let modified = meta.modified().ok(); + let perm = meta.permissions(); + InputMetadata { + size: Some(size), + modified: modified, + permissions: Some(InputPermissions { + // the 3 digits from right are the familiar mode bits + // we are looking for + mode: perm.mode() & 0o777, + }), + ..InputMetadata::default() + } + } + Err(_) => InputMetadata::default(), + }; + Input { description: kind.description(), - metadata: InputMetadata::default(), + metadata: metadata, kind, } } 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..7764108b05 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -4,6 +4,9 @@ use std::vec::Vec; use ansi_term::Colour::{Fixed, Green, Red, Yellow}; use ansi_term::Style; +use bytesize::ByteSize; +use time::{format_description, OffsetDateTime}; + use console::AnsiCodeIterator; use syntect::easy::HighlightLines; @@ -26,6 +29,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 +293,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() { @@ -315,17 +310,73 @@ impl<'a> Printer for InteractivePrinter<'a> { }; let description = &input.description; + let metadata = &input.metadata; + + 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 { "" }), + )?; + }; - writeln!( - handle, - "{}{}{}", - description - .kind() - .map(|kind| format!("{}: ", kind)) - .unwrap_or_else(|| "".into()), - self.colors.filename.paint(description.title()), - mode - )?; + 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 => { + let bsize = metadata + .size + .map(|s| format!("{}", ByteSize(s))) + .unwrap_or("".into()); + writeln!(handle, "Size: {}", bsize) + } + + HeaderComponent::Permissions => writeln!( + handle, + "Permissions: {:o}", + metadata + .permissions + .clone() + .map(|perm| perm.mode) + .unwrap_or(0) + ), + + HeaderComponent::LastModified => { + let format = format_description::parse( + "[day] [month repr:short] [year] [hour]:[minute]:[second]", + ) + .unwrap(); + let fmt_modified = metadata + .modified + .map(|t| OffsetDateTime::from(t).format(&format).unwrap()); + + writeln!( + handle, + "Last Modified At: {}", + fmt_modified.unwrap_or("".into()) + ) + } + + _ => Ok(()), + } + })?; if self.config.style_components.grid() { if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {