on 1d9e7ac help sign in
bat/src/app.rs
431 lines 18 kB history raw
1use std::collections::HashSet;
2use std::env;
4use atty::{self, Stream};
6use clap::{App as ClapApp, AppSettings, Arg, ArgGroup, ArgMatches, SubCommand};
8use console::Term;
10#[cfg(windows)]
11use ansi_term;
13use assets::BAT_THEME_DEFAULT;
14use errors::*;
15use line_range::LineRange;
16use style::{OutputComponent, OutputComponents, OutputWrap};
18#[derive(Debug, Clone, Copy)]
19pub enum PagingMode {
20 Always,
21 QuitIfOneScreen,
22 Never,
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum InputFile<'a> {
27 StdIn,
28 Ordinary(&'a str),
29 ThemePreviewFile,
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 /// Whether or not to simply loop through all input (`cat` mode)
44 pub loop_through: bool,
46 /// Whether or not the output should be colorized
47 pub colored_output: bool,
49 /// Whether or not the output terminal supports true color
50 pub true_color: bool,
52 /// Style elements (grid, line numbers, ...)
53 pub output_components: OutputComponents,
55 /// Text wrapping mode
56 pub output_wrap: OutputWrap,
58 /// Pager or STDOUT
59 pub paging_mode: PagingMode,
61 /// The range lines that should be printed, if specified
62 pub line_range: Option<LineRange>,
64 /// The syntax highlighting theme
65 pub theme: String,
68fn is_truecolor_terminal() -> bool {
69 env::var("COLORTERM")
70 .map(|colorterm| colorterm == "truecolor" || colorterm == "24bit")
71 .unwrap_or(false)
74/// Helper function that should might appear in Rust stable at some point
75/// (https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.transpose)
76fn transpose<T>(opt: Option<Result<T>>) -> Result<Option<T>> {
77 opt.map_or(Ok(None), |res| res.map(Some))
80pub struct App {
81 pub matches: ArgMatches<'static>,
82 interactive_output: bool,
85impl App {
86 pub fn new() -> Self {
87 #[cfg(windows)]
88 let _ = ansi_term::enable_ansi_support();
90 let interactive_output = atty::is(Stream::Stdout);
92 App {
93 matches: Self::matches(interactive_output),
94 interactive_output,
95 }
96 }
98 fn matches(interactive_output: bool) -> ArgMatches<'static> {
99 let clap_color_setting = if interactive_output {
100 AppSettings::ColoredHelp
101 } else {
102 AppSettings::ColorNever
103 };
105 ClapApp::new(crate_name!())
106 .version(crate_version!())
107 .global_setting(clap_color_setting)
108 .global_setting(AppSettings::DeriveDisplayOrder)
109 .global_setting(AppSettings::UnifiedHelpMessage)
110 .global_setting(AppSettings::NextLineHelp)
111 .setting(AppSettings::InferSubcommands)
112 .setting(AppSettings::ArgsNegateSubcommands)
113 .setting(AppSettings::DisableHelpSubcommand)
114 .setting(AppSettings::VersionlessSubcommands)
115 .max_term_width(90)
116 .about(
117 "A cat(1) clone with wings.\n\n\
118 Use '--help' instead of '-h' to see a more detailed version of the help text.",
119 ).long_about("A cat(1) clone with syntax highlighting and Git integration.")
120 .arg(
121 Arg::with_name("FILE")
122 .help("File(s) to print / concatenate. Use '-' for standard input.")
123 .long_help(
124 "File(s) to print / concatenate. Use a dash ('-') or no argument at all \
125 to read from standard input.",
126 ).multiple(true)
127 .empty_values(false),
128 )
129 .arg(
130 Arg::with_name("language")
131 .short("l")
132 .long("language")
133 .overrides_with("language")
134 .help("Set the language for syntax highlighting.")
135 .long_help(
136 "Explicitly set the language for syntax highlighting. The language can be \
137 specified as a name (like 'C++' or 'LaTeX') or possible file extension \
138 (like 'cpp', 'hpp' or 'md'). Use '--list-languages' to show all supported \
139 language names and file extensions."
140 ).takes_value(true),
141 ).arg(
142 Arg::with_name("list-languages")
143 .long("list-languages")
144 .conflicts_with("list-themes")
145 .help("Display all supported languages.")
146 .long_help("Display a list of supported languages for syntax highlighting."),
147 ).arg(
148 Arg::with_name("theme")
149 .long("theme")
150 .overrides_with("theme")
151 .takes_value(true)
152 .help("Set the color theme for syntax highlighting.")
153 .long_help(
154 "Set the theme for syntax highlighting. Use '--list-themes' to \
155 see all available themes. To set a default theme, export the \
156 BAT_THEME environment variable (e.g.: export \
157 BAT_THEME=\"TwoDark\").",
158 ),
159 ).arg(
160 Arg::with_name("list-themes")
161 .long("list-themes")
162 .help("Display all supported highlighting themes.")
163 .long_help("Display a list of supported themes for syntax highlighting."),
164 ).arg(
165 Arg::with_name("style")
166 .long("style")
167 .value_name("style-components")
168 .use_delimiter(true)
169 .takes_value(true)
170 .possible_values(&[
171 "auto", "full", "plain", "changes", "header", "grid", "numbers",
172 ]).default_value("auto")
173 .help("Comma-separated list of style elements to display.")
174 .long_help(
175 "Configure which elements (line numbers, file headers, grid \
176 borders, Git modifications, ..) to display in addition to the \
177 file contents. The argument is a comma-separated list of \
178 components to display (e.g. 'numbers,changes,grid') or a \
179 pre-defined style ('full')",
180 ),
181 ).arg(
182 Arg::with_name("plain")
183 .overrides_with("plain")
184 .short("p")
185 .long("plain")
186 .conflicts_with("style")
187 .conflicts_with("number")
188 .help("Show plain style (alias for '--style=plain').")
189 .long_help(
190 "Only show plain style, no decorations. This is an alias for \
191 '--style=plain'",
192 ),
193 ).arg(
194 Arg::with_name("number")
195 .long("number")
196 .overrides_with("number")
197 .short("n")
198 .conflicts_with("style")
199 .help("Show line numbers (alias for '--style=numbers').")
200 .long_help(
201 "Only show line numbers, no other decorations. This is an alias for \
202 '--style=numbers'",
203 ),
204 ).arg(
205 Arg::with_name("line-range")
206 .long("line-range")
207 .overrides_with("line-range")
208 .takes_value(true)
209 .value_name("N:M")
210 .help("Only print the lines from N to M.")
211 .long_help(
212 "Only print the specified range of lines for each file. \
213 For example:\n \
214 '--line-range 30:40' prints lines 30 to 40\n \
215 '--line-range :40' prints lines 1 to 40\n \
216 '--line-range 40:' prints lines 40 to the end of the file",
217 ),
218 ).arg(
219 Arg::with_name("color")
220 .long("color")
221 .overrides_with("color")
222 .takes_value(true)
223 .value_name("when")
224 .possible_values(&["auto", "never", "always"])
225 .default_value("auto")
226 .help("When to use colors.")
227 .long_help("Specify when to use colored output. The automatic mode \
228 only enables colors if an interactive terminal is detected."),
229 ).arg(
230 Arg::with_name("decorations")
231 .long("decorations")
232 .overrides_with("decorations")
233 .takes_value(true)
234 .value_name("when")
235 .possible_values(&["auto", "never", "always"])
236 .default_value("auto")
237 .help("When to show the decorations specified by '--style'.")
238 .long_help("Specify when to use the decorations that have been specified \
239 via '--style'. The automatic mode only enables decorations if \
240 an interactive terminal is detected."),
241 ).arg(
242 Arg::with_name("paging")
243 .long("paging")
244 .overrides_with("paging")
245 .takes_value(true)
246 .value_name("when")
247 .possible_values(&["auto", "never", "always"])
248 .default_value("auto")
249 .help("Specify when to use the pager.")
250 .long_help("Specify when to use the pager. To control which pager \
251 is used, set the PAGER or BAT_PAGER environment \
252 variables (the latter takes precedence). The default \
253 pager is 'less'. To disable the pager permanently, set \
254 BAT_PAGER to an empty string."),
255 ).arg(
256 Arg::with_name("wrap")
257 .long("wrap")
258 .overrides_with("wrap")
259 .takes_value(true)
260 .value_name("mode")
261 .possible_values(&["character", "never"])
262 .default_value("character")
263 .help("Specify the text-wrapping mode.")
264 .long_help("Specify the text-wrapping mode."),
265 ).arg(
266 Arg::with_name("unbuffered")
267 .short("u")
268 .hidden_short_help(true)
269 .long_help(
270 "This option exists for POSIX-compliance reasons ('u' is for \
271 'unbuffered'). The output is always unbuffered - this option \
272 is simply ignored.",
273 ),
274 ).arg(
275 Arg::with_name("terminal-width")
276 .long("terminal-width")
277 .takes_value(true)
278 .value_name("width")
279 .hidden(true)
280 .help("Set the width of the terminal"),
281 ).subcommand(
282 SubCommand::with_name("cache")
283 .about("Modify the syntax-definition and theme cache")
284 .arg(
285 Arg::with_name("init")
286 .long("init")
287 .short("i")
288 .help("Initialize the syntax/theme cache.")
289 .long_help(
290 "Initialize the syntax/theme cache by loading from the \
291 source directory (default: the configuration directory).",
292 ),
293 ).arg(
294 Arg::with_name("clear")
295 .long("clear")
296 .short("c")
297 .help("Remove the cached syntax definitions and themes."),
298 ).arg(
299 Arg::with_name("config-dir")
300 .long("config-dir")
301 .short("d")
302 .help("Show bat's configuration directory."),
303 ).group(
304 ArgGroup::with_name("cache-actions")
305 .args(&["init", "clear", "config-dir"])
306 .required(true),
307 ).arg(
308 Arg::with_name("source")
309 .long("source")
310 .requires("init")
311 .takes_value(true)
312 .value_name("dir")
313 .help("Use a different directory to load syntaxes and themes from."),
314 ).arg(
315 Arg::with_name("target")
316 .long("target")
317 .requires("init")
318 .takes_value(true)
319 .value_name("dir")
320 .help(
321 "Use a different directory to store the cached syntax and theme set.",
322 ),
323 ).arg(
324 Arg::with_name("blank")
325 .long("blank")
326 .requires("init")
327 .help("Create completely new syntax and theme sets \
328 (instead of appending to the default sets).")
329 ),
330 ).help_message("Print this help message.")
331 .version_message("Show version information.")
332 .get_matches()
333 }
335 pub fn config(&self) -> Result<Config> {
336 let files = self.files();
338 Ok(Config {
339 true_color: is_truecolor_terminal(),
340 output_components: self.output_components()?,
341 language: self.matches.value_of("language"),
342 output_wrap: if !self.interactive_output {
343 // We don't have the tty width when piping to another program.
344 // There's no point in wrapping when this is the case.
345 OutputWrap::None
346 } else {
347 match self.matches.value_of("wrap") {
348 Some("character") => OutputWrap::Character,
349 Some("never") | _ => OutputWrap::None,
350 }
351 },
352 colored_output: match self.matches.value_of("color") {
353 Some("always") => true,
354 Some("never") => false,
355 Some("auto") | _ => self.interactive_output,
356 },
357 paging_mode: match self.matches.value_of("paging") {
358 Some("always") => PagingMode::Always,
359 Some("never") => PagingMode::Never,
360 Some("auto") | _ => if files.contains(&InputFile::StdIn) {
361 // If we are reading from stdin, only enable paging if we write to an
362 // interactive terminal and if we do not *read* from an interactive
363 // terminal.
364 if self.interactive_output && !atty::is(Stream::Stdin) {
365 PagingMode::QuitIfOneScreen
366 } else {
367 PagingMode::Never
368 }
369 } else {
370 if self.interactive_output {
371 PagingMode::QuitIfOneScreen
372 } else {
373 PagingMode::Never
374 }
375 },
376 },
377 term_width: self
378 .matches
379 .value_of("terminal-width")
380 .and_then(|w| w.parse().ok())
381 .unwrap_or(Term::stdout().size().1 as usize),
382 loop_through: !(self.interactive_output
383 || self.matches.value_of("color") == Some("always")
384 || self.matches.value_of("decorations") == Some("always")),
385 files,
386 theme: self
387 .matches
388 .value_of("theme")
389 .map(String::from)
390 .or_else(|| env::var("BAT_THEME").ok())
391 .unwrap_or(String::from(BAT_THEME_DEFAULT)),
392 line_range: transpose(self.matches.value_of("line-range").map(LineRange::from))?,
393 })
394 }
396 fn files(&self) -> Vec<InputFile> {
397 self.matches
398 .values_of("FILE")
399 .map(|values| {
400 values
401 .map(|filename| {
402 if filename == "-" {
403 InputFile::StdIn
404 } else {
405 InputFile::Ordinary(filename)
406 }
407 }).collect()
408 }).unwrap_or_else(|| vec![InputFile::StdIn])
409 }
411 fn output_components(&self) -> Result<OutputComponents> {
412 let matches = &self.matches;
413 Ok(OutputComponents(
414 if matches.value_of("decorations") == Some("never") {
415 HashSet::new()
416 } else if matches.is_present("number") {
417 [OutputComponent::Numbers].iter().cloned().collect()
418 } else if matches.is_present("plain") {
419 [OutputComponent::Plain].iter().cloned().collect()
420 } else {
421 values_t!(matches.values_of("style"), OutputComponent)?
422 .into_iter()
423 .map(|style| style.components(self.interactive_output))
424 .fold(HashSet::new(), |mut acc, components| {
425 acc.extend(components.iter().cloned());
426 acc
427 })
428 },
429 ))
430 }