on afc5aac help sign in
bat/src/app.rs
275 lines 9.2 kB history raw
1use std::collections::HashSet;
2use std::env;
3use std::str::FromStr;
5use atty::{self, Stream};
7use clap::ArgMatches;
8use clap_app;
9use wild;
11use console::Term;
13#[cfg(windows)]
14use ansi_term;
16use assets::BAT_THEME_DEFAULT;
17use config::{get_args_from_config_file, get_args_from_env_var};
18use errors::*;
19use inputfile::InputFile;
20use line_range::LineRange;
21use style::{OutputComponent, OutputComponents, OutputWrap};
22use syntax_mapping::SyntaxMapping;
23use util::transpose;
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum PagingMode {
27 Always,
28 QuitIfOneScreen,
29 Never,
32#[derive(Clone)]
33pub struct Config<'a> {
34 /// List of files to print
35 pub files: Vec<InputFile<'a>>,
37 /// The explicitly configured language, if any
38 pub language: Option<&'a str>,
40 /// The character width of the terminal
41 pub term_width: usize,
43 /// The width of tab characters.
44 /// Currently, a value of 0 will cause tabs to be passed through without expanding them.
45 pub tab_width: usize,
47 /// Whether or not to simply loop through all input (`cat` mode)
48 pub loop_through: bool,
50 /// Whether or not the output should be colorized
51 pub colored_output: bool,
53 /// Whether or not the output terminal supports true color
54 pub true_color: bool,
56 /// Style elements (grid, line numbers, ...)
57 pub output_components: OutputComponents,
59 /// Text wrapping mode
60 pub output_wrap: OutputWrap,
62 /// Pager or STDOUT
63 pub paging_mode: PagingMode,
65 /// The range lines that should be printed, if specified
66 pub line_range: Option<LineRange>,
68 /// The syntax highlighting theme
69 pub theme: String,
71 /// File extension/name mappings
72 pub syntax_mapping: SyntaxMapping,
75fn is_truecolor_terminal() -> bool {
76 env::var("COLORTERM")
77 .map(|colorterm| colorterm == "truecolor" || colorterm == "24bit")
78 .unwrap_or(false)
81pub struct App {
82 pub matches: ArgMatches<'static>,
83 interactive_output: bool,
86impl App {
87 pub fn new() -> Result<Self> {
88 #[cfg(windows)]
89 let _ = ansi_term::enable_ansi_support();
91 let interactive_output = atty::is(Stream::Stdout);
93 Ok(App {
94 matches: Self::matches(interactive_output)?,
95 interactive_output,
96 })
97 }
99 fn matches(interactive_output: bool) -> Result<ArgMatches<'static>> {
100 let args = if wild::args_os().nth(1) == Some("cache".into())
101 || wild::args_os().any(|arg| arg == "--no-config")
102 {
103 // Skip the arguments in bats config file
105 wild::args_os().collect::<Vec<_>>()
106 } else {
107 let mut cli_args = wild::args_os();
109 // Read arguments from bats config file
110 let mut args = get_args_from_env_var()
111 .unwrap_or_else(|| get_args_from_config_file())
112 .chain_err(|| "Could not parse configuration file")?;
114 // Put the zero-th CLI argument (program name) first
115 args.insert(0, cli_args.next().unwrap());
117 // .. and the rest at the end
118 cli_args.for_each(|a| args.push(a));
120 args
121 };
123 Ok(clap_app::build_app(interactive_output).get_matches_from(args))
124 }
126 pub fn config(&self) -> Result<Config> {
127 let files = self.files();
128 let output_components = self.output_components()?;
130 let paging_mode = match self.matches.value_of("paging") {
131 Some("always") => PagingMode::Always,
132 Some("never") => PagingMode::Never,
133 Some("auto") | _ => {
134 if files.contains(&InputFile::StdIn) {
135 // If we are reading from stdin, only enable paging if we write to an
136 // interactive terminal and if we do not *read* from an interactive
137 // terminal.
138 if self.interactive_output && !atty::is(Stream::Stdin) {
139 PagingMode::QuitIfOneScreen
140 } else {
141 PagingMode::Never
142 }
143 } else {
144 if self.interactive_output {
145 PagingMode::QuitIfOneScreen
146 } else {
147 PagingMode::Never
148 }
149 }
150 }
151 };
153 let mut syntax_mapping = SyntaxMapping::new();
155 if let Some(values) = self.matches.values_of("map-syntax") {
156 for from_to in values {
157 let parts: Vec<_> = from_to.split(":").collect();
159 if parts.len() != 2 {
160 return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is 'from:to'.".into());
161 }
163 syntax_mapping.insert(parts[0].into(), parts[1].into());
164 }
165 }
167 Ok(Config {
168 true_color: is_truecolor_terminal(),
169 language: self.matches.value_of("language"),
170 output_wrap: if !self.interactive_output {
171 // We don't have the tty width when piping to another program.
172 // There's no point in wrapping when this is the case.
173 OutputWrap::None
174 } else {
175 match self.matches.value_of("wrap") {
176 Some("character") => OutputWrap::Character,
177 Some("never") => OutputWrap::None,
178 Some("auto") | _ => {
179 if output_components.plain() {
180 OutputWrap::None
181 } else {
182 OutputWrap::Character
183 }
184 }
185 }
186 },
187 colored_output: match self.matches.value_of("color") {
188 Some("always") => true,
189 Some("never") => false,
190 Some("auto") | _ => self.interactive_output,
191 },
192 paging_mode,
193 term_width: self
194 .matches
195 .value_of("terminal-width")
196 .and_then(|w| w.parse().ok())
197 .unwrap_or(Term::stdout().size().1 as usize),
198 loop_through: !(self.interactive_output
199 || self.matches.value_of("color") == Some("always")
200 || self.matches.value_of("decorations") == Some("always")),
201 files,
202 tab_width: self
203 .matches
204 .value_of("tabs")
205 .map(String::from)
206 .or_else(|| env::var("BAT_TABS").ok())
207 .and_then(|t| t.parse().ok())
208 .unwrap_or(
209 if output_components.plain() && paging_mode == PagingMode::Never {
210 0
211 } else {
212 8
213 },
214 ),
215 theme: self
216 .matches
217 .value_of("theme")
218 .map(String::from)
219 .or_else(|| env::var("BAT_THEME").ok())
220 .unwrap_or(String::from(BAT_THEME_DEFAULT)),
221 line_range: transpose(self.matches.value_of("line-range").map(LineRange::from))?,
222 output_components,
223 syntax_mapping,
224 })
225 }
227 fn files(&self) -> Vec<InputFile> {
228 self.matches
229 .values_of("FILE")
230 .map(|values| {
231 values
232 .map(|filename| {
233 if filename == "-" {
234 InputFile::StdIn
235 } else {
236 InputFile::Ordinary(filename)
237 }
238 })
239 .collect()
240 })
241 .unwrap_or_else(|| vec![InputFile::StdIn])
242 }
244 fn output_components(&self) -> Result<OutputComponents> {
245 let matches = &self.matches;
246 Ok(OutputComponents(
247 if matches.value_of("decorations") == Some("never") {
248 HashSet::new()
249 } else if matches.is_present("number") {
250 [OutputComponent::Numbers].iter().cloned().collect()
251 } else if matches.is_present("plain") {
252 [OutputComponent::Plain].iter().cloned().collect()
253 } else {
254 let env_style_components: Option<Vec<OutputComponent>> =
255 transpose(env::var("BAT_STYLE").ok().map(|style_str| {
256 style_str
257 .split(",")
258 .map(|x| OutputComponent::from_str(&x))
259 .collect::<Result<Vec<OutputComponent>>>()
260 }))?;
262 values_t!(matches.values_of("style"), OutputComponent)
263 .ok()
264 .or(env_style_components)
265 .unwrap_or(vec![OutputComponent::Full])
266 .into_iter()
267 .map(|style| style.components(self.interactive_output))
268 .fold(HashSet::new(), |mut acc, components| {
269 acc.extend(components.iter().cloned());
270 acc
271 })
272 },
273 ))
274 }