From 23813cc08bd94bd52775922a3fe94cd019b64178 Mon Sep 17 00:00:00 2001 From: Pit Kleyersburg Date: Sun, 6 May 2018 20:15:46 +0200 Subject: [PATCH] Make `--style` parameter more flexible The `--style` parameter now accepts a comma-separated list of strings, where every element defines either a single output component (`changes`, `grid`, `header`, `numbers`) or a predefined style (`full`, `line-numbers`, `plain`). If available, bat picks the first predefined style in the user-supplied style-list and ignores everything else. If no predefined style was requested, the other parameters that are simple output components will be used. Examples: --style changes,full,numbers Will internally be reduced to only the predefined style `full`. --style plain,full Will internally be reduced to only the predefined style `plain`. --style changes,numbers Will not be reduced, because the list does not contain any predefined styles. (Note: if `grid` is requested but no other parameters, bat still creates the left-most column with a width of `PANEL_WIDTH`. I didn't want to introduce further logic in this PR that drops or adapts the width of the left column.) --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/main.rs | 141 ++++++++++++++++++++++++++++++++++++----- src/printer.rs | 166 +++++++++++++++++++++++++++++++------------------ 4 files changed, 239 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 983c77b5..467cca0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,7 @@ dependencies = [ "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", "console 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "directories 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -194,6 +195,11 @@ dependencies = [ "shared_child 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "either" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "error-chain" version = "0.11.0" @@ -820,6 +826,7 @@ dependencies = [ "checksum directories 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc2561db021b6f1321d0f16b67ed28ce843ef4610dfaa432e3ffa2e8a3050ebf" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum duct 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "166298c17c5b4fe5997b962c2f22e887c7c5adc44308eb9103ce5b66af45a423" +"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0" "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" "checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" diff --git a/Cargo.toml b/Cargo.toml index ccb1f44d..8b85bf1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ version = "0.3.0" atty = "0.2.2" ansi_term = "0.10" console = "0.6" +either = "1.4" error-chain = "0.11" directories = "0.10" lazy_static = "1.0" diff --git a/src/main.rs b/src/main.rs index 773f3432..8c1949c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ extern crate ansi_term; extern crate atty; extern crate console; extern crate directories; +extern crate either; extern crate git2; extern crate syntect; @@ -26,6 +27,7 @@ use std::fs::{self, File}; use std::io::{self, BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::process::{self, Child, Command, Stdio}; +use std::str::FromStr; #[cfg(unix)] use std::os::unix::fs::FileTypeExt; @@ -35,6 +37,7 @@ use ansi_term::Style; use atty::Stream; use clap::{App, AppSettings, Arg, ArgGroup, SubCommand}; use directories::ProjectDirs; +use either::Either; use git2::{DiffOptions, IntoCString, Repository}; use syntect::dumps::{dump_to_file, from_binary, from_reader}; @@ -53,20 +56,79 @@ mod errors { foreign_links { Io(::std::io::Error); } + + errors { + NoCorrectStylesSpecified { + description("no correct styles specified") + } + + UnknownStyleName(name: String) { + description("unknown style name") + display("unknown style name: '{}'", name) + } + } } } use errors::*; -pub enum OptionsStyle { - Plain, - LineNumbers, +#[derive(Debug, Eq, PartialEq)] +pub enum OutputComponent { + Changes, + Grid, + Header, + Numbers, +} + +impl FromStr for OutputComponent { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "changes" => Ok(OutputComponent::Changes), + "grid" => Ok(OutputComponent::Grid), + "header" => Ok(OutputComponent::Header), + "numbers" => Ok(OutputComponent::Numbers), + _ => Err(ErrorKind::UnknownStyleName(s.to_owned()).into()), + } + } +} + +#[derive(Debug, Eq, PartialEq)] +enum PredefinedStyle { Full, + Plain, +} + +impl PredefinedStyle { + fn components(&self) -> &'static [OutputComponent] { + match *self { + PredefinedStyle::Full => &[ + OutputComponent::Changes, + OutputComponent::Grid, + OutputComponent::Header, + OutputComponent::Numbers, + ], + PredefinedStyle::Plain => &[], + } + } +} + +impl FromStr for PredefinedStyle { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "full" => Ok(PredefinedStyle::Full), + "plain" => Ok(PredefinedStyle::Plain), + _ => Err(ErrorKind::UnknownStyleName(s.to_owned()).into()), + } + } } pub struct Options<'a> { pub true_color: bool, - pub style: OptionsStyle, + pub output_components: &'a [OutputComponent], pub language: Option<&'a str>, pub colored_output: bool, pub paging: bool, @@ -456,8 +518,10 @@ fn run() -> Result<()> { .arg( Arg::with_name("style") .long("style") - .possible_values(&["auto", "plain", "line-numbers", "full"]) - .default_value("auto") + .use_delimiter(true) + .takes_value(true) + .possible_values(&["full", "plain", "changes", "header", "grid", "numbers"]) + .default_value("full") .help("Additional info to display along with content"), ) .arg( @@ -545,18 +609,63 @@ fn run() -> Result<()> { }) .unwrap_or_else(|| vec![None]); // read from stdin (None) if no args are given + // Split the user-supplied parameter on commas (','), then go over each part and map it + // to either a predefined style or a simple output style. Once this mapping is done, + // split it up into two lists: one for the matched predefined styles and one for the + // simple output styles. + let (_predefined_styles, _output_components): (Vec<_>, Vec<_>) = app_matches + .values_of("style") + .unwrap() + .map(|s| { + let predefined_style = PredefinedStyle::from_str(s); + if let Ok(style) = predefined_style { + Some(Either::Left(style)) + } else { + match OutputComponent::from_str(s) { + Ok(style) => Some(Either::Right(style)), + Err(error) => { + eprintln!("{}: {}", Red.paint("[bat error]"), error); + None + } + } + } + }) + .filter_map(|s| s) + .partition(|style| match *style { + Either::Left(_) => true, + Either::Right(_) => false, + }); + + // Once we have the two lists of predefined styles and simple output styles, we take the + // first matched predefined style and use that as our style, discarding every other + // predefined or simple output style. If we have no predefined styles though, we take + // all matched simple output styles and pass those to the `Options` struct. + let output_components: Vec; + let output_components = if let Some(predefined_style) = _predefined_styles + .into_iter() + .map(|e| e.left().unwrap()) + .next() + { + predefined_style.components() + } else { + output_components = _output_components + .into_iter() + .map(|e| e.right().unwrap()) + .collect::>(); + + // If we are in this else-block, `output_components` is empty and the requested style + // wasn't the empty string, the user supplied only wrong style arguments. In this + // case we inform them about this. + if output_components.is_empty() && app_matches.value_of("style").unwrap() != "" { + return Err(ErrorKind::NoCorrectStylesSpecified.into()); + } + + &output_components + }; + let options = Options { true_color: is_truecolor_terminal(), - style: match app_matches.value_of("style") { - Some("plain") => OptionsStyle::Plain, - Some("line-numbers") => OptionsStyle::LineNumbers, - Some("full") => OptionsStyle::Full, - Some("auto") | _ => if interactive_terminal { - OptionsStyle::Full - } else { - OptionsStyle::Plain - }, - }, + output_components, language: app_matches.value_of("language"), colored_output: match app_matches.value_of("color") { Some("always") => true, diff --git a/src/printer.rs b/src/printer.rs index 0d0a84c5..c6181ed4 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,9 +1,10 @@ use ansi_term::Style; use errors::*; +use std::borrow::Cow; use std::io::Write; use syntect::highlighting; use terminal::as_terminal_escaped; -use {Colors, LineChange, LineChanges, Options, OptionsStyle}; +use {Colors, LineChange, LineChanges, Options, OutputComponent}; const PANEL_WIDTH: usize = 7; @@ -31,32 +32,48 @@ impl<'a> Printer<'a> { } pub fn print_header(&mut self, filename: Option<&str>) -> Result<()> { - match self.options.style { - OptionsStyle::Full => {} - _ => return Ok(()), + if !self.options + .output_components + .contains(&OutputComponent::Header) + { + return Ok(()); } - self.print_horizontal_line('┬')?; + if self.options + .output_components + .contains(&OutputComponent::Grid) + { + self.print_horizontal_line('┬')?; - write!( - self.handle, - "{}{} ", - " ".repeat(PANEL_WIDTH), - self.colors.grid.paint("│"), - )?; + write!( + self.handle, + "{}{} ", + " ".repeat(PANEL_WIDTH), + self.colors.grid.paint("│"), + )?; - writeln!( - self.handle, - "{}{}", - filename.map_or("", |_| "File: "), - self.colors.filename.paint(filename.unwrap_or("STDIN")) - )?; + writeln!( + self.handle, + "{}{}", + filename.map_or("", |_| "File: "), + self.colors.filename.paint(filename.unwrap_or("STDIN")) + )?; - self.print_horizontal_line('┼') + self.print_horizontal_line('┼') + } else { + writeln!( + self.handle, + "File {}", + self.colors.filename.paint(filename.unwrap_or("STDIN")) + ).map_err(Into::into) + } } pub fn print_footer(&mut self) -> Result<()> { - if let OptionsStyle::Full = self.options.style { + if self.options + .output_components + .contains(&OutputComponent::Grid) + { self.print_horizontal_line('┴') } else { Ok(()) @@ -72,19 +89,28 @@ impl<'a> Printer<'a> { self.print_line_number(line_number), self.print_git_marker(line_number), self.print_line_border(), - Some(as_terminal_escaped( - ®ions, - self.options.true_color, - self.options.colored_output, - )), + Some( + as_terminal_escaped( + ®ions, + self.options.true_color, + self.options.colored_output, + ).into(), + ), ]; + let grid_requested = self.options + .output_components + .contains(&OutputComponent::Grid); write!( self.handle, "{}", decorations .into_iter() - .filter_map(|dec| dec) + .filter_map(|dec| if grid_requested { + Some(dec.unwrap_or(" ".into())) + } else { + dec + }) .collect::>() .join(" ") )?; @@ -92,46 +118,66 @@ impl<'a> Printer<'a> { Ok(()) } - fn print_line_number(&self, line_number: usize) -> Option { - if let OptionsStyle::Plain = self.options.style { - return None; - } - - Some( - self.colors - .line_number - .paint(format!("{:4}", line_number)) - .to_string(), - ) - } - - fn print_git_marker(&self, line_number: usize) -> Option { - match self.options.style { - OptionsStyle::Full => {} - _ => return None, - } - - let marker = if let Some(ref changes) = self.line_changes { - match changes.get(&(line_number as u32)) { - Some(&LineChange::Added) => self.colors.git_added.paint("+"), - Some(&LineChange::RemovedAbove) => self.colors.git_removed.paint("‾"), - Some(&LineChange::RemovedBelow) => self.colors.git_removed.paint("_"), - Some(&LineChange::Modified) => self.colors.git_modified.paint("~"), - _ => Style::default().paint(" "), - } + fn print_line_number<'s>(&self, line_number: usize) -> Option> { + if self.options + .output_components + .contains(&OutputComponent::Numbers) + { + Some( + self.colors + .line_number + .paint(format!("{:4}", line_number)) + .to_string() + .into(), + ) + } else if self.options + .output_components + .contains(&OutputComponent::Grid) + { + Some(" ".into()) } else { - Style::default().paint(" ") - }; - - Some(marker.to_string()) + None + } } - fn print_line_border(&self) -> Option { - if let OptionsStyle::Plain = self.options.style { - return None; + fn print_git_marker<'s>(&self, line_number: usize) -> Option> { + if self.options + .output_components + .contains(&OutputComponent::Changes) + { + Some( + if let Some(ref changes) = self.line_changes { + match changes.get(&(line_number as u32)) { + Some(&LineChange::Added) => self.colors.git_added.paint("+"), + Some(&LineChange::RemovedAbove) => self.colors.git_removed.paint("‾"), + Some(&LineChange::RemovedBelow) => self.colors.git_removed.paint("_"), + Some(&LineChange::Modified) => self.colors.git_modified.paint("~"), + _ => Style::default().paint(" "), + } + } else { + Style::default().paint(" ") + }.to_string() + .into(), + ) + } else if self.options + .output_components + .contains(&OutputComponent::Grid) + { + Some(" ".into()) + } else { + None } + } - Some(self.colors.grid.paint("│").to_string()) + fn print_line_border<'s>(&self) -> Option> { + if self.options + .output_components + .contains(&OutputComponent::Grid) + { + Some(self.colors.grid.paint("│").to_string().into()) + } else { + None + } } fn print_horizontal_line(&mut self, grid_char: char) -> Result<()> {