on 3a89486 help sign in
bat/src/app.rs
420 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 let interactive_output = atty::is(Stream::Stdout);
89 #[cfg(windows)]
90 let interactive_output = interactive_output && ansi_term::enable_ansi_support().is_ok();
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 ).subcommand(
275 SubCommand::with_name("cache")
276 .about("Modify the syntax-definition and theme cache")
277 .arg(
278 Arg::with_name("init")
279 .long("init")
280 .short("i")
281 .help("Initialize the syntax/theme cache.")
282 .long_help(
283 "Initialize the syntax/theme cache by loading from the \
284 source directory (default: the configuration directory).",
285 ),
286 ).arg(
287 Arg::with_name("clear")
288 .long("clear")
289 .short("c")
290 .help("Remove the cached syntax definitions and themes."),
291 ).arg(
292 Arg::with_name("config-dir")
293 .long("config-dir")
294 .short("d")
295 .help("Show bat's configuration directory."),
296 ).group(
297 ArgGroup::with_name("cache-actions")
298 .args(&["init", "clear", "config-dir"])
299 .required(true),
300 ).arg(
301 Arg::with_name("source")
302 .long("source")
303 .requires("init")
304 .takes_value(true)
305 .value_name("dir")
306 .help("Use a different directory to load syntaxes and themes from."),
307 ).arg(
308 Arg::with_name("target")
309 .long("target")
310 .requires("init")
311 .takes_value(true)
312 .value_name("dir")
313 .help(
314 "Use a different directory to store the cached syntax and theme set.",
315 ),
316 ).arg(
317 Arg::with_name("blank")
318 .long("blank")
319 .requires("init")
320 .help("Create completely new syntax and theme sets \
321 (instead of appending to the default sets).")
322 ),
323 ).help_message("Print this help message.")
324 .version_message("Show version information.")
325 .get_matches()
326 }
328 pub fn config(&self) -> Result<Config> {
329 let files = self.files();
331 Ok(Config {
332 true_color: is_truecolor_terminal(),
333 output_components: self.output_components()?,
334 language: self.matches.value_of("language"),
335 output_wrap: if !self.interactive_output {
336 // We don't have the tty width when piping to another program.
337 // There's no point in wrapping when this is the case.
338 OutputWrap::None
339 } else {
340 match self.matches.value_of("wrap") {
341 Some("character") => OutputWrap::Character,
342 Some("never") | _ => OutputWrap::None,
343 }
344 },
345 colored_output: match self.matches.value_of("color") {
346 Some("always") => true,
347 Some("never") => false,
348 Some("auto") | _ => self.interactive_output,
349 },
350 paging_mode: match self.matches.value_of("paging") {
351 Some("always") => PagingMode::Always,
352 Some("never") => PagingMode::Never,
353 Some("auto") | _ => if files.contains(&InputFile::StdIn) {
354 // If we are reading from stdin, only enable paging if we write to an
355 // interactive terminal and if we do not *read* from an interactive
356 // terminal.
357 if self.interactive_output && !atty::is(Stream::Stdin) {
358 PagingMode::QuitIfOneScreen
359 } else {
360 PagingMode::Never
361 }
362 } else {
363 if self.interactive_output {
364 PagingMode::QuitIfOneScreen
365 } else {
366 PagingMode::Never
367 }
368 },
369 },
370 term_width: Term::stdout().size().1 as usize,
371 loop_through: !(self.interactive_output
372 || self.matches.value_of("color") == Some("always")
373 || self.matches.value_of("decorations") == Some("always")),
374 files,
375 theme: self
376 .matches
377 .value_of("theme")
378 .map(String::from)
379 .or_else(|| env::var("BAT_THEME").ok())
380 .unwrap_or(String::from(BAT_THEME_DEFAULT)),
381 line_range: transpose(self.matches.value_of("line-range").map(LineRange::from))?,
382 })
383 }
385 fn files(&self) -> Vec<InputFile> {
386 self.matches
387 .values_of("FILE")
388 .map(|values| {
389 values
390 .map(|filename| {
391 if filename == "-" {
392 InputFile::StdIn
393 } else {
394 InputFile::Ordinary(filename)
395 }
396 }).collect()
397 }).unwrap_or_else(|| vec![InputFile::StdIn])
398 }
400 fn output_components(&self) -> Result<OutputComponents> {
401 let matches = &self.matches;
402 Ok(OutputComponents(
403 if matches.value_of("decorations") == Some("never") {
404 HashSet::new()
405 } else if matches.is_present("number") {
406 [OutputComponent::Numbers].iter().cloned().collect()
407 } else if matches.is_present("plain") {
408 [OutputComponent::Plain].iter().cloned().collect()
409 } else {
410 values_t!(matches.values_of("style"), OutputComponent)?
411 .into_iter()
412 .map(|style| style.components(self.interactive_output))
413 .fold(HashSet::new(), |mut acc, components| {
414 acc.extend(components.iter().cloned());
415 acc
416 })
417 },
418 ))
419 }