on 89fcbb2 help sign in
bat/src/app.rs
335 lines 12 kB history raw
1use std::collections::HashSet;
2use std::env;
3use std::str::FromStr;
5use atty::{self, Stream};
7use crate::clap_app;
8use clap::ArgMatches;
9use wild;
11use console::Term;
13#[cfg(windows)]
14use ansi_term;
16use crate::assets::BAT_THEME_DEFAULT;
17use crate::config::{get_args_from_config_file, get_args_from_env_var};
18use crate::errors::*;
19use crate::inputfile::InputFile;
20use crate::line_range::{LineRange, LineRanges};
21use crate::style::{OutputComponent, OutputComponents, OutputWrap};
22use crate::syntax_mapping::SyntaxMapping;
23use crate::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 /// Whether or not to show/replace non-printable characters like space, tab and newline.
41 pub show_nonprintable: bool,
43 /// The character width of the terminal
44 pub term_width: usize,
46 /// The width of tab characters.
47 /// Currently, a value of 0 will cause tabs to be passed through without expanding them.
48 pub tab_width: usize,
50 /// Whether or not to simply loop through all input (`cat` mode)
51 pub loop_through: bool,
53 /// Whether or not the output should be colorized
54 pub colored_output: bool,
56 /// Whether or not the output terminal supports true color
57 pub true_color: bool,
59 /// Style elements (grid, line numbers, ...)
60 pub output_components: OutputComponents,
62 /// Text wrapping mode
63 pub output_wrap: OutputWrap,
65 /// Pager or STDOUT
66 pub paging_mode: PagingMode,
68 /// Specifies the lines that should be printed
69 pub line_ranges: LineRanges,
71 /// The syntax highlighting theme
72 pub theme: String,
74 /// File extension/name mappings
75 pub syntax_mapping: SyntaxMapping,
77 /// Command to start the pager
78 pub pager: Option<&'a str>,
80 /// Whether or not to use ANSI italics
81 pub use_italic_text: bool,
83 /// Lines to highlight
84 pub highlight_lines: Vec<usize>,
87fn is_truecolor_terminal() -> bool {
88 env::var("COLORTERM")
89 .map(|colorterm| colorterm == "truecolor" || colorterm == "24bit")
90 .unwrap_or(false)
93pub struct App {
94 pub matches: ArgMatches<'static>,
95 interactive_output: bool,
98impl App {
99 pub fn new() -> Result<Self> {
100 #[cfg(windows)]
101 let _ = ansi_term::enable_ansi_support();
103 let interactive_output = atty::is(Stream::Stdout);
105 Ok(App {
106 matches: Self::matches(interactive_output)?,
107 interactive_output,
108 })
109 }
111 fn matches(interactive_output: bool) -> Result<ArgMatches<'static>> {
112 let args = if wild::args_os().nth(1) == Some("cache".into())
113 || wild::args_os().any(|arg| arg == "--no-config")
114 {
115 // Skip the arguments in bats config file
117 wild::args_os().collect::<Vec<_>>()
118 } else {
119 let mut cli_args = wild::args_os();
121 // Read arguments from bats config file
122 let mut args = get_args_from_env_var()
123 .unwrap_or_else(get_args_from_config_file)
124 .chain_err(|| "Could not parse configuration file")?;
126 // Put the zero-th CLI argument (program name) first
127 args.insert(0, cli_args.next().unwrap());
129 // .. and the rest at the end
130 cli_args.for_each(|a| args.push(a));
132 args
133 };
135 Ok(clap_app::build_app(interactive_output).get_matches_from(args))
136 }
138 pub fn config(&self) -> Result<Config> {
139 let files = self.files();
140 let output_components = self.output_components()?;
142 let paging_mode = match self.matches.value_of("paging") {
143 Some("always") => PagingMode::Always,
144 Some("never") => PagingMode::Never,
145 Some("auto") | _ => {
146 if self.matches.occurrences_of("plain") > 1 {
147 // If we have -pp as an option when in auto mode, the pager should be disabled.
148 PagingMode::Never
149 } else if files.contains(&InputFile::StdIn) {
150 // If we are reading from stdin, only enable paging if we write to an
151 // interactive terminal and if we do not *read* from an interactive
152 // terminal.
153 if self.interactive_output && !atty::is(Stream::Stdin) {
154 PagingMode::QuitIfOneScreen
155 } else {
156 PagingMode::Never
157 }
158 } else if self.interactive_output {
159 PagingMode::QuitIfOneScreen
160 } else {
161 PagingMode::Never
162 }
163 }
164 };
166 let mut syntax_mapping = SyntaxMapping::new();
168 if let Some(values) = self.matches.values_of("map-syntax") {
169 for from_to in values {
170 let parts: Vec<_> = from_to.split(':').collect();
172 if parts.len() != 2 {
173 return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is 'from:to'.".into());
174 }
176 syntax_mapping.insert(parts[0], parts[1]);
177 }
178 }
180 Ok(Config {
181 true_color: is_truecolor_terminal(),
182 language: self.matches.value_of("language").or_else(|| {
183 if self.matches.is_present("show-all") {
184 Some("show-nonprintable")
185 } else {
186 None
187 }
188 }),
189 show_nonprintable: self.matches.is_present("show-all"),
190 output_wrap: if !self.interactive_output {
191 // We don't have the tty width when piping to another program.
192 // There's no point in wrapping when this is the case.
193 OutputWrap::None
194 } else {
195 match self.matches.value_of("wrap") {
196 Some("character") => OutputWrap::Character,
197 Some("never") => OutputWrap::None,
198 Some("auto") | _ => {
199 if output_components.plain() {
200 OutputWrap::None
201 } else {
202 OutputWrap::Character
203 }
204 }
205 }
206 },
207 colored_output: match self.matches.value_of("color") {
208 Some("always") => true,
209 Some("never") => false,
210 Some("auto") | _ => self.interactive_output,
211 },
212 paging_mode,
213 term_width: self
214 .matches
215 .value_of("terminal-width")
216 .and_then(|w| {
217 if w.starts_with('+') || w.starts_with('-') {
218 // Treat argument as a delta to the current terminal width
219 w.parse().ok().map(|delta: i16| {
220 let old_width: u16 = Term::stdout().size().1;
221 let new_width: i32 = i32::from(old_width) + i32::from(delta);
223 if new_width <= 0 {
224 old_width as usize
225 } else {
226 new_width as usize
227 }
228 })
229 } else {
230 w.parse().ok()
231 }
232 })
233 .unwrap_or(Term::stdout().size().1 as usize),
234 loop_through: !(self.interactive_output
235 || self.matches.value_of("color") == Some("always")
236 || self.matches.value_of("decorations") == Some("always")),
237 files,
238 tab_width: self
239 .matches
240 .value_of("tabs")
241 .map(String::from)
242 .or_else(|| env::var("BAT_TABS").ok())
243 .and_then(|t| t.parse().ok())
244 .unwrap_or(
245 if output_components.plain() && paging_mode == PagingMode::Never {
246 0
247 } else {
248 4
249 },
250 ),
251 theme: self
252 .matches
253 .value_of("theme")
254 .map(String::from)
255 .or_else(|| env::var("BAT_THEME").ok())
256 .unwrap_or_else(|| String::from(BAT_THEME_DEFAULT)),
257 line_ranges: LineRanges::from(
258 transpose(
259 self.matches
260 .values_of("line-range")
261 .map(|vs| vs.map(LineRange::from).collect()),
262 )?
263 .unwrap_or_else(|| vec![]),
264 ),
265 output_components,
266 syntax_mapping,
267 pager: self.matches.value_of("pager"),
268 use_italic_text: match self.matches.value_of("italic-text") {
269 Some("always") => true,
270 _ => false,
271 },
272 highlight_lines: self
273 .matches
274 .values_of("highlight-line")
275 .and_then(|ws| ws.map(|w| w.parse().ok()).collect())
276 .unwrap_or_default(),
277 })
278 }
280 fn files(&self) -> Vec<InputFile> {
281 self.matches
282 .values_of("FILE")
283 .map(|values| {
284 values
285 .map(|filename| {
286 if filename == "-" {
287 InputFile::StdIn
288 } else {
289 InputFile::Ordinary(filename)
290 }
291 })
292 .collect()
293 })
294 .unwrap_or_else(|| vec![InputFile::StdIn])
295 }
297 fn output_components(&self) -> Result<OutputComponents> {
298 let matches = &self.matches;
299 Ok(OutputComponents(
300 if matches.value_of("decorations") == Some("never") {
301 HashSet::new()
302 } else if matches.is_present("number") {
303 [OutputComponent::Numbers].iter().cloned().collect()
304 } else if matches.is_present("plain") {
305 [OutputComponent::Plain].iter().cloned().collect()
306 } else {
307 let env_style_components: Option<Vec<OutputComponent>> =
308 transpose(env::var("BAT_STYLE").ok().map(|style_str| {
309 style_str
310 .split(',')
311 .map(|x| OutputComponent::from_str(&x))
312 .collect::<Result<Vec<OutputComponent>>>()
313 }))?;
315 matches
316 .value_of("style")
317 .map(|styles| {
318 styles
319 .split(',')
320 .map(|style| style.parse::<OutputComponent>())
321 .filter_map(|style| style.ok())
322 .collect::<Vec<_>>()
323 })
324 .or(env_style_components)
325 .unwrap_or_else(|| vec![OutputComponent::Full])
326 .into_iter()
327 .map(|style| style.components(self.interactive_output))
328 .fold(HashSet::new(), |mut acc, components| {
329 acc.extend(components.iter().cloned());
330 acc
331 })
332 },
333 ))
334 }