on master help sign in
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.)

commit23813cc08bd94bd52775922a3fe94cd019b64178 authorPit Kleyersburg <pitkley@googlemail.com> dateSun 6 May 2018, 20:15 (9 years ago) parentfdeb98e
4 files changed+240−77
Cargo.lock+7 Cargo.toml+1 src/main.rs+125−16 src/printer.rs+107−61
Cargo.lock+7
@@ -68,6 +68,7 @@ dependencies = [
6868 "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)",
6969 "console 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
7070 "directories 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
71+ "either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
7172 "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
7273 "git2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
7374 "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -194,6 +195,11 @@ dependencies = [
194195 "shared_child 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
195196]
196197
198+[[package]]
199+name = "either"
200+version = "1.5.0"
201+source = "registry+https://github.com/rust-lang/crates.io-index"
202+
197203[[package]]
198204name = "error-chain"
199205version = "0.11.0"
@@ -820,6 +826,7 @@ dependencies = [
820826"checksum directories 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc2561db021b6f1321d0f16b67ed28ce843ef4610dfaa432e3ffa2e8a3050ebf"
821827"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
822828"checksum duct 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "166298c17c5b4fe5997b962c2f22e887c7c5adc44308eb9103ce5b66af45a423"
829+"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0"
823830"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3"
824831"checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909"
825832"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
Cargo.toml+1
@@ -13,6 +13,7 @@ version = "0.3.0"
1313atty = "0.2.2"
1414ansi_term = "0.10"
1515console = "0.6"
16+either = "1.4"
1617error-chain = "0.11"
1718directories = "0.10"
1819lazy_static = "1.0"
src/main.rs+125−16
@@ -14,6 +14,7 @@ extern crate ansi_term;
1414extern crate atty;
1515extern crate console;
1616extern crate directories;
17+extern crate either;
1718extern crate git2;
1819extern crate syntect;
1920
@@ -26,6 +27,7 @@ use std::fs::{self, File};
2627use std::io::{self, BufRead, BufReader, Write};
2728use std::path::{Path, PathBuf};
2829use std::process::{self, Child, Command, Stdio};
30+use std::str::FromStr;
2931
3032#[cfg(unix)]
3133use std::os::unix::fs::FileTypeExt;
@@ -35,6 +37,7 @@ use ansi_term::Style;
3537use atty::Stream;
3638use clap::{App, AppSettings, Arg, ArgGroup, SubCommand};
3739use directories::ProjectDirs;
40+use either::Either;
3841use git2::{DiffOptions, IntoCString, Repository};
3942
4043use syntect::dumps::{dump_to_file, from_binary, from_reader};
@@ -53,20 +56,79 @@ mod errors {
5356 foreign_links {
5457 Io(::std::io::Error);
5558 }
59+
60+ errors {
61+ NoCorrectStylesSpecified {
62+ description("no correct styles specified")
63+ }
64+
65+ UnknownStyleName(name: String) {
66+ description("unknown style name")
67+ display("unknown style name: '{}'", name)
68+ }
69+ }
5670 }
5771}
5872
5973use errors::*;
6074
61−pub enum OptionsStyle {
62− Plain,
63− LineNumbers,
75+#[derive(Debug, Eq, PartialEq)]
76+pub enum OutputComponent {
77+ Changes,
78+ Grid,
79+ Header,
80+ Numbers,
81+}
82+
83+impl FromStr for OutputComponent {
84+ type Err = Error;
85+
86+ fn from_str(s: &str) -> Result<Self> {
87+ match s {
88+ "changes" => Ok(OutputComponent::Changes),
89+ "grid" => Ok(OutputComponent::Grid),
90+ "header" => Ok(OutputComponent::Header),
91+ "numbers" => Ok(OutputComponent::Numbers),
92+ _ => Err(ErrorKind::UnknownStyleName(s.to_owned()).into()),
93+ }
94+ }
95+}
96+
97+#[derive(Debug, Eq, PartialEq)]
98+enum PredefinedStyle {
6499 Full,
100+ Plain,
101+}
102+
103+impl PredefinedStyle {
104+ fn components(&self) -> &'static [OutputComponent] {
105+ match *self {
106+ PredefinedStyle::Full => &[
107+ OutputComponent::Changes,
108+ OutputComponent::Grid,
109+ OutputComponent::Header,
110+ OutputComponent::Numbers,
111+ ],
112+ PredefinedStyle::Plain => &[],
113+ }
114+ }
115+}
116+
117+impl FromStr for PredefinedStyle {
118+ type Err = Error;
119+
120+ fn from_str(s: &str) -> Result<Self> {
121+ match s {
122+ "full" => Ok(PredefinedStyle::Full),
123+ "plain" => Ok(PredefinedStyle::Plain),
124+ _ => Err(ErrorKind::UnknownStyleName(s.to_owned()).into()),
125+ }
126+ }
65127}
66128
67129pub struct Options<'a> {
68130 pub true_color: bool,
69− pub style: OptionsStyle,
131+ pub output_components: &'a [OutputComponent],
70132 pub language: Option<&'a str>,
71133 pub colored_output: bool,
72134 pub paging: bool,
@@ -456,8 +518,10 @@ fn run() -> Result<()> {
456518 .arg(
457519 Arg::with_name("style")
458520 .long("style")
459− .possible_values(&["auto", "plain", "line-numbers", "full"])
460− .default_value("auto")
521+ .use_delimiter(true)
522+ .takes_value(true)
523+ .possible_values(&["full", "plain", "changes", "header", "grid", "numbers"])
524+ .default_value("full")
461525 .help("Additional info to display along with content"),
462526 )
463527 .arg(
@@ -545,18 +609,63 @@ fn run() -> Result<()> {
545609 })
546610 .unwrap_or_else(|| vec![None]); // read from stdin (None) if no args are given
547611
612+ // Split the user-supplied parameter on commas (','), then go over each part and map it
613+ // to either a predefined style or a simple output style. Once this mapping is done,
614+ // split it up into two lists: one for the matched predefined styles and one for the
615+ // simple output styles.
616+ let (_predefined_styles, _output_components): (Vec<_>, Vec<_>) = app_matches
617+ .values_of("style")
618+ .unwrap()
619+ .map(|s| {
620+ let predefined_style = PredefinedStyle::from_str(s);
621+ if let Ok(style) = predefined_style {
622+ Some(Either::Left(style))
623+ } else {
624+ match OutputComponent::from_str(s) {
625+ Ok(style) => Some(Either::Right(style)),
626+ Err(error) => {
627+ eprintln!("{}: {}", Red.paint("[bat error]"), error);
628+ None
629+ }
630+ }
631+ }
632+ })
633+ .filter_map(|s| s)
634+ .partition(|style| match *style {
635+ Either::Left(_) => true,
636+ Either::Right(_) => false,
637+ });
638+
639+ // Once we have the two lists of predefined styles and simple output styles, we take the
640+ // first matched predefined style and use that as our style, discarding every other
641+ // predefined or simple output style. If we have no predefined styles though, we take
642+ // all matched simple output styles and pass those to the `Options` struct.
643+ let output_components: Vec<OutputComponent>;
644+ let output_components = if let Some(predefined_style) = _predefined_styles
645+ .into_iter()
646+ .map(|e| e.left().unwrap())
647+ .next()
648+ {
649+ predefined_style.components()
650+ } else {
651+ output_components = _output_components
652+ .into_iter()
653+ .map(|e| e.right().unwrap())
654+ .collect::<Vec<_>>();
655+
656+ // If we are in this else-block, `output_components` is empty and the requested style
657+ // wasn't the empty string, the user supplied only wrong style arguments. In this
658+ // case we inform them about this.
659+ if output_components.is_empty() && app_matches.value_of("style").unwrap() != "" {
660+ return Err(ErrorKind::NoCorrectStylesSpecified.into());
661+ }
662+
663+ &output_components
664+ };
665+
548666 let options = Options {
549667 true_color: is_truecolor_terminal(),
550− style: match app_matches.value_of("style") {
551− Some("plain") => OptionsStyle::Plain,
552− Some("line-numbers") => OptionsStyle::LineNumbers,
553− Some("full") => OptionsStyle::Full,
554− Some("auto") | _ => if interactive_terminal {
555− OptionsStyle::Full
556− } else {
557− OptionsStyle::Plain
558− },
559− },
668+ output_components,
560669 language: app_matches.value_of("language"),
561670 colored_output: match app_matches.value_of("color") {
562671 Some("always") => true,
src/printer.rs+107−61
@@ -1,9 +1,10 @@
11use ansi_term::Style;
22use errors::*;
3+use std::borrow::Cow;
34use std::io::Write;
45use syntect::highlighting;
56use terminal::as_terminal_escaped;
6−use {Colors, LineChange, LineChanges, Options, OptionsStyle};
7+use {Colors, LineChange, LineChanges, Options, OutputComponent};
78
89const PANEL_WIDTH: usize = 7;
910
@@ -31,32 +32,48 @@ impl<'a> Printer<'a> {
3132 }
3233
3334 pub fn print_header(&mut self, filename: Option<&str>) -> Result<()> {
34− match self.options.style {
35− OptionsStyle::Full => {}
36− _ => return Ok(()),
35+ if !self.options
36+ .output_components
37+ .contains(&OutputComponent::Header)
38+ {
39+ return Ok(());
3740 }
3841
39− self.print_horizontal_line('┬')?;
40−
41− write!(
42− self.handle,
43− "{}{} ",
44− " ".repeat(PANEL_WIDTH),
45− self.colors.grid.paint("│"),
46− )?;
47−
48− writeln!(
49− self.handle,
50− "{}{}",
51− filename.map_or("", |_| "File: "),
52− self.colors.filename.paint(filename.unwrap_or("STDIN"))
53− )?;
54−
55− self.print_horizontal_line('┼')
42+ if self.options
43+ .output_components
44+ .contains(&OutputComponent::Grid)
45+ {
46+ self.print_horizontal_line('┬')?;
47+
48+ write!(
49+ self.handle,
50+ "{}{} ",
51+ " ".repeat(PANEL_WIDTH),
52+ self.colors.grid.paint("│"),
53+ )?;
54+
55+ writeln!(
56+ self.handle,
57+ "{}{}",
58+ filename.map_or("", |_| "File: "),
59+ self.colors.filename.paint(filename.unwrap_or("STDIN"))
60+ )?;
61+
62+ self.print_horizontal_line('┼')
63+ } else {
64+ writeln!(
65+ self.handle,
66+ "File {}",
67+ self.colors.filename.paint(filename.unwrap_or("STDIN"))
68+ ).map_err(Into::into)
69+ }
5670 }
5771
5872 pub fn print_footer(&mut self) -> Result<()> {
59− if let OptionsStyle::Full = self.options.style {
73+ if self.options
74+ .output_components
75+ .contains(&OutputComponent::Grid)
76+ {
6077 self.print_horizontal_line('┴')
6178 } else {
6279 Ok(())
@@ -72,19 +89,28 @@ impl<'a> Printer<'a> {
7289 self.print_line_number(line_number),
7390 self.print_git_marker(line_number),
7491 self.print_line_border(),
75− Some(as_terminal_escaped(
76− &regions,
77− self.options.true_color,
78− self.options.colored_output,
79− )),
92+ Some(
93+ as_terminal_escaped(
94+ &regions,
95+ self.options.true_color,
96+ self.options.colored_output,
97+ ).into(),
98+ ),
8099 ];
81100
101+ let grid_requested = self.options
102+ .output_components
103+ .contains(&OutputComponent::Grid);
82104 write!(
83105 self.handle,
84106 "{}",
85107 decorations
86108 .into_iter()
87− .filter_map(|dec| dec)
109+ .filter_map(|dec| if grid_requested {
110+ Some(dec.unwrap_or(" ".into()))
111+ } else {
112+ dec
113+ })
88114 .collect::<Vec<_>>()
89115 .join(" ")
90116 )?;
@@ -92,46 +118,66 @@ impl<'a> Printer<'a> {
92118 Ok(())
93119 }
94120
95− fn print_line_number(&self, line_number: usize) -> Option<String> {
96− if let OptionsStyle::Plain = self.options.style {
97− return None;
121+ fn print_line_number<'s>(&self, line_number: usize) -> Option<Cow<'s, str>> {
122+ if self.options
123+ .output_components
124+ .contains(&OutputComponent::Numbers)
125+ {
126+ Some(
127+ self.colors
128+ .line_number
129+ .paint(format!("{:4}", line_number))
130+ .to_string()
131+ .into(),
132+ )
133+ } else if self.options
134+ .output_components
135+ .contains(&OutputComponent::Grid)
136+ {
137+ Some(" ".into())
138+ } else {
139+ None
98140 }
99−
100− Some(
101− self.colors
102− .line_number
103− .paint(format!("{:4}", line_number))
104− .to_string(),
105− )
106141 }
107142
108− fn print_git_marker(&self, line_number: usize) -> Option<String> {
109− match self.options.style {
110− OptionsStyle::Full => {}
111− _ => return None,
112− }
113−
114− let marker = if let Some(ref changes) = self.line_changes {
115− match changes.get(&(line_number as u32)) {
116− Some(&LineChange::Added) => self.colors.git_added.paint("+"),
117− Some(&LineChange::RemovedAbove) => self.colors.git_removed.paint("‾"),
118− Some(&LineChange::RemovedBelow) => self.colors.git_removed.paint("_"),
119− Some(&LineChange::Modified) => self.colors.git_modified.paint("~"),
120− _ => Style::default().paint(" "),
121− }
143+ fn print_git_marker<'s>(&self, line_number: usize) -> Option<Cow<'s, str>> {
144+ if self.options
145+ .output_components
146+ .contains(&OutputComponent::Changes)
147+ {
148+ Some(
149+ if let Some(ref changes) = self.line_changes {
150+ match changes.get(&(line_number as u32)) {
151+ Some(&LineChange::Added) => self.colors.git_added.paint("+"),
152+ Some(&LineChange::RemovedAbove) => self.colors.git_removed.paint("‾"),
153+ Some(&LineChange::RemovedBelow) => self.colors.git_removed.paint("_"),
154+ Some(&LineChange::Modified) => self.colors.git_modified.paint("~"),
155+ _ => Style::default().paint(" "),
156+ }
157+ } else {
158+ Style::default().paint(" ")
159+ }.to_string()
160+ .into(),
161+ )
162+ } else if self.options
163+ .output_components
164+ .contains(&OutputComponent::Grid)
165+ {
166+ Some(" ".into())
122167 } else {
123− Style::default().paint(" ")
124− };
125−
126− Some(marker.to_string())
168+ None
169+ }
127170 }
128171
129− fn print_line_border(&self) -> Option<String> {
130− if let OptionsStyle::Plain = self.options.style {
131− return None;
172+ fn print_line_border<'s>(&self) -> Option<Cow<'s, str>> {
173+ if self.options
174+ .output_components
175+ .contains(&OutputComponent::Grid)
176+ {
177+ Some(self.colors.grid.paint("│").to_string().into())
178+ } else {
179+ None
132180 }
133−
134− Some(self.colors.grid.paint("│").to_string())
135181 }
136182
137183 fn print_horizontal_line(&mut self, grid_char: char) -> Result<()> {