mirror of
https://github.com/sharkdp/bat
synced 2026-07-29 18:01:43 +00:00
Move {main,app,clap_app}.rs into src/bin/
This commit is contained in:
committed by
David Peter
parent
f03b45f3e5
commit
fc0ad4db2e
+335
@@ -0,0 +1,335 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
|
||||
use atty::{self, Stream};
|
||||
|
||||
use crate::clap_app;
|
||||
use clap::ArgMatches;
|
||||
use wild;
|
||||
|
||||
use console::Term;
|
||||
|
||||
#[cfg(windows)]
|
||||
use ansi_term;
|
||||
|
||||
use crate::assets::BAT_THEME_DEFAULT;
|
||||
use crate::config::{get_args_from_config_file, get_args_from_env_var};
|
||||
use crate::errors::*;
|
||||
use crate::inputfile::InputFile;
|
||||
use crate::line_range::{LineRange, LineRanges};
|
||||
use crate::style::{OutputComponent, OutputComponents, OutputWrap};
|
||||
use crate::syntax_mapping::SyntaxMapping;
|
||||
use crate::util::transpose;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum PagingMode {
|
||||
Always,
|
||||
QuitIfOneScreen,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config<'a> {
|
||||
/// List of files to print
|
||||
pub files: Vec<InputFile<'a>>,
|
||||
|
||||
/// The explicitly configured language, if any
|
||||
pub language: Option<&'a str>,
|
||||
|
||||
/// Whether or not to show/replace non-printable characters like space, tab and newline.
|
||||
pub show_nonprintable: bool,
|
||||
|
||||
/// The character width of the terminal
|
||||
pub term_width: usize,
|
||||
|
||||
/// The width of tab characters.
|
||||
/// Currently, a value of 0 will cause tabs to be passed through without expanding them.
|
||||
pub tab_width: usize,
|
||||
|
||||
/// Whether or not to simply loop through all input (`cat` mode)
|
||||
pub loop_through: bool,
|
||||
|
||||
/// Whether or not the output should be colorized
|
||||
pub colored_output: bool,
|
||||
|
||||
/// Whether or not the output terminal supports true color
|
||||
pub true_color: bool,
|
||||
|
||||
/// Style elements (grid, line numbers, ...)
|
||||
pub output_components: OutputComponents,
|
||||
|
||||
/// Text wrapping mode
|
||||
pub output_wrap: OutputWrap,
|
||||
|
||||
/// Pager or STDOUT
|
||||
pub paging_mode: PagingMode,
|
||||
|
||||
/// Specifies the lines that should be printed
|
||||
pub line_ranges: LineRanges,
|
||||
|
||||
/// The syntax highlighting theme
|
||||
pub theme: String,
|
||||
|
||||
/// File extension/name mappings
|
||||
pub syntax_mapping: SyntaxMapping,
|
||||
|
||||
/// Command to start the pager
|
||||
pub pager: Option<&'a str>,
|
||||
|
||||
/// Whether or not to use ANSI italics
|
||||
pub use_italic_text: bool,
|
||||
|
||||
/// Lines to highlight
|
||||
pub highlight_lines: Vec<usize>,
|
||||
}
|
||||
|
||||
fn is_truecolor_terminal() -> bool {
|
||||
env::var("COLORTERM")
|
||||
.map(|colorterm| colorterm == "truecolor" || colorterm == "24bit")
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub matches: ArgMatches<'static>,
|
||||
interactive_output: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Result<Self> {
|
||||
#[cfg(windows)]
|
||||
let _ = ansi_term::enable_ansi_support();
|
||||
|
||||
let interactive_output = atty::is(Stream::Stdout);
|
||||
|
||||
Ok(App {
|
||||
matches: Self::matches(interactive_output)?,
|
||||
interactive_output,
|
||||
})
|
||||
}
|
||||
|
||||
fn matches(interactive_output: bool) -> Result<ArgMatches<'static>> {
|
||||
let args = if wild::args_os().nth(1) == Some("cache".into())
|
||||
|| wild::args_os().any(|arg| arg == "--no-config")
|
||||
{
|
||||
// Skip the arguments in bats config file
|
||||
|
||||
wild::args_os().collect::<Vec<_>>()
|
||||
} else {
|
||||
let mut cli_args = wild::args_os();
|
||||
|
||||
// Read arguments from bats config file
|
||||
let mut args = get_args_from_env_var()
|
||||
.unwrap_or_else(get_args_from_config_file)
|
||||
.chain_err(|| "Could not parse configuration file")?;
|
||||
|
||||
// Put the zero-th CLI argument (program name) first
|
||||
args.insert(0, cli_args.next().unwrap());
|
||||
|
||||
// .. and the rest at the end
|
||||
cli_args.for_each(|a| args.push(a));
|
||||
|
||||
args
|
||||
};
|
||||
|
||||
Ok(clap_app::build_app(interactive_output).get_matches_from(args))
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Result<Config> {
|
||||
let files = self.files();
|
||||
let output_components = self.output_components()?;
|
||||
|
||||
let paging_mode = match self.matches.value_of("paging") {
|
||||
Some("always") => PagingMode::Always,
|
||||
Some("never") => PagingMode::Never,
|
||||
Some("auto") | _ => {
|
||||
if self.matches.occurrences_of("plain") > 1 {
|
||||
// If we have -pp as an option when in auto mode, the pager should be disabled.
|
||||
PagingMode::Never
|
||||
} else if files.contains(&InputFile::StdIn) {
|
||||
// If we are reading from stdin, only enable paging if we write to an
|
||||
// interactive terminal and if we do not *read* from an interactive
|
||||
// terminal.
|
||||
if self.interactive_output && !atty::is(Stream::Stdin) {
|
||||
PagingMode::QuitIfOneScreen
|
||||
} else {
|
||||
PagingMode::Never
|
||||
}
|
||||
} else if self.interactive_output {
|
||||
PagingMode::QuitIfOneScreen
|
||||
} else {
|
||||
PagingMode::Never
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut syntax_mapping = SyntaxMapping::new();
|
||||
|
||||
if let Some(values) = self.matches.values_of("map-syntax") {
|
||||
for from_to in values {
|
||||
let parts: Vec<_> = from_to.split(':').collect();
|
||||
|
||||
if parts.len() != 2 {
|
||||
return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is 'from:to'.".into());
|
||||
}
|
||||
|
||||
syntax_mapping.insert(parts[0], parts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Config {
|
||||
true_color: is_truecolor_terminal(),
|
||||
language: self.matches.value_of("language").or_else(|| {
|
||||
if self.matches.is_present("show-all") {
|
||||
Some("show-nonprintable")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
show_nonprintable: self.matches.is_present("show-all"),
|
||||
output_wrap: if !self.interactive_output {
|
||||
// We don't have the tty width when piping to another program.
|
||||
// There's no point in wrapping when this is the case.
|
||||
OutputWrap::None
|
||||
} else {
|
||||
match self.matches.value_of("wrap") {
|
||||
Some("character") => OutputWrap::Character,
|
||||
Some("never") => OutputWrap::None,
|
||||
Some("auto") | _ => {
|
||||
if output_components.plain() {
|
||||
OutputWrap::None
|
||||
} else {
|
||||
OutputWrap::Character
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
colored_output: match self.matches.value_of("color") {
|
||||
Some("always") => true,
|
||||
Some("never") => false,
|
||||
Some("auto") | _ => self.interactive_output,
|
||||
},
|
||||
paging_mode,
|
||||
term_width: self
|
||||
.matches
|
||||
.value_of("terminal-width")
|
||||
.and_then(|w| {
|
||||
if w.starts_with('+') || w.starts_with('-') {
|
||||
// Treat argument as a delta to the current terminal width
|
||||
w.parse().ok().map(|delta: i16| {
|
||||
let old_width: u16 = Term::stdout().size().1;
|
||||
let new_width: i32 = i32::from(old_width) + i32::from(delta);
|
||||
|
||||
if new_width <= 0 {
|
||||
old_width as usize
|
||||
} else {
|
||||
new_width as usize
|
||||
}
|
||||
})
|
||||
} else {
|
||||
w.parse().ok()
|
||||
}
|
||||
})
|
||||
.unwrap_or(Term::stdout().size().1 as usize),
|
||||
loop_through: !(self.interactive_output
|
||||
|| self.matches.value_of("color") == Some("always")
|
||||
|| self.matches.value_of("decorations") == Some("always")),
|
||||
files,
|
||||
tab_width: self
|
||||
.matches
|
||||
.value_of("tabs")
|
||||
.map(String::from)
|
||||
.or_else(|| env::var("BAT_TABS").ok())
|
||||
.and_then(|t| t.parse().ok())
|
||||
.unwrap_or(
|
||||
if output_components.plain() && paging_mode == PagingMode::Never {
|
||||
0
|
||||
} else {
|
||||
4
|
||||
},
|
||||
),
|
||||
theme: self
|
||||
.matches
|
||||
.value_of("theme")
|
||||
.map(String::from)
|
||||
.or_else(|| env::var("BAT_THEME").ok())
|
||||
.unwrap_or_else(|| String::from(BAT_THEME_DEFAULT)),
|
||||
line_ranges: LineRanges::from(
|
||||
transpose(
|
||||
self.matches
|
||||
.values_of("line-range")
|
||||
.map(|vs| vs.map(LineRange::from).collect()),
|
||||
)?
|
||||
.unwrap_or_else(|| vec![]),
|
||||
),
|
||||
output_components,
|
||||
syntax_mapping,
|
||||
pager: self.matches.value_of("pager"),
|
||||
use_italic_text: match self.matches.value_of("italic-text") {
|
||||
Some("always") => true,
|
||||
_ => false,
|
||||
},
|
||||
highlight_lines: self
|
||||
.matches
|
||||
.values_of("highlight-line")
|
||||
.and_then(|ws| ws.map(|w| w.parse().ok()).collect())
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn files(&self) -> Vec<InputFile> {
|
||||
self.matches
|
||||
.values_of("FILE")
|
||||
.map(|values| {
|
||||
values
|
||||
.map(|filename| {
|
||||
if filename == "-" {
|
||||
InputFile::StdIn
|
||||
} else {
|
||||
InputFile::Ordinary(filename)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(|| vec![InputFile::StdIn])
|
||||
}
|
||||
|
||||
fn output_components(&self) -> Result<OutputComponents> {
|
||||
let matches = &self.matches;
|
||||
Ok(OutputComponents(
|
||||
if matches.value_of("decorations") == Some("never") {
|
||||
HashSet::new()
|
||||
} else if matches.is_present("number") {
|
||||
[OutputComponent::Numbers].iter().cloned().collect()
|
||||
} else if matches.is_present("plain") {
|
||||
[OutputComponent::Plain].iter().cloned().collect()
|
||||
} else {
|
||||
let env_style_components: Option<Vec<OutputComponent>> =
|
||||
transpose(env::var("BAT_STYLE").ok().map(|style_str| {
|
||||
style_str
|
||||
.split(',')
|
||||
.map(|x| OutputComponent::from_str(&x))
|
||||
.collect::<Result<Vec<OutputComponent>>>()
|
||||
}))?;
|
||||
|
||||
matches
|
||||
.value_of("style")
|
||||
.map(|styles| {
|
||||
styles
|
||||
.split(',')
|
||||
.map(|style| style.parse::<OutputComponent>())
|
||||
.filter_map(|style| style.ok())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.or(env_style_components)
|
||||
.unwrap_or_else(|| vec![OutputComponent::Full])
|
||||
.into_iter()
|
||||
.map(|style| style.components(self.interactive_output))
|
||||
.fold(HashSet::new(), |mut acc, components| {
|
||||
acc.extend(components.iter().cloned());
|
||||
acc
|
||||
})
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
use clap::{App as ClapApp, AppSettings, Arg, ArgGroup, SubCommand};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
|
||||
let clap_color_setting = if interactive_output {
|
||||
AppSettings::ColoredHelp
|
||||
} else {
|
||||
AppSettings::ColorNever
|
||||
};
|
||||
|
||||
let app = ClapApp::new(crate_name!())
|
||||
.version(crate_version!())
|
||||
.global_setting(clap_color_setting)
|
||||
.global_setting(AppSettings::DeriveDisplayOrder)
|
||||
.global_setting(AppSettings::UnifiedHelpMessage)
|
||||
.global_setting(AppSettings::HidePossibleValuesInHelp)
|
||||
.setting(AppSettings::ArgsNegateSubcommands)
|
||||
.setting(AppSettings::AllowExternalSubcommands)
|
||||
.setting(AppSettings::DisableHelpSubcommand)
|
||||
.setting(AppSettings::VersionlessSubcommands)
|
||||
.max_term_width(100)
|
||||
.about(
|
||||
"A cat(1) clone with wings.\n\n\
|
||||
Use '--help' instead of '-h' to see a more detailed version of the help text.",
|
||||
)
|
||||
.long_about("A cat(1) clone with syntax highlighting and Git integration.")
|
||||
.arg(
|
||||
Arg::with_name("FILE")
|
||||
.help("File(s) to print / concatenate. Use '-' for standard input.")
|
||||
.long_help(
|
||||
"File(s) to print / concatenate. Use a dash ('-') or no argument at all \
|
||||
to read from standard input.",
|
||||
)
|
||||
.multiple(true)
|
||||
.empty_values(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("language")
|
||||
.short("l")
|
||||
.long("language")
|
||||
.overrides_with("language")
|
||||
.help("Set the language for syntax highlighting.")
|
||||
.long_help(
|
||||
"Explicitly set the language for syntax highlighting. The language can be \
|
||||
specified as a name (like 'C++' or 'LaTeX') or possible file extension \
|
||||
(like 'cpp', 'hpp' or 'md'). Use '--list-languages' to show all supported \
|
||||
language names and file extensions.",
|
||||
)
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("list-languages")
|
||||
.long("list-languages")
|
||||
.short("L")
|
||||
.conflicts_with("list-themes")
|
||||
.help("Display all supported languages.")
|
||||
.long_help("Display a list of supported languages for syntax highlighting."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("map-syntax")
|
||||
.short("m")
|
||||
.long("map-syntax")
|
||||
.multiple(true)
|
||||
.takes_value(true)
|
||||
.number_of_values(1)
|
||||
.value_name("from:to")
|
||||
.help("Map a file extension or name to an existing syntax.")
|
||||
.long_help(
|
||||
"Map a file extension or file name to an existing syntax. For example, \
|
||||
to highlight *.conf files with the INI syntax, use '-m conf:ini'. \
|
||||
To highlight files named '.myignore' with the Git Ignore syntax, use \
|
||||
'-m .myignore:gitignore'.",
|
||||
)
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("theme")
|
||||
.long("theme")
|
||||
.overrides_with("theme")
|
||||
.takes_value(true)
|
||||
.help("Set the color theme for syntax highlighting.")
|
||||
.long_help(
|
||||
"Set the theme for syntax highlighting. Use '--list-themes' to \
|
||||
see all available themes. To set a default theme, add the \
|
||||
'--theme=\"...\"' option to the configuration file or export the \
|
||||
BAT_THEME environment variable (e.g.: export \
|
||||
BAT_THEME=\"...\").",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("list-themes")
|
||||
.long("list-themes")
|
||||
.help("Display all supported highlighting themes.")
|
||||
.long_help("Display a list of supported themes for syntax highlighting."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("style")
|
||||
.long("style")
|
||||
.value_name("style-components")
|
||||
// Need to turn this off for overrides_with to work as we want. See the bottom most
|
||||
// example at https://docs.rs/clap/2.32.0/clap/struct.Arg.html#method.overrides_with
|
||||
.use_delimiter(false)
|
||||
.takes_value(true)
|
||||
.overrides_with("style")
|
||||
.overrides_with("plain")
|
||||
.overrides_with("number")
|
||||
// Cannot use clap's built in validation because we have to turn off clap's delimiters
|
||||
.validator(|val| {
|
||||
let mut invalid_vals = val.split(',').filter(|style| {
|
||||
!&[
|
||||
"auto", "full", "plain", "changes", "header", "grid", "numbers", "snip"
|
||||
]
|
||||
.contains(style)
|
||||
});
|
||||
|
||||
if let Some(invalid) = invalid_vals.next() {
|
||||
Err(format!("Unknown style, '{}'", invalid))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.help(
|
||||
"Comma-separated list of style elements to display \
|
||||
(*auto*, full, plain, changes, header, grid, numbers, snip).",
|
||||
)
|
||||
.long_help(
|
||||
"Configure which elements (line numbers, file headers, grid \
|
||||
borders, Git modifications, ..) to display in addition to the \
|
||||
file contents. The argument is a comma-separated list of \
|
||||
components to display (e.g. 'numbers,changes,grid') or a \
|
||||
pre-defined style ('full'). To set a default style, add the \
|
||||
'--style=\"..\"' option to the configuration file or export the \
|
||||
BAT_STYLE environment variable (e.g.: export BAT_STYLE=\"..\"). \
|
||||
Possible values: *auto*, full, plain, changes, header, grid, numbers, snip.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("plain")
|
||||
.overrides_with("plain")
|
||||
.overrides_with("number")
|
||||
.short("p")
|
||||
.long("plain")
|
||||
.multiple(true)
|
||||
.help("Show plain style (alias for '--style=plain').")
|
||||
.long_help(
|
||||
"Only show plain style, no decorations. This is an alias for \
|
||||
'--style=plain'. When '-p' is used twice ('-pp'), it also disables \
|
||||
automatic paging (alias for '--style=plain --pager=never').",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("number")
|
||||
.long("number")
|
||||
.overrides_with("number")
|
||||
.short("n")
|
||||
.help("Show line numbers (alias for '--style=numbers').")
|
||||
.long_help(
|
||||
"Only show line numbers, no other decorations. This is an alias for \
|
||||
'--style=numbers'",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("show-all")
|
||||
.long("show-all")
|
||||
.alias("show-nonprintable")
|
||||
.short("A")
|
||||
.conflicts_with("language")
|
||||
.help("Show non-printable characters (space, tab, newline, ..).")
|
||||
.long_help(
|
||||
"Show non-printable characters like space, tab or newline. \
|
||||
This option can also be used to print binary files. \
|
||||
Use '--tabs' to control the width of the tab-placeholders."
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("line-range")
|
||||
.long("line-range")
|
||||
.short("r")
|
||||
.multiple(true)
|
||||
.takes_value(true)
|
||||
.number_of_values(1)
|
||||
.value_name("N:M")
|
||||
.help("Only print the lines from N to M.")
|
||||
.long_help(
|
||||
"Only print the specified range of lines for each file. \
|
||||
For example:\n \
|
||||
'--line-range 30:40' prints lines 30 to 40\n \
|
||||
'--line-range :40' prints lines 1 to 40\n \
|
||||
'--line-range 40:' prints lines 40 to the end of the file",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("highlight-line")
|
||||
.long("highlight-line")
|
||||
.short("H")
|
||||
.takes_value(true)
|
||||
.number_of_values(1)
|
||||
.multiple(true)
|
||||
.value_name("N")
|
||||
.help("Highlight the given line.")
|
||||
.long_help(
|
||||
"Highlight the N-th line with a different background color",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("color")
|
||||
.long("color")
|
||||
.overrides_with("color")
|
||||
.takes_value(true)
|
||||
.value_name("when")
|
||||
.possible_values(&["auto", "never", "always"])
|
||||
.hide_default_value(true)
|
||||
.default_value("auto")
|
||||
.help("When to use colors (*auto*, never, always).")
|
||||
.long_help(
|
||||
"Specify when to use colored output. The automatic mode \
|
||||
only enables colors if an interactive terminal is detected. \
|
||||
Possible values: *auto*, never, always.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("italic-text")
|
||||
.long("italic-text")
|
||||
.takes_value(true)
|
||||
.value_name("when")
|
||||
.possible_values(&["always", "never"])
|
||||
.default_value("never")
|
||||
.hide_default_value(true)
|
||||
.help("Use italics in output (always, *never*)")
|
||||
.long_help("Specify when to use ANSI sequences for italic text in the output. Possible values: always, *never*."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("decorations")
|
||||
.long("decorations")
|
||||
.overrides_with("decorations")
|
||||
.takes_value(true)
|
||||
.value_name("when")
|
||||
.possible_values(&["auto", "never", "always"])
|
||||
.default_value("auto")
|
||||
.hide_default_value(true)
|
||||
.help("When to show the decorations (*auto*, never, always).")
|
||||
.long_help(
|
||||
"Specify when to use the decorations that have been specified \
|
||||
via '--style'. The automatic mode only enables decorations if \
|
||||
an interactive terminal is detected. Possible values: *auto*, never, always.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("paging")
|
||||
.long("paging")
|
||||
.overrides_with("paging")
|
||||
.takes_value(true)
|
||||
.value_name("when")
|
||||
.possible_values(&["auto", "never", "always"])
|
||||
.default_value("auto")
|
||||
.hide_default_value(true)
|
||||
.help("Specify when to use the pager (*auto*, never, always).")
|
||||
.long_help(
|
||||
"Specify when to use the pager. To control which pager \
|
||||
is used, set the PAGER or BAT_PAGER environment \
|
||||
variables (the latter takes precedence) or use the '--pager' option. \
|
||||
To disable the pager permanently, set BAT_PAGER to an empty string \
|
||||
or set '--paging=never' in the configuration file. \
|
||||
Possible values: *auto*, never, always.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("pager")
|
||||
.long("pager")
|
||||
.overrides_with("pager")
|
||||
.takes_value(true)
|
||||
.value_name("command")
|
||||
.hidden_short_help(true)
|
||||
.help("Determine which pager to use.")
|
||||
.long_help(
|
||||
"Determine which pager is used. This option will overwrite \
|
||||
the PAGER and BAT_PAGER environment variables. The default \
|
||||
pager is 'less'. To disable the pager completely, use the \
|
||||
'--paging' option. \
|
||||
Example: '--pager \"less -RF\"'.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("wrap")
|
||||
.long("wrap")
|
||||
.overrides_with("wrap")
|
||||
.takes_value(true)
|
||||
.value_name("mode")
|
||||
.possible_values(&["auto", "never", "character"])
|
||||
.default_value("auto")
|
||||
.hide_default_value(true)
|
||||
.help("Specify the text-wrapping mode (*auto*, never, character).")
|
||||
.long_help("Specify the text-wrapping mode (*auto*, never, character). \
|
||||
The '--terminal-width' option can be used in addition to \
|
||||
control the output width."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("tabs")
|
||||
.long("tabs")
|
||||
.overrides_with("tabs")
|
||||
.takes_value(true)
|
||||
.value_name("T")
|
||||
.validator(
|
||||
|t| {
|
||||
t.parse::<u32>()
|
||||
.map_err(|_t| "must be a number")
|
||||
.map(|_t| ()) // Convert to Result<(), &str>
|
||||
.map_err(|e| e.to_string())
|
||||
}, // Convert to Result<(), String>
|
||||
)
|
||||
.help("Set the tab width to T spaces.")
|
||||
.long_help(
|
||||
"Set the tab width to T spaces. Use a width of 0 to pass tabs through \
|
||||
directly",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("unbuffered")
|
||||
.short("u")
|
||||
.long("unbuffered")
|
||||
.hidden_short_help(true)
|
||||
.long_help(
|
||||
"This option exists for POSIX-compliance reasons ('u' is for \
|
||||
'unbuffered'). The output is always unbuffered - this option \
|
||||
is simply ignored.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("terminal-width")
|
||||
.long("terminal-width")
|
||||
.takes_value(true)
|
||||
.value_name("width")
|
||||
.hidden_short_help(true)
|
||||
.allow_hyphen_values(true)
|
||||
.validator(
|
||||
|t| {
|
||||
let is_offset = t.starts_with('+') || t.starts_with('-');
|
||||
t.parse::<i32>()
|
||||
.map_err(|_e| "must be an offset or number")
|
||||
.and_then(|v| if v == 0 && !is_offset {
|
||||
Err("terminal width cannot be zero".into())
|
||||
} else {
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.help(
|
||||
"Explicitly set the width of the terminal instead of determining it \
|
||||
automatically. If prefixed with '+' or '-', the value will be treated \
|
||||
as an offset to the actual terminal width. See also: '--wrap'.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("no-config")
|
||||
.long("no-config")
|
||||
.hidden(true)
|
||||
.help("Do not use the configuration file"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("config-file")
|
||||
.long("config-file")
|
||||
.conflicts_with("list-languages")
|
||||
.conflicts_with("list-themes")
|
||||
.hidden(true)
|
||||
.help("Show path to the configuration file."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("config-dir")
|
||||
.long("config-dir")
|
||||
.hidden(true)
|
||||
.help("Show bat's configuration directory."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("cache-dir")
|
||||
.long("cache-dir")
|
||||
.hidden(true)
|
||||
.help("Show bat's cache directory."),
|
||||
)
|
||||
.help_message("Print this help message.")
|
||||
.version_message("Show version information.");
|
||||
|
||||
// Check if the current directory contains a file name cache. Otherwise,
|
||||
// enable the 'bat cache' subcommand.
|
||||
if Path::new("cache").exists() {
|
||||
app
|
||||
} else {
|
||||
app.subcommand(
|
||||
SubCommand::with_name("cache")
|
||||
.about("Modify the syntax-definition and theme cache")
|
||||
.arg(
|
||||
Arg::with_name("build")
|
||||
.long("build")
|
||||
.short("b")
|
||||
.help("Initialize (or update) the syntax/theme cache.")
|
||||
.long_help(
|
||||
"Initialize (or update) the syntax/theme cache by loading from \
|
||||
the source directory (default: the configuration directory).",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("clear")
|
||||
.long("clear")
|
||||
.short("c")
|
||||
.help("Remove the cached syntax definitions and themes."),
|
||||
)
|
||||
.group(
|
||||
ArgGroup::with_name("cache-actions")
|
||||
.args(&["build", "clear"])
|
||||
.required(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("source")
|
||||
.long("source")
|
||||
.requires("build")
|
||||
.takes_value(true)
|
||||
.value_name("dir")
|
||||
.help("Use a different directory to load syntaxes and themes from."),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("target")
|
||||
.long("target")
|
||||
.requires("build")
|
||||
.takes_value(true)
|
||||
.value_name("dir")
|
||||
.help(
|
||||
"Use a different directory to store the cached syntax and theme set.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("blank")
|
||||
.long("blank")
|
||||
.requires("build")
|
||||
.help(
|
||||
"Create completely new syntax and theme sets \
|
||||
(instead of appending to the default sets).",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
// `error_chain!` can recurse deeply
|
||||
#![recursion_limit = "1024"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
extern crate ansi_term;
|
||||
extern crate atty;
|
||||
extern crate console;
|
||||
extern crate content_inspector;
|
||||
extern crate dirs as dirs_rs;
|
||||
extern crate encoding;
|
||||
extern crate git2;
|
||||
extern crate shell_words;
|
||||
extern crate syntect;
|
||||
extern crate wild;
|
||||
|
||||
mod app;
|
||||
mod assets;
|
||||
mod clap_app;
|
||||
mod config;
|
||||
mod controller;
|
||||
mod decorations;
|
||||
mod diff;
|
||||
mod dirs;
|
||||
mod inputfile;
|
||||
mod line_range;
|
||||
mod output;
|
||||
mod preprocessor;
|
||||
mod printer;
|
||||
mod style;
|
||||
mod syntax_mapping;
|
||||
mod terminal;
|
||||
mod util;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
|
||||
use ansi_term::Colour::Green;
|
||||
use ansi_term::Style;
|
||||
|
||||
use crate::app::{App, Config};
|
||||
use crate::assets::{cache_dir, clear_assets, config_dir, HighlightingAssets};
|
||||
use crate::config::config_file;
|
||||
use crate::controller::Controller;
|
||||
use crate::inputfile::InputFile;
|
||||
use crate::style::{OutputComponent, OutputComponents};
|
||||
|
||||
mod errors {
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Clap(::clap::Error);
|
||||
Io(::std::io::Error);
|
||||
SyntectError(::syntect::LoadingError);
|
||||
ParseIntError(::std::num::ParseIntError);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_error(error: &Error) {
|
||||
match error {
|
||||
Error(ErrorKind::Io(ref io_error), _)
|
||||
if io_error.kind() == super::io::ErrorKind::BrokenPipe =>
|
||||
{
|
||||
super::process::exit(0);
|
||||
}
|
||||
_ => {
|
||||
use ansi_term::Colour::Red;
|
||||
eprintln!("{}: {}", Red.paint("[bat error]"), error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
fn run_cache_subcommand(matches: &clap::ArgMatches) -> Result<()> {
|
||||
if matches.is_present("build") {
|
||||
let source_dir = matches.value_of("source").map(Path::new);
|
||||
let target_dir = matches.value_of("target").map(Path::new);
|
||||
|
||||
let blank = matches.is_present("blank");
|
||||
|
||||
let assets = HighlightingAssets::from_files(source_dir, blank)?;
|
||||
assets.save(target_dir)?;
|
||||
} else if matches.is_present("clear") {
|
||||
clear_assets();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_languages(config: &Config) -> Result<()> {
|
||||
let assets = HighlightingAssets::new();
|
||||
let mut languages = assets
|
||||
.syntax_set
|
||||
.syntaxes()
|
||||
.iter()
|
||||
.filter(|syntax| !syntax.hidden && !syntax.file_extensions.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
languages.sort_by_key(|lang| lang.name.to_uppercase());
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
if config.loop_through {
|
||||
for lang in languages {
|
||||
write!(stdout, "{}:{}\n", lang.name, lang.file_extensions.join(","))?;
|
||||
}
|
||||
} else {
|
||||
let longest = languages
|
||||
.iter()
|
||||
.map(|syntax| syntax.name.len())
|
||||
.max()
|
||||
.unwrap_or(32); // Fallback width if they have no language definitions.
|
||||
|
||||
let comma_separator = ", ";
|
||||
let separator = " ";
|
||||
// Line-wrapping for the possible file extension overflow.
|
||||
let desired_width = config.term_width - longest - separator.len();
|
||||
|
||||
let style = if config.colored_output {
|
||||
Green.normal()
|
||||
} else {
|
||||
Style::default()
|
||||
};
|
||||
|
||||
for lang in languages {
|
||||
write!(stdout, "{:width$}{}", lang.name, separator, width = longest)?;
|
||||
|
||||
// Number of characters on this line so far, wrap before `desired_width`
|
||||
let mut num_chars = 0;
|
||||
|
||||
let mut extension = lang.file_extensions.iter().peekable();
|
||||
while let Some(word) = extension.next() {
|
||||
// If we can't fit this word in, then create a line break and align it in.
|
||||
let new_chars = word.len() + comma_separator.len();
|
||||
if num_chars + new_chars >= desired_width {
|
||||
num_chars = 0;
|
||||
write!(stdout, "\n{:width$}{}", "", separator, width = longest)?;
|
||||
}
|
||||
|
||||
num_chars += new_chars;
|
||||
write!(stdout, "{}", style.paint(&word[..]))?;
|
||||
if extension.peek().is_some() {
|
||||
write!(stdout, "{}", comma_separator)?;
|
||||
}
|
||||
}
|
||||
writeln!(stdout)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_themes(cfg: &Config) -> Result<()> {
|
||||
let assets = HighlightingAssets::new();
|
||||
let themes = &assets.theme_set.themes;
|
||||
let mut config = cfg.clone();
|
||||
let mut style = HashSet::new();
|
||||
style.insert(OutputComponent::Plain);
|
||||
config.files = vec![InputFile::ThemePreviewFile];
|
||||
config.output_components = OutputComponents(style);
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
if config.colored_output {
|
||||
for (theme, _) in themes.iter() {
|
||||
writeln!(
|
||||
stdout,
|
||||
"Theme: {}\n",
|
||||
Style::new().bold().paint(theme.to_string())
|
||||
)?;
|
||||
config.theme = theme.to_string();
|
||||
let _controller = Controller::new(&config, &assets).run();
|
||||
writeln!(stdout)?;
|
||||
}
|
||||
} else {
|
||||
for (theme, _) in themes.iter() {
|
||||
writeln!(stdout, "{}", theme)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_controller(config: &Config) -> Result<bool> {
|
||||
let assets = HighlightingAssets::new();
|
||||
let controller = Controller::new(&config, &assets);
|
||||
controller.run()
|
||||
}
|
||||
|
||||
/// Returns `Err(..)` upon fatal errors. Otherwise, returns `Ok(true)` on full success and
|
||||
/// `Ok(false)` if any intermediate errors occurred (were printed).
|
||||
fn run() -> Result<bool> {
|
||||
let app = App::new()?;
|
||||
|
||||
match app.matches.subcommand() {
|
||||
("cache", Some(cache_matches)) => {
|
||||
// If there is a file named 'cache' in the current working directory,
|
||||
// arguments for subcommand 'cache' are not mandatory.
|
||||
// If there are non-zero arguments, execute the subcommand cache, else, open the file cache.
|
||||
if !cache_matches.args.is_empty() {
|
||||
run_cache_subcommand(cache_matches)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
let mut config = app.config()?;
|
||||
config.files = vec![InputFile::Ordinary(&"cache")];
|
||||
|
||||
run_controller(&config)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let config = app.config()?;
|
||||
|
||||
if app.matches.is_present("list-languages") {
|
||||
list_languages(&config)?;
|
||||
|
||||
Ok(true)
|
||||
} else if app.matches.is_present("list-themes") {
|
||||
list_themes(&config)?;
|
||||
|
||||
Ok(true)
|
||||
} else if app.matches.is_present("config-file") {
|
||||
println!("{}", config_file().to_string_lossy());
|
||||
|
||||
Ok(true)
|
||||
} else if app.matches.is_present("config-dir") {
|
||||
writeln!(io::stdout(), "{}", config_dir())?;
|
||||
Ok(true)
|
||||
} else if app.matches.is_present("cache-dir") {
|
||||
writeln!(io::stdout(), "{}", cache_dir())?;
|
||||
Ok(true)
|
||||
} else {
|
||||
run_controller(&config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let result = run();
|
||||
|
||||
match result {
|
||||
Err(error) => {
|
||||
handle_error(&error);
|
||||
process::exit(1);
|
||||
}
|
||||
Ok(false) => {
|
||||
process::exit(1);
|
||||
}
|
||||
Ok(true) => {
|
||||
process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user