bat/src/app.rs

258 lines
8.3 KiB
Rust
Raw Normal View History

2018-08-22 22:29:12 +02:00
use std::collections::HashSet;
use std::env;
use std::str::FromStr;
2018-08-22 22:29:12 +02:00
2018-05-10 23:39:13 +02:00
use atty::{self, Stream};
2018-08-22 22:29:12 +02:00
use clap::ArgMatches;
use clap_app;
use wild;
2018-08-22 22:29:12 +02:00
2018-05-10 23:39:13 +02:00
use console::Term;
#[cfg(windows)]
use ansi_term;
use assets::BAT_THEME_DEFAULT;
use config::get_args_from_config_file;
2018-08-22 22:29:12 +02:00
use errors::*;
2018-10-07 11:21:41 +02:00
use inputfile::InputFile;
2018-08-22 22:29:12 +02:00
use line_range::LineRange;
use style::{OutputComponent, OutputComponents, OutputWrap};
#[derive(Debug, Clone, Copy, PartialEq)]
2018-08-22 22:29:12 +02:00
pub enum PagingMode {
Always,
QuitIfOneScreen,
Never,
}
2018-08-27 21:43:22 +02:00
#[derive(Clone)]
2018-08-22 22:29:12 +02:00
pub struct Config<'a> {
2018-08-22 22:36:37 +02:00
/// List of files to print
2018-08-28 20:12:45 +02:00
pub files: Vec<InputFile<'a>>,
2018-08-22 22:36:37 +02:00
/// The explicitly configured language, if any
2018-08-22 22:29:12 +02:00
pub language: Option<&'a str>,
2018-08-22 22:36:37 +02:00
/// The character width of the terminal
pub term_width: usize,
/// The width of tab characters.
/// Currently, a value of 0 will cause tabs to be passed through without expanding them.
pub tab_width: usize,
2018-08-23 19:43:10 +02:00
/// Whether or not to simply loop through all input (`cat` mode)
pub loop_through: bool,
2018-08-22 22:36:59 +02:00
2018-08-22 22:36:37 +02:00
/// Whether or not the output should be colorized
2018-08-22 22:29:12 +02:00
pub colored_output: bool,
2018-08-22 22:36:37 +02:00
/// Whether or not the output terminal supports true color
pub true_color: bool,
/// Style elements (grid, line numbers, ...)
pub output_components: OutputComponents,
/// Text wrapping mode
pub output_wrap: OutputWrap,
/// Pager or STDOUT
2018-08-22 22:29:12 +02:00
pub paging_mode: PagingMode,
2018-08-22 22:36:37 +02:00
/// The range lines that should be printed, if specified
2018-08-22 22:29:12 +02:00
pub line_range: Option<LineRange>,
2018-08-22 22:36:37 +02:00
/// The syntax highlighting theme
pub theme: String,
2018-08-22 22:29:12 +02:00
}
fn is_truecolor_terminal() -> bool {
env::var("COLORTERM")
.map(|colorterm| colorterm == "truecolor" || colorterm == "24bit")
.unwrap_or(false)
}
2018-09-26 19:13:32 +02:00
/// Helper function that might appear in Rust stable at some point
2018-08-22 22:29:12 +02:00
/// (https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.transpose)
fn transpose<T>(opt: Option<Result<T>>) -> Result<Option<T>> {
opt.map_or(Ok(None), |res| res.map(Some))
}
2018-05-10 23:39:13 +02:00
pub struct App {
pub matches: ArgMatches<'static>,
interactive_output: bool,
}
impl App {
pub fn new() -> Self {
2018-05-13 09:37:54 +02:00
#[cfg(windows)]
let _ = ansi_term::enable_ansi_support();
let interactive_output = atty::is(Stream::Stdout);
2018-05-13 09:37:54 +02:00
2018-05-10 23:39:13 +02:00
App {
matches: Self::matches(interactive_output),
interactive_output,
}
}
fn matches(interactive_output: bool) -> ArgMatches<'static> {
let args = if wild::args_os().nth(1) == Some("cache".into()) {
// Skip the arguments in bats config file
wild::args_os().collect::<Vec<_>>()
} else {
let mut cli_args = wild::args_os();
// Read arguments from bats config file
let mut args = get_args_from_config_file();
// Put the zero-th CLI argument (program name) first
args.insert(0, cli_args.next().unwrap());
// .. and the rest at the end
cli_args.for_each(|a| args.push(a));
args
};
clap_app::build_app(interactive_output).get_matches_from(args)
2018-05-10 23:39:13 +02:00
}
pub fn config(&self) -> Result<Config> {
let files = self.files();
let output_components = self.output_components()?;
2018-05-10 23:39:13 +02:00
let paging_mode = match self.matches.value_of("paging") {
Some("always") => PagingMode::Always,
Some("never") => PagingMode::Never,
2018-09-26 19:16:03 +02:00
Some("auto") | _ => {
if files.contains(&InputFile::StdIn) {
// If we are reading from stdin, only enable paging if we write to an
// interactive terminal and if we do not *read* from an interactive
// terminal.
if self.interactive_output && !atty::is(Stream::Stdin) {
PagingMode::QuitIfOneScreen
} else {
PagingMode::Never
}
} else {
2018-09-26 19:16:03 +02:00
if self.interactive_output {
PagingMode::QuitIfOneScreen
} else {
PagingMode::Never
}
}
2018-09-26 19:16:03 +02:00
}
};
2018-05-10 23:39:13 +02:00
Ok(Config {
true_color: is_truecolor_terminal(),
language: self.matches.value_of("language"),
output_wrap: if !self.interactive_output {
// We don't have the tty width when piping to another program.
// There's no point in wrapping when this is the case.
OutputWrap::None
} else {
match self.matches.value_of("wrap") {
Some("character") => OutputWrap::Character,
Some("never") => OutputWrap::None,
2018-09-26 19:16:03 +02:00
Some("auto") | _ => {
if output_components.plain() {
OutputWrap::None
} else {
OutputWrap::Character
}
}
}
},
colored_output: match self.matches.value_of("color") {
Some("always") => true,
Some("never") => false,
Some("auto") | _ => self.interactive_output,
},
paging_mode,
2018-09-07 20:13:03 +02:00
term_width: self
.matches
2018-08-31 22:52:22 +02:00
.value_of("terminal-width")
.and_then(|w| w.parse().ok())
.unwrap_or(Term::stdout().size().1 as usize),
loop_through: !(self.interactive_output
|| self.matches.value_of("color") == Some("always")
|| self.matches.value_of("decorations") == Some("always")),
2018-05-10 23:39:13 +02:00
files,
tab_width: self
.matches
.value_of("tabs")
.map(String::from)
.or_else(|| env::var("BAT_TABS").ok())
.and_then(|t| t.parse().ok())
.unwrap_or(
if output_components.plain() && paging_mode == PagingMode::Never {
0
} else {
8
},
),
2018-09-07 20:13:03 +02:00
theme: self
.matches
.value_of("theme")
.map(String::from)
.or_else(|| env::var("BAT_THEME").ok())
.unwrap_or(String::from(BAT_THEME_DEFAULT)),
2018-06-12 06:10:14 +02:00
line_range: transpose(self.matches.value_of("line-range").map(LineRange::from))?,
output_components,
2018-05-10 23:39:13 +02:00
})
}
2018-08-28 20:12:45 +02:00
fn files(&self) -> Vec<InputFile> {
2018-05-10 23:39:13 +02:00
self.matches
.values_of("FILE")
.map(|values| {
values
.map(|filename| {
if filename == "-" {
2018-08-28 20:12:45 +02:00
InputFile::StdIn
2018-05-10 23:39:13 +02:00
} else {
2018-08-28 20:12:45 +02:00
InputFile::Ordinary(filename)
2018-05-10 23:39:13 +02:00
}
2018-09-26 19:16:03 +02:00
})
.collect()
})
.unwrap_or_else(|| vec![InputFile::StdIn])
2018-05-10 23:39:13 +02:00
}
fn output_components(&self) -> Result<OutputComponents> {
let matches = &self.matches;
Ok(OutputComponents(
if matches.value_of("decorations") == Some("never") {
HashSet::new()
} else if matches.is_present("number") {
[OutputComponent::Numbers].iter().cloned().collect()
} else if matches.is_present("plain") {
[OutputComponent::Plain].iter().cloned().collect()
} else {
let env_style_components: Option<Vec<OutputComponent>> =
transpose(env::var("BAT_STYLE").ok().map(|style_str| {
style_str
.split(",")
.map(|x| OutputComponent::from_str(&x))
.collect::<Result<Vec<OutputComponent>>>()
}))?;
values_t!(matches.values_of("style"), OutputComponent)
.ok()
.or(env_style_components)
.unwrap_or(vec![OutputComponent::Full])
.into_iter()
.map(|style| style.components(self.interactive_output))
.fold(HashSet::new(), |mut acc, components| {
acc.extend(components.iter().cloned());
acc
})
},
))
2018-05-10 23:39:13 +02:00
}
}