on 159f234 help sign in
bat/src/app.rs
325 lines 11 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, LineRanges};
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 /// 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 pub use_italic_text: bool,
83fn is_truecolor_terminal() -> bool {
84 env::var("COLORTERM")
85 .map(|colorterm| colorterm == "truecolor" || colorterm == "24bit")
86 .unwrap_or(false)
89pub struct App {
90 pub matches: ArgMatches<'static>,
91 interactive_output: bool,
94impl App {
95 pub fn new() -> Result<Self> {
96 #[cfg(windows)]
97 let _ = ansi_term::enable_ansi_support();
99 let interactive_output = atty::is(Stream::Stdout);
101 Ok(App {
102 matches: Self::matches(interactive_output)?,
103 interactive_output,
104 })
105 }
107 fn matches(interactive_output: bool) -> Result<ArgMatches<'static>> {
108 let args = if wild::args_os().nth(1) == Some("cache".into())
109 || wild::args_os().any(|arg| arg == "--no-config")
110 {
111 // Skip the arguments in bats config file
113 wild::args_os().collect::<Vec<_>>()
114 } else {
115 let mut cli_args = wild::args_os();
117 // Read arguments from bats config file
118 let mut args = get_args_from_env_var()
119 .unwrap_or_else(|| get_args_from_config_file())
120 .chain_err(|| "Could not parse configuration file")?;
122 // Put the zero-th CLI argument (program name) first
123 args.insert(0, cli_args.next().unwrap());
125 // .. and the rest at the end
126 cli_args.for_each(|a| args.push(a));
128 args
129 };
131 Ok(clap_app::build_app(interactive_output).get_matches_from(args))
132 }
134 pub fn config(&self) -> Result<Config> {
135 let files = self.files();
136 let output_components = self.output_components()?;
138 let paging_mode = match self.matches.value_of("paging") {
139 Some("always") => PagingMode::Always,
140 Some("never") => PagingMode::Never,
141 Some("auto") | _ => {
142 if files.contains(&InputFile::StdIn) {
143 // If we are reading from stdin, only enable paging if we write to an
144 // interactive terminal and if we do not *read* from an interactive
145 // terminal.
146 if self.interactive_output && !atty::is(Stream::Stdin) {
147 PagingMode::QuitIfOneScreen
148 } else {
149 PagingMode::Never
150 }
151 } else {
152 if self.interactive_output {
153 PagingMode::QuitIfOneScreen
154 } else {
155 PagingMode::Never
156 }
157 }
158 }
159 };
161 let mut syntax_mapping = SyntaxMapping::new();
163 if let Some(values) = self.matches.values_of("map-syntax") {
164 for from_to in values {
165 let parts: Vec<_> = from_to.split(":").collect();
167 if parts.len() != 2 {
168 return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is 'from:to'.".into());
169 }
171 syntax_mapping.insert(parts[0].into(), parts[1].into());
172 }
173 }
175 Ok(Config {
176 true_color: is_truecolor_terminal(),
177 language: self.matches.value_of("language").or_else(|| {
178 if self.matches.is_present("show-all") {
179 Some("show-nonprintable")
180 } else {
181 None
182 }
183 }),
184 show_nonprintable: self.matches.is_present("show-all"),
185 output_wrap: if !self.interactive_output {
186 // We don't have the tty width when piping to another program.
187 // There's no point in wrapping when this is the case.
188 OutputWrap::None
189 } else {
190 match self.matches.value_of("wrap") {
191 Some("character") => OutputWrap::Character,
192 Some("never") => OutputWrap::None,
193 Some("auto") | _ => {
194 if output_components.plain() {
195 OutputWrap::None
196 } else {
197 OutputWrap::Character
198 }
199 }
200 }
201 },
202 colored_output: match self.matches.value_of("color") {
203 Some("always") => true,
204 Some("never") => false,
205 Some("auto") | _ => self.interactive_output,
206 },
207 paging_mode,
208 term_width: self
209 .matches
210 .value_of("terminal-width")
211 .and_then(|w| {
212 if w.starts_with("+") || w.starts_with("-") {
213 // Treat argument as a delta to the current terminal width
214 w.parse().ok().map(|delta: i16| {
215 let old_width: u16 = Term::stdout().size().1;
216 let new_width: i32 = old_width as i32 + delta as i32;
218 if new_width <= 0 {
219 old_width as usize
220 } else {
221 new_width as usize
222 }
223 })
224 } else {
225 w.parse().ok()
226 }
227 })
228 .unwrap_or(Term::stdout().size().1 as usize),
229 loop_through: !(self.interactive_output
230 || self.matches.value_of("color") == Some("always")
231 || self.matches.value_of("decorations") == Some("always")),
232 files,
233 tab_width: self
234 .matches
235 .value_of("tabs")
236 .map(String::from)
237 .or_else(|| env::var("BAT_TABS").ok())
238 .and_then(|t| t.parse().ok())
239 .unwrap_or(
240 if output_components.plain() && paging_mode == PagingMode::Never {
241 0
242 } else {
243 4
244 },
245 ),
246 theme: self
247 .matches
248 .value_of("theme")
249 .map(String::from)
250 .or_else(|| env::var("BAT_THEME").ok())
251 .unwrap_or(String::from(BAT_THEME_DEFAULT)),
252 line_ranges: LineRanges::from(
253 transpose(
254 self.matches
255 .values_of("line-range")
256 .map(|vs| vs.map(LineRange::from).collect()),
257 )?
258 .unwrap_or(vec![]),
259 ),
260 output_components,
261 syntax_mapping,
262 pager: self.matches.value_of("pager"),
263 use_italic_text: match self.matches.value_of("italic-text") {
264 Some("always") => true,
265 _ => false,
266 },
267 })
268 }
270 fn files(&self) -> Vec<InputFile> {
271 self.matches
272 .values_of("FILE")
273 .map(|values| {
274 values
275 .map(|filename| {
276 if filename == "-" {
277 InputFile::StdIn
278 } else {
279 InputFile::Ordinary(filename)
280 }
281 })
282 .collect()
283 })
284 .unwrap_or_else(|| vec![InputFile::StdIn])
285 }
287 fn output_components(&self) -> Result<OutputComponents> {
288 let matches = &self.matches;
289 Ok(OutputComponents(
290 if matches.value_of("decorations") == Some("never") {
291 HashSet::new()
292 } else if matches.is_present("number") {
293 [OutputComponent::Numbers].iter().cloned().collect()
294 } else if matches.is_present("plain") {
295 [OutputComponent::Plain].iter().cloned().collect()
296 } else {
297 let env_style_components: Option<Vec<OutputComponent>> =
298 transpose(env::var("BAT_STYLE").ok().map(|style_str| {
299 style_str
300 .split(",")
301 .map(|x| OutputComponent::from_str(&x))
302 .collect::<Result<Vec<OutputComponent>>>()
303 }))?;
305 matches
306 .value_of("style")
307 .map(|styles| {
308 styles
309 .split(",")
310 .map(|style| style.parse::<OutputComponent>())
311 .filter_map(|style| style.ok())
312 .collect::<Vec<_>>()
313 })
314 .or(env_style_components)
315 .unwrap_or(vec![OutputComponent::Full])
316 .into_iter()
317 .map(|style| style.components(self.interactive_output))
318 .fold(HashSet::new(), |mut acc, components| {
319 acc.extend(components.iter().cloned());
320 acc
321 })
322 },
323 ))
324 }