mirror of
https://github.com/sharkdp/bat
synced 2026-07-29 18:01:43 +00:00
Move src/bin/* into src/bin/bat/
This will limit [[bin]] to *bat* only which will make: - `cargo run` works without specifying --bin - prevent `cargo build --bins` to produce multiple binaries (app,clap_app,...)
This commit is contained in:
committed by
David Peter
parent
710a1df4ff
commit
e981bd88c1
@@ -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).",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::app::{Config, PagingMode};
|
||||
use crate::assets::HighlightingAssets;
|
||||
use crate::errors::*;
|
||||
use crate::inputfile::{InputFile, InputFileReader};
|
||||
use crate::line_range::{LineRanges, RangeCheckResult};
|
||||
use crate::output::OutputType;
|
||||
use crate::printer::{InteractivePrinter, Printer, SimplePrinter};
|
||||
|
||||
pub struct Controller<'a> {
|
||||
config: &'a Config<'a>,
|
||||
assets: &'a HighlightingAssets,
|
||||
}
|
||||
|
||||
impl<'b> Controller<'b> {
|
||||
pub fn new<'a>(config: &'a Config, assets: &'a HighlightingAssets) -> Controller<'a> {
|
||||
Controller { config, assets }
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<bool> {
|
||||
// Do not launch the pager if NONE of the input files exist
|
||||
let mut paging_mode = self.config.paging_mode;
|
||||
if self.config.paging_mode != PagingMode::Never {
|
||||
let call_pager = self.config.files.iter().any(|file| {
|
||||
if let InputFile::Ordinary(path) = file {
|
||||
return Path::new(path).exists();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if !call_pager {
|
||||
paging_mode = PagingMode::Never;
|
||||
}
|
||||
}
|
||||
|
||||
let mut output_type = OutputType::from_mode(paging_mode, self.config.pager)?;
|
||||
let writer = output_type.handle()?;
|
||||
let mut no_errors: bool = true;
|
||||
|
||||
let stdin = io::stdin();
|
||||
|
||||
for input_file in &self.config.files {
|
||||
match input_file.get_reader(&stdin) {
|
||||
Err(error) => {
|
||||
handle_error(&error);
|
||||
no_errors = false;
|
||||
}
|
||||
Ok(mut reader) => {
|
||||
let result = if self.config.loop_through {
|
||||
let mut printer = SimplePrinter::new();
|
||||
self.print_file(reader, &mut printer, writer, *input_file)
|
||||
} else {
|
||||
let mut printer = InteractivePrinter::new(
|
||||
&self.config,
|
||||
&self.assets,
|
||||
*input_file,
|
||||
&mut reader,
|
||||
);
|
||||
self.print_file(reader, &mut printer, writer, *input_file)
|
||||
};
|
||||
|
||||
if let Err(error) = result {
|
||||
handle_error(&error);
|
||||
no_errors = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(no_errors)
|
||||
}
|
||||
|
||||
fn print_file<'a, P: Printer>(
|
||||
&self,
|
||||
reader: InputFileReader,
|
||||
printer: &mut P,
|
||||
writer: &mut dyn Write,
|
||||
input_file: InputFile<'a>,
|
||||
) -> Result<()> {
|
||||
printer.print_header(writer, input_file)?;
|
||||
if !reader.first_line.is_empty() {
|
||||
self.print_file_ranges(printer, writer, reader, &self.config.line_ranges)?;
|
||||
}
|
||||
printer.print_footer(writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_file_ranges<P: Printer>(
|
||||
&self,
|
||||
printer: &mut P,
|
||||
writer: &mut dyn Write,
|
||||
mut reader: InputFileReader,
|
||||
line_ranges: &LineRanges,
|
||||
) -> Result<()> {
|
||||
let mut line_buffer = Vec::new();
|
||||
let mut line_number: usize = 1;
|
||||
|
||||
let mut first_range: bool = true;
|
||||
let mut mid_range: bool = false;
|
||||
|
||||
while reader.read_line(&mut line_buffer)? {
|
||||
match line_ranges.check(line_number) {
|
||||
RangeCheckResult::OutsideRange => {
|
||||
// Call the printer in case we need to call the syntax highlighter
|
||||
// for this line. However, set `out_of_range` to `true`.
|
||||
printer.print_line(true, writer, line_number, &line_buffer)?;
|
||||
mid_range = false;
|
||||
}
|
||||
|
||||
RangeCheckResult::InRange => {
|
||||
if self.config.output_components.snip() {
|
||||
if first_range {
|
||||
first_range = false;
|
||||
mid_range = true;
|
||||
} else if !mid_range {
|
||||
mid_range = true;
|
||||
printer.print_snip(writer)?;
|
||||
}
|
||||
}
|
||||
|
||||
printer.print_line(false, writer, line_number, &line_buffer)?;
|
||||
}
|
||||
RangeCheckResult::AfterLastRange => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
line_number += 1;
|
||||
line_buffer.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
use crate::diff::LineChange;
|
||||
use crate::printer::{Colors, InteractivePrinter};
|
||||
use ansi_term::Style;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DecorationText {
|
||||
pub width: usize,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
pub trait Decoration {
|
||||
fn generate(
|
||||
&self,
|
||||
line_number: usize,
|
||||
continuation: bool,
|
||||
printer: &InteractivePrinter,
|
||||
) -> DecorationText;
|
||||
fn width(&self) -> usize;
|
||||
}
|
||||
|
||||
pub struct LineNumberDecoration {
|
||||
color: Style,
|
||||
cached_wrap: DecorationText,
|
||||
cached_wrap_invalid_at: usize,
|
||||
}
|
||||
|
||||
impl LineNumberDecoration {
|
||||
pub fn new(colors: &Colors) -> Self {
|
||||
LineNumberDecoration {
|
||||
color: colors.line_number,
|
||||
cached_wrap_invalid_at: 10000,
|
||||
cached_wrap: DecorationText {
|
||||
text: colors.line_number.paint(" ".repeat(4)).to_string(),
|
||||
width: 4,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoration for LineNumberDecoration {
|
||||
fn generate(
|
||||
&self,
|
||||
line_number: usize,
|
||||
continuation: bool,
|
||||
_printer: &InteractivePrinter,
|
||||
) -> DecorationText {
|
||||
if continuation {
|
||||
if line_number > self.cached_wrap_invalid_at {
|
||||
let new_width = self.cached_wrap.width + 1;
|
||||
return DecorationText {
|
||||
text: self.color.paint(" ".repeat(new_width)).to_string(),
|
||||
width: new_width,
|
||||
};
|
||||
}
|
||||
|
||||
self.cached_wrap.clone()
|
||||
} else {
|
||||
let plain: String = format!("{:4}", line_number);
|
||||
DecorationText {
|
||||
width: plain.len(),
|
||||
text: self.color.paint(plain).to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
4
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LineChangesDecoration {
|
||||
cached_none: DecorationText,
|
||||
cached_added: DecorationText,
|
||||
cached_removed_above: DecorationText,
|
||||
cached_removed_below: DecorationText,
|
||||
cached_modified: DecorationText,
|
||||
}
|
||||
|
||||
impl LineChangesDecoration {
|
||||
#[inline]
|
||||
fn generate_cached(style: Style, text: &str) -> DecorationText {
|
||||
DecorationText {
|
||||
text: style.paint(text).to_string(),
|
||||
width: text.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(colors: &Colors) -> Self {
|
||||
LineChangesDecoration {
|
||||
cached_none: Self::generate_cached(Style::default(), " "),
|
||||
cached_added: Self::generate_cached(colors.git_added, "+"),
|
||||
cached_removed_above: Self::generate_cached(colors.git_removed, "‾"),
|
||||
cached_removed_below: Self::generate_cached(colors.git_removed, "_"),
|
||||
cached_modified: Self::generate_cached(colors.git_modified, "~"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoration for LineChangesDecoration {
|
||||
fn generate(
|
||||
&self,
|
||||
line_number: usize,
|
||||
continuation: bool,
|
||||
printer: &InteractivePrinter,
|
||||
) -> DecorationText {
|
||||
if !continuation {
|
||||
if let Some(ref changes) = printer.line_changes {
|
||||
return match changes.get(&(line_number as u32)) {
|
||||
Some(&LineChange::Added) => self.cached_added.clone(),
|
||||
Some(&LineChange::RemovedAbove) => self.cached_removed_above.clone(),
|
||||
Some(&LineChange::RemovedBelow) => self.cached_removed_below.clone(),
|
||||
Some(&LineChange::Modified) => self.cached_modified.clone(),
|
||||
_ => self.cached_none.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.cached_none.clone()
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.cached_none.width
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GridBorderDecoration {
|
||||
cached: DecorationText,
|
||||
}
|
||||
|
||||
impl GridBorderDecoration {
|
||||
pub fn new(colors: &Colors) -> Self {
|
||||
GridBorderDecoration {
|
||||
cached: DecorationText {
|
||||
text: colors.grid.paint("│").to_string(),
|
||||
width: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoration for GridBorderDecoration {
|
||||
fn generate(
|
||||
&self,
|
||||
_line_number: usize,
|
||||
_continuation: bool,
|
||||
_printer: &InteractivePrinter,
|
||||
) -> DecorationText {
|
||||
self.cached.clone()
|
||||
}
|
||||
|
||||
fn width(&self) -> usize {
|
||||
self.cached.width
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
use shell_words;
|
||||
|
||||
use crate::app::PagingMode;
|
||||
use crate::errors::*;
|
||||
|
||||
pub enum OutputType {
|
||||
Pager(Child),
|
||||
Stdout(io::Stdout),
|
||||
}
|
||||
|
||||
impl OutputType {
|
||||
pub fn from_mode(mode: PagingMode, pager: Option<&str>) -> Result<Self> {
|
||||
use self::PagingMode::*;
|
||||
Ok(match mode {
|
||||
Always => OutputType::try_pager(false, pager)?,
|
||||
QuitIfOneScreen => OutputType::try_pager(true, pager)?,
|
||||
_ => OutputType::stdout(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to launch the pager. Fall back to stdout in case of errors.
|
||||
fn try_pager(quit_if_one_screen: bool, pager_from_config: Option<&str>) -> Result<Self> {
|
||||
let mut replace_arguments_to_less = false;
|
||||
|
||||
let pager_from_env = match (env::var("BAT_PAGER"), env::var("PAGER")) {
|
||||
(Ok(bat_pager), _) => Some(bat_pager),
|
||||
(_, Ok(pager)) => {
|
||||
// less needs to be called with the '-R' option in order to properly interpret the
|
||||
// ANSI color sequences printed by bat. If someone has set PAGER="less -F", we
|
||||
// therefore need to overwrite the arguments and add '-R'.
|
||||
//
|
||||
// We only do this for PAGER (as it is not specific to 'bat'), not for BAT_PAGER
|
||||
// or bats '--pager' command line option.
|
||||
replace_arguments_to_less = true;
|
||||
Some(pager)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let pager_from_config = pager_from_config.map(|p| p.to_string());
|
||||
|
||||
if pager_from_config.is_some() {
|
||||
replace_arguments_to_less = false;
|
||||
}
|
||||
|
||||
let pager = pager_from_config
|
||||
.or(pager_from_env)
|
||||
.unwrap_or_else(|| String::from("less"));
|
||||
|
||||
let pagerflags =
|
||||
shell_words::split(&pager).chain_err(|| "Could not parse pager command.")?;
|
||||
|
||||
match pagerflags.split_first() {
|
||||
Some((pager_name, args)) => {
|
||||
let mut pager_path = PathBuf::from(pager_name);
|
||||
|
||||
if pager_path.file_stem() == Some(&OsString::from("bat")) {
|
||||
pager_path = PathBuf::from("less");
|
||||
}
|
||||
|
||||
let is_less = pager_path.file_stem() == Some(&OsString::from("less"));
|
||||
|
||||
let mut process = if is_less {
|
||||
let mut p = Command::new(&pager_path);
|
||||
if args.is_empty() || replace_arguments_to_less {
|
||||
p.args(vec!["--RAW-CONTROL-CHARS", "--no-init"]);
|
||||
if quit_if_one_screen {
|
||||
p.arg("--quit-if-one-screen");
|
||||
}
|
||||
} else {
|
||||
p.args(args);
|
||||
}
|
||||
p.env("LESSCHARSET", "UTF-8");
|
||||
p
|
||||
} else {
|
||||
let mut p = Command::new(&pager_path);
|
||||
p.args(args);
|
||||
p
|
||||
};
|
||||
|
||||
Ok(process
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.map(OutputType::Pager)
|
||||
.unwrap_or_else(|_| OutputType::stdout()))
|
||||
}
|
||||
None => Ok(OutputType::stdout()),
|
||||
}
|
||||
}
|
||||
|
||||
fn stdout() -> Self {
|
||||
OutputType::Stdout(io::stdout())
|
||||
}
|
||||
|
||||
pub fn handle(&mut self) -> Result<&mut dyn Write> {
|
||||
Ok(match *self {
|
||||
OutputType::Pager(ref mut command) => command
|
||||
.stdin
|
||||
.as_mut()
|
||||
.chain_err(|| "Could not open stdin for pager")?,
|
||||
OutputType::Stdout(ref mut handle) => handle,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OutputType {
|
||||
fn drop(&mut self) {
|
||||
if let OutputType::Pager(ref mut command) = *self {
|
||||
let _ = command.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
use std::io::Write;
|
||||
use std::vec::Vec;
|
||||
|
||||
use ansi_term::Colour::{Fixed, Green, Red, Yellow};
|
||||
use ansi_term::Style;
|
||||
|
||||
use console::AnsiCodeIterator;
|
||||
|
||||
use syntect::easy::HighlightLines;
|
||||
use syntect::highlighting::Color;
|
||||
use syntect::highlighting::Theme;
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
use content_inspector::ContentType;
|
||||
|
||||
use encoding::all::{UTF_16BE, UTF_16LE};
|
||||
use encoding::{DecoderTrap, Encoding};
|
||||
|
||||
use crate::app::Config;
|
||||
use crate::assets::HighlightingAssets;
|
||||
use crate::decorations::{
|
||||
Decoration, GridBorderDecoration, LineChangesDecoration, LineNumberDecoration,
|
||||
};
|
||||
use crate::diff::get_git_diff;
|
||||
use crate::diff::LineChanges;
|
||||
use crate::errors::*;
|
||||
use crate::inputfile::{InputFile, InputFileReader};
|
||||
use crate::preprocessor::{expand_tabs, replace_nonprintable};
|
||||
use crate::style::OutputWrap;
|
||||
use crate::terminal::{as_terminal_escaped, to_ansi_color};
|
||||
|
||||
pub trait Printer {
|
||||
fn print_header(&mut self, handle: &mut dyn Write, file: InputFile) -> Result<()>;
|
||||
fn print_footer(&mut self, handle: &mut dyn Write) -> Result<()>;
|
||||
|
||||
fn print_snip(&mut self, handle: &mut dyn Write) -> Result<()>;
|
||||
|
||||
fn print_line(
|
||||
&mut self,
|
||||
out_of_range: bool,
|
||||
handle: &mut dyn Write,
|
||||
line_number: usize,
|
||||
line_buffer: &[u8],
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
pub struct SimplePrinter;
|
||||
|
||||
impl SimplePrinter {
|
||||
pub fn new() -> Self {
|
||||
SimplePrinter {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Printer for SimplePrinter {
|
||||
fn print_header(&mut self, _handle: &mut dyn Write, _file: InputFile) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_footer(&mut self, _handle: &mut dyn Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_snip(&mut self, _handle: &mut dyn Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_line(
|
||||
&mut self,
|
||||
out_of_range: bool,
|
||||
handle: &mut dyn Write,
|
||||
_line_number: usize,
|
||||
line_buffer: &[u8],
|
||||
) -> Result<()> {
|
||||
if !out_of_range {
|
||||
handle.write_all(line_buffer)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InteractivePrinter<'a> {
|
||||
colors: Colors,
|
||||
config: &'a Config<'a>,
|
||||
decorations: Vec<Box<dyn Decoration>>,
|
||||
panel_width: usize,
|
||||
ansi_prefix_sgr: String,
|
||||
content_type: Option<ContentType>,
|
||||
pub line_changes: Option<LineChanges>,
|
||||
highlighter: Option<HighlightLines<'a>>,
|
||||
syntax_set: &'a SyntaxSet,
|
||||
background_color_highlight: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> InteractivePrinter<'a> {
|
||||
pub fn new(
|
||||
config: &'a Config,
|
||||
assets: &'a HighlightingAssets,
|
||||
file: InputFile,
|
||||
reader: &mut InputFileReader,
|
||||
) -> Self {
|
||||
let theme = assets.get_theme(&config.theme);
|
||||
|
||||
let background_color_highlight = theme.settings.line_highlight;
|
||||
|
||||
let colors = if config.colored_output {
|
||||
Colors::colored(theme, config.true_color)
|
||||
} else {
|
||||
Colors::plain()
|
||||
};
|
||||
|
||||
// Create decorations.
|
||||
let mut decorations: Vec<Box<dyn Decoration>> = Vec::new();
|
||||
|
||||
if config.output_components.numbers() {
|
||||
decorations.push(Box::new(LineNumberDecoration::new(&colors)));
|
||||
}
|
||||
|
||||
if config.output_components.changes() {
|
||||
decorations.push(Box::new(LineChangesDecoration::new(&colors)));
|
||||
}
|
||||
|
||||
let mut panel_width: usize =
|
||||
decorations.len() + decorations.iter().fold(0, |a, x| a + x.width());
|
||||
|
||||
// The grid border decoration isn't added until after the panel_width calculation, since the
|
||||
// print_horizontal_line, print_header, and print_footer functions all assume the panel
|
||||
// width is without the grid border.
|
||||
if config.output_components.grid() && !decorations.is_empty() {
|
||||
decorations.push(Box::new(GridBorderDecoration::new(&colors)));
|
||||
}
|
||||
|
||||
// Disable the panel if the terminal is too small (i.e. can't fit 5 characters with the
|
||||
// panel showing).
|
||||
if config.term_width
|
||||
< (decorations.len() + decorations.iter().fold(0, |a, x| a + x.width())) + 5
|
||||
{
|
||||
decorations.clear();
|
||||
panel_width = 0;
|
||||
}
|
||||
|
||||
let mut line_changes = None;
|
||||
|
||||
let highlighter = if reader
|
||||
.content_type
|
||||
.map_or(false, |c| c.is_binary() && !config.show_nonprintable)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
// Get the Git modifications
|
||||
line_changes = if config.output_components.changes() {
|
||||
match file {
|
||||
InputFile::Ordinary(filename) => get_git_diff(filename),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Determine the type of syntax for highlighting
|
||||
let syntax = assets.get_syntax(config.language, file, reader, &config.syntax_mapping);
|
||||
Some(HighlightLines::new(syntax, theme))
|
||||
};
|
||||
|
||||
InteractivePrinter {
|
||||
panel_width,
|
||||
colors,
|
||||
config,
|
||||
decorations,
|
||||
content_type: reader.content_type,
|
||||
ansi_prefix_sgr: String::new(),
|
||||
line_changes,
|
||||
highlighter,
|
||||
syntax_set: &assets.syntax_set,
|
||||
background_color_highlight,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_horizontal_line(&mut self, handle: &mut dyn Write, grid_char: char) -> Result<()> {
|
||||
if self.panel_width == 0 {
|
||||
writeln!(
|
||||
handle,
|
||||
"{}",
|
||||
self.colors.grid.paint("─".repeat(self.config.term_width))
|
||||
)?;
|
||||
} else {
|
||||
let hline = "─".repeat(self.config.term_width - (self.panel_width + 1));
|
||||
let hline = format!("{}{}{}", "─".repeat(self.panel_width), grid_char, hline);
|
||||
writeln!(handle, "{}", self.colors.grid.paint(hline))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_fake_panel(&self, text: &str) -> String {
|
||||
if self.panel_width == 0 {
|
||||
"".to_string()
|
||||
} else {
|
||||
let text_truncated: String = text.chars().take(self.panel_width - 1).collect();
|
||||
let text_filled: String = format!(
|
||||
"{}{}",
|
||||
text_truncated,
|
||||
" ".repeat(self.panel_width - 1 - text_truncated.len())
|
||||
);
|
||||
if self.config.output_components.grid() {
|
||||
format!("{} │ ", text_filled)
|
||||
} else {
|
||||
format!("{}", text_filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn preprocess(&self, text: &str, cursor: &mut usize) -> String {
|
||||
if self.config.tab_width > 0 {
|
||||
expand_tabs(text, self.config.tab_width, cursor)
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Printer for InteractivePrinter<'a> {
|
||||
fn print_header(&mut self, handle: &mut dyn Write, file: InputFile) -> Result<()> {
|
||||
if !self.config.output_components.header() {
|
||||
if Some(ContentType::BINARY) == self.content_type && !self.config.show_nonprintable {
|
||||
let input = match file {
|
||||
InputFile::Ordinary(filename) => format!("file '{}'", filename),
|
||||
_ => "STDIN".into(),
|
||||
};
|
||||
|
||||
writeln!(
|
||||
handle,
|
||||
"{}: Binary content from {} will not be printed to the terminal \
|
||||
(but will be present if the output of 'bat' is piped). You can use 'bat -A' \
|
||||
to show the binary file contents.",
|
||||
Yellow.paint("[bat warning]"),
|
||||
input
|
||||
)?;
|
||||
} else {
|
||||
if self.config.output_components.grid() {
|
||||
self.print_horizontal_line(handle, '┬')?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.config.output_components.grid() {
|
||||
self.print_horizontal_line(handle, '┬')?;
|
||||
|
||||
write!(
|
||||
handle,
|
||||
"{}{}",
|
||||
" ".repeat(self.panel_width),
|
||||
self.colors
|
||||
.grid
|
||||
.paint(if self.panel_width > 0 { "│ " } else { "" }),
|
||||
)?;
|
||||
} else {
|
||||
write!(handle, "{}", " ".repeat(self.panel_width))?;
|
||||
}
|
||||
|
||||
let (prefix, name) = match file {
|
||||
InputFile::Ordinary(filename) => ("File: ", filename),
|
||||
_ => ("", "STDIN"),
|
||||
};
|
||||
|
||||
let mode = match self.content_type {
|
||||
Some(ContentType::BINARY) => " <BINARY>",
|
||||
Some(ContentType::UTF_16LE) => " <UTF-16LE>",
|
||||
Some(ContentType::UTF_16BE) => " <UTF-16BE>",
|
||||
None => " <EMPTY>",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
writeln!(
|
||||
handle,
|
||||
"{}{}{}",
|
||||
prefix,
|
||||
self.colors.filename.paint(name),
|
||||
mode
|
||||
)?;
|
||||
|
||||
if self.config.output_components.grid() {
|
||||
if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
|
||||
self.print_horizontal_line(handle, '┼')?;
|
||||
} else {
|
||||
self.print_horizontal_line(handle, '┴')?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_footer(&mut self, handle: &mut dyn Write) -> Result<()> {
|
||||
if self.config.output_components.grid()
|
||||
&& (self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable)
|
||||
{
|
||||
self.print_horizontal_line(handle, '┴')
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_snip(&mut self, handle: &mut dyn Write) -> Result<()> {
|
||||
let panel = self.create_fake_panel(" ...");
|
||||
let panel_count = panel.chars().count();
|
||||
|
||||
let title = "8<";
|
||||
let title_count = title.chars().count();
|
||||
|
||||
let snip_left = "─ ".repeat((self.config.term_width - panel_count - (title_count / 2)) / 4);
|
||||
let snip_left_count = snip_left.chars().count(); // Can't use .len() with Unicode.
|
||||
|
||||
let snip_right =
|
||||
" ─".repeat((self.config.term_width - panel_count - snip_left_count - title_count) / 2);
|
||||
|
||||
write!(
|
||||
handle,
|
||||
"{}\n",
|
||||
self.colors
|
||||
.grid
|
||||
.paint(format!("{}{}{}{}", panel, snip_left, title, snip_right))
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_line(
|
||||
&mut self,
|
||||
out_of_range: bool,
|
||||
handle: &mut dyn Write,
|
||||
line_number: usize,
|
||||
line_buffer: &[u8],
|
||||
) -> Result<()> {
|
||||
let line = if self.config.show_nonprintable {
|
||||
replace_nonprintable(&line_buffer, self.config.tab_width)
|
||||
} else {
|
||||
match self.content_type {
|
||||
Some(ContentType::BINARY) | None => {
|
||||
return Ok(());
|
||||
}
|
||||
Some(ContentType::UTF_16LE) => UTF_16LE
|
||||
.decode(&line_buffer, DecoderTrap::Replace)
|
||||
.map_err(|_| "Invalid UTF-16LE")?,
|
||||
Some(ContentType::UTF_16BE) => UTF_16BE
|
||||
.decode(&line_buffer, DecoderTrap::Replace)
|
||||
.map_err(|_| "Invalid UTF-16BE")?,
|
||||
_ => String::from_utf8_lossy(&line_buffer).to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let regions = {
|
||||
let highlighter = match self.highlighter {
|
||||
Some(ref mut highlighter) => highlighter,
|
||||
_ => {
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
highlighter.highlight(line.as_ref(), self.syntax_set)
|
||||
};
|
||||
|
||||
if out_of_range {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut cursor: usize = 0;
|
||||
let mut cursor_max: usize = self.config.term_width;
|
||||
let mut cursor_total: usize = 0;
|
||||
let mut panel_wrap: Option<String> = None;
|
||||
|
||||
// Line highlighting
|
||||
let highlight_this_line = self
|
||||
.config
|
||||
.highlight_lines
|
||||
.iter()
|
||||
.any(|&l| l == line_number);
|
||||
|
||||
let background_color = self
|
||||
.background_color_highlight
|
||||
.filter(|_| highlight_this_line);
|
||||
|
||||
// Line decorations.
|
||||
if self.panel_width > 0 {
|
||||
let decorations = self
|
||||
.decorations
|
||||
.iter()
|
||||
.map(|ref d| d.generate(line_number, false, self))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for deco in decorations {
|
||||
write!(handle, "{} ", deco.text)?;
|
||||
cursor_max -= deco.width + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Line contents.
|
||||
if self.config.output_wrap == OutputWrap::None {
|
||||
let true_color = self.config.true_color;
|
||||
let colored_output = self.config.colored_output;
|
||||
let italics = self.config.use_italic_text;
|
||||
|
||||
for &(style, region) in regions.iter() {
|
||||
let text = &*self.preprocess(region, &mut cursor_total);
|
||||
let text_trimmed = text.trim_end_matches(|c| c == '\r' || c == '\n');
|
||||
write!(
|
||||
handle,
|
||||
"{}",
|
||||
as_terminal_escaped(
|
||||
style,
|
||||
text_trimmed,
|
||||
true_color,
|
||||
colored_output,
|
||||
italics,
|
||||
background_color
|
||||
)
|
||||
)?;
|
||||
|
||||
if text.len() != text_trimmed.len() {
|
||||
if let Some(background_color) = background_color {
|
||||
let mut ansi_style = Style::default();
|
||||
ansi_style.background = Some(to_ansi_color(background_color, true_color));
|
||||
let width = if cursor_total <= cursor_max {
|
||||
cursor_max - cursor_total + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
write!(handle, "{}", ansi_style.paint(" ".repeat(width)))?;
|
||||
}
|
||||
write!(handle, "{}", &text[text_trimmed.len()..])?;
|
||||
}
|
||||
}
|
||||
|
||||
if line.bytes().next_back() != Some(b'\n') {
|
||||
writeln!(handle)?;
|
||||
}
|
||||
} else {
|
||||
for &(style, region) in regions.iter() {
|
||||
let ansi_iterator = AnsiCodeIterator::new(region);
|
||||
let mut ansi_prefix: String = String::new();
|
||||
for chunk in ansi_iterator {
|
||||
match chunk {
|
||||
// ANSI escape passthrough.
|
||||
(text, true) => {
|
||||
let is_ansi_csi = text.starts_with("\x1B[");
|
||||
|
||||
if is_ansi_csi && text.ends_with('m') {
|
||||
// It's an ANSI SGR sequence.
|
||||
// We should be mostly safe to just append these together.
|
||||
ansi_prefix.push_str(text);
|
||||
if text == "\x1B[0m" {
|
||||
self.ansi_prefix_sgr = "\x1B[0m".to_owned();
|
||||
} else {
|
||||
self.ansi_prefix_sgr.push_str(text);
|
||||
}
|
||||
} else if is_ansi_csi {
|
||||
// It's a regular CSI sequence.
|
||||
// We should be mostly safe to just append these together.
|
||||
ansi_prefix.push_str(text);
|
||||
} else {
|
||||
// It's probably a VT100 code.
|
||||
// Passing it through is the safest bet.
|
||||
write!(handle, "{}", text)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular text.
|
||||
(text, false) => {
|
||||
let text = self.preprocess(
|
||||
text.trim_end_matches(|c| c == '\r' || c == '\n'),
|
||||
&mut cursor_total,
|
||||
);
|
||||
|
||||
let mut chars = text.chars();
|
||||
let mut remaining = text.chars().count();
|
||||
|
||||
while remaining > 0 {
|
||||
let available = cursor_max - cursor;
|
||||
|
||||
// It fits.
|
||||
if remaining <= available {
|
||||
let text = chars.by_ref().take(remaining).collect::<String>();
|
||||
cursor += remaining;
|
||||
|
||||
write!(
|
||||
handle,
|
||||
"{}",
|
||||
as_terminal_escaped(
|
||||
style,
|
||||
&*format!(
|
||||
"{}{}{}",
|
||||
self.ansi_prefix_sgr, ansi_prefix, text
|
||||
),
|
||||
self.config.true_color,
|
||||
self.config.colored_output,
|
||||
self.config.use_italic_text,
|
||||
background_color
|
||||
)
|
||||
)?;
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate wrap padding if not already generated.
|
||||
if panel_wrap.is_none() {
|
||||
panel_wrap = if self.panel_width > 0 {
|
||||
Some(format!(
|
||||
"{} ",
|
||||
self.decorations
|
||||
.iter()
|
||||
.map(|ref d| d
|
||||
.generate(line_number, true, self)
|
||||
.text)
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
))
|
||||
} else {
|
||||
Some("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// It wraps.
|
||||
let text = chars.by_ref().take(available).collect::<String>();
|
||||
cursor = 0;
|
||||
remaining -= available;
|
||||
|
||||
write!(
|
||||
handle,
|
||||
"{}\n{}",
|
||||
as_terminal_escaped(
|
||||
style,
|
||||
&*format!(
|
||||
"{}{}{}",
|
||||
self.ansi_prefix_sgr, ansi_prefix, text
|
||||
),
|
||||
self.config.true_color,
|
||||
self.config.colored_output,
|
||||
self.config.use_italic_text,
|
||||
background_color
|
||||
),
|
||||
panel_wrap.clone().unwrap()
|
||||
)?;
|
||||
}
|
||||
|
||||
// Clear the ANSI prefix buffer.
|
||||
ansi_prefix.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(background_color) = background_color {
|
||||
let mut ansi_style = Style::default();
|
||||
ansi_style.background =
|
||||
Some(to_ansi_color(background_color, self.config.true_color));
|
||||
|
||||
write!(
|
||||
handle,
|
||||
"{}",
|
||||
ansi_style.paint(" ".repeat(cursor_max - cursor))
|
||||
)?;
|
||||
}
|
||||
writeln!(handle)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_GUTTER_COLOR: u8 = 238;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Colors {
|
||||
pub grid: Style,
|
||||
pub filename: Style,
|
||||
pub git_added: Style,
|
||||
pub git_removed: Style,
|
||||
pub git_modified: Style,
|
||||
pub line_number: Style,
|
||||
}
|
||||
|
||||
impl Colors {
|
||||
fn plain() -> Self {
|
||||
Colors::default()
|
||||
}
|
||||
|
||||
fn colored(theme: &Theme, true_color: bool) -> Self {
|
||||
let gutter_color = theme
|
||||
.settings
|
||||
.gutter_foreground
|
||||
.map(|c| to_ansi_color(c, true_color))
|
||||
.unwrap_or(Fixed(DEFAULT_GUTTER_COLOR));
|
||||
|
||||
Colors {
|
||||
grid: gutter_color.normal(),
|
||||
filename: Style::new().bold(),
|
||||
git_added: Green.normal(),
|
||||
git_removed: Red.normal(),
|
||||
git_modified: Yellow.normal(),
|
||||
line_number: gutter_color.normal(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user