summaryrefslogtreecommitdiffstats
path: root/src/utils/bat
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2021-11-15 18:45:58 -0500
committerDan Davison <dandavison7@gmail.com>2021-11-15 21:03:10 -0500
commit45c802528adcb5646ec5fe0d92b8e59119c80fb7 (patch)
treedde0969b8c3f27b4ba431c81550e9a947d4ac064 /src/utils/bat
parent24a07ff9474dc8dc4942026b7d6c3804132cb047 (diff)
Refactor: utils module
Diffstat (limited to 'src/utils/bat')
-rw-r--r--src/utils/bat/LICENSE26
-rw-r--r--src/utils/bat/assets.rs144
-rw-r--r--src/utils/bat/dirs.rs41
-rw-r--r--src/utils/bat/less.rs66
-rw-r--r--src/utils/bat/mod.rs5
-rw-r--r--src/utils/bat/output.rs204
-rw-r--r--src/utils/bat/terminal.rs83
7 files changed, 569 insertions, 0 deletions
diff --git a/src/utils/bat/LICENSE b/src/utils/bat/LICENSE
new file mode 100644
index 00000000..8af3b8b8
--- /dev/null
+++ b/src/utils/bat/LICENSE
@@ -0,0 +1,26 @@
+Files under this directory (i.e. src/bat_utils/) originated as copies of
+files from the `bat` project (https://github.com/sharkdp/bat), with
+subsequent modifications, as recorded in git history. The `bat`
+license is reproduced below.
+
+-----------------------------------------------------------------------------
+
+Copyright (c) 2018 bat-developers (https://github.com/sharkdp/bat).
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/utils/bat/assets.rs b/src/utils/bat/assets.rs
new file mode 100644
index 00000000..cfe0f5bd
--- /dev/null
+++ b/src/utils/bat/assets.rs
@@ -0,0 +1,144 @@
+// Based on code from https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468
+// See src/utils/bat/LICENSE
+
+use std::fs::File;
+use std::io::{self, BufReader, Write};
+use std::path::PathBuf;
+
+use ansi_term::Colour::Green;
+use ansi_term::Style;
+use syntect::dumps::{from_binary, from_reader};
+use syntect::highlighting::ThemeSet;
+use syntect::parsing::SyntaxSet;
+
+use crate::errors::*;
+use crate::utils::bat::dirs::PROJECT_DIRS;
+
+pub struct HighlightingAssets {
+ pub syntax_set: SyntaxSet,
+ pub theme_set: ThemeSet,
+}
+
+impl HighlightingAssets {
+ pub fn new() -> Self {
+ Self::from_cache().unwrap_or_else(|_| Self::from_binary())
+ }
+
+ fn get_integrated_syntaxset() -> SyntaxSet {
+ from_binary(include_bytes!("../../../etc/assets/syntaxes.bin"))
+ }
+
+ fn get_integrated_themeset() -> ThemeSet {
+ from_binary(include_bytes!("../../../etc/assets/themes.bin"))
+ }
+
+ fn from_cache() -> Result<Self> {
+ let theme_set_path = theme_set_path();
+ let syntax_set_file = File::open(&syntax_set_path()).chain_err(|| {
+ format!(
+ "Could not load cached syntax set '{}'",
+ syntax_set_path().to_string_lossy()
+ )
+ })?;
+ let syntax_set: SyntaxSet = from_reader(BufReader::new(syntax_set_file))
+ .chain_err(|| "Could not parse cached syntax set")?;
+
+ let theme_set_file = File::open(&theme_set_path).chain_err(|| {
+ format!(
+ "Could not load cached theme set '{}'",
+ theme_set_path.to_string_lossy()
+ )
+ })?;
+ let theme_set: ThemeSet = from_reader(BufReader::new(theme_set_file))
+ .chain_err(|| "Could not parse cached theme set")?;
+
+ Ok(HighlightingAssets {
+ syntax_set,
+ theme_set,
+ })
+ }
+
+ fn from_binary() -> Self {
+ let syntax_set = Self::get_integrated_syntaxset();
+ let theme_set = Self::get_integrated_themeset();
+
+ HighlightingAssets {
+ syntax_set,
+ theme_set,
+ }
+ }
+}
+
+fn theme_set_path() -> PathBuf {
+ PROJECT_DIRS.cache_dir().join("themes.bin")
+}
+
+fn syntax_set_path() -> PathBuf {
+ PROJECT_DIRS.cache_dir().join("syntaxes.bin")
+}
+
+pub fn list_languages() -> std::io::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 loop_through = false;
+ let colored_output = true;
+
+ let stdout = io::stdout();
+ let mut stdout = stdout.lock();
+
+ if loop_through {
+ for lang in languages {
+ writeln!(stdout, "{}:{}", 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 = 100;
+
+ let style = if 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(())
+}
diff --git a/src/utils/bat/dirs.rs b/src/utils/bat/dirs.rs
new file mode 100644
index 00000000..efaef81f
--- /dev/null
+++ b/src/utils/bat/dirs.rs
@@ -0,0 +1,41 @@
+// Based on code from https://github.com/sharkdp/bat e981e974076a926a38f124b7d8746de2ca5f0a28
+// See src/utils/bat/LICENSE
+
+use lazy_static::lazy_static;
+use std::path::{Path, PathBuf};
+
+#[cfg(target_os = "macos")]
+use std::env;
+
+/// Wrapper for 'dirs' that treats MacOS more like Linux, by following the XDG specification.
+/// This means that the `XDG_CACHE_HOME` and `XDG_CONFIG_HOME` environment variables are
+/// checked first. The fallback directories are `~/.cache/bat` and `~/.config/bat`, respectively.
+pub struct BatProjectDirs {
+ cache_dir: PathBuf,
+}
+
+impl BatProjectDirs {
+ fn new() -> Option<BatProjectDirs> {
+ #[cfg(target_os = "macos")]
+ let cache_dir_op = env::var_os("XDG_CACHE_HOME")
+ .map(PathBuf::from)
+ .filter(|p| p.is_absolute())
+ .or_else(|| dirs_next::home_dir().map(|d| d.join(".cache")));
+
+ #[cfg(not(target_os = "macos"))]
+ let cache_dir_op = dirs_next::cache_dir();
+
+ let cache_dir = cache_dir_op.map(|d| d.join("bat"))?;
+
+ Some(BatProjectDirs { cache_dir })
+ }
+
+ pub fn cache_dir(&self) -> &Path {
+ &self.cache_dir
+ }
+}
+
+lazy_static! {
+ pub static ref PROJECT_DIRS: BatProjectDirs =
+ BatProjectDirs::new().unwrap_or_else(|| panic!("Could not get home directory"));
+}
diff --git a/src/utils/bat/less.rs b/src/utils/bat/less.rs
new file mode 100644
index 00000000..1ca9f76f
--- /dev/null
+++ b/src/utils/bat/less.rs
@@ -0,0 +1,66 @@
+use std::process::Command;
+
+pub fn retrieve_less_version() -> Option<usize> {
+ if let Ok(less_path) = grep_cli::resolve_binary("less") {
+ let cmd = Command::new(less_path).arg("--version").output().ok()?;
+ parse_less_version(&cmd.stdout)
+ } else {
+ None
+ }
+}
+
+fn parse_less_version(output: &[u8]) -> Option<usize> {
+ if output.starts_with(b"less ") {
+ let version = std::str::from_utf8(&output[5..]).ok()?;
+ let end = version.find(|c: char| !c.is_ascii_digit())?;
+ version[..end].parse::<usize>().ok()
+ } else {
+ None
+ }
+}
+
+#[test]
+fn test_parse_less_version_487() {
+ let output = b"less 487 (GNU regular expressions)
+Copyright (C) 1984-2016 Mark Nudelman
+
+less comes with NO WARRANTY, to the extent permitted by law.
+For information about the terms of redistribution,
+see the file named README in the less distribution.
+Homepage: http://www.greenwoodsoftware.com/less";
+
+ assert_eq!(Some(487), parse_less_version(output));
+}
+
+#[test]
+fn test_parse_less_version_529() {
+ let output = b"less 529 (Spencer V8 regular expressions)
+Copyright (C) 1984-2017 Mark Nudelman
+
+less comes with NO WARRANTY, to the extent permitted by law.
+For information about the terms of redistribution,
+see the file named README in the less distribution.
+Homepage: http://www.greenwoodsoftware.com/less";
+
+ assert_eq!(Some(529), parse_less_version(output));
+}
+
+#[test]
+fn test_parse_less_version_551() {
+ let output = b"less 551 (PCRE regular expressions)
+Copyright (C) 1984-2019 Mark Nudelman
+
+less comes with NO WARRANTY, to the extent permitted by law.
+For information about the terms of redistribution,
+see the file named README in the less distribution.
+Home page: http://www.greenwoodsoftware.com/less";
+
+ assert_eq!(Some(551), parse_less_version(output));
+}
+
+#[test]
+fn test_parse_less_version_wrong_program() {
+ let output = b"more from util-linux 2.34";
+
+ assert_eq!(None, parse_less_version(output));
+}
diff --git a/src/utils/bat/mod.rs b/src/utils/bat/mod.rs
new file mode 100644
index 00000000..7812e7c0
--- /dev/null
+++ b/src/utils/bat/mod.rs
@@ -0,0 +1,5 @@
+pub mod assets;
+pub mod dirs;
+mod less;
+pub mod output;
+pub mod terminal;
diff --git a/src/utils/bat/output.rs b/src/utils/bat/output.rs
new file mode 100644
index 00000000..a231168b
--- /dev/null
+++ b/src/utils/bat/output.rs
@@ -0,0 +1,204 @@
+// https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468
+// src/output.rs
+// See src/utils/bat/LICENSE
+use std::env;
+use std::ffi::OsString;
+use std::io::{self, Write};
+use std::path::PathBuf;
+use std::process::{Child, Command, Stdio};
+
+use super::less::retrieve_less_version;
+
+use crate::config;
+use crate::fatal;
+use crate::features::navigate;
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+#[allow(dead_code)]
+pub enum PagingMode {
+ Always,
+ QuitIfOneScreen,
+ Never,
+}
+use crate::errors::*;
+
+pub enum OutputType {
+ Pager(Child),
+ Stdout(io::Stdout),
+}
+
+impl OutputType {
+ pub fn from_mode(
+ mode: PagingMode,
+ pager: Option<String>,
+ config: &config::Config,
+ ) -> Result<Self> {
+ use self::PagingMode::*;
+ Ok(match mode {
+ Always => OutputType::try_pager(false, pager, config)?,
+ QuitIfOneScreen => OutputType::try_pager(true, pager, config)?,
+ _ => 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<String>,
+ config: &config::Config,
+ ) -> Result<Self> {
+ let mut replace_arguments_to_less = false;
+
+ let pager_from_env = match (
+ env::var("DELTA_PAGER"),
+ env::var("BAT_PAGER"),
+ env::var("PAGER"),
+ ) {
+ (Ok(delta_pager), _, _) => Some(delta_pager),
+ (_, Ok(bat_pager), _) => Some(bat_pager),
+ (_, _, Ok(pager)) => {
+ // less needs to be called with the '-R' option in order to properly interpret ANSI
+ // color sequences. If someone has set PAGER="less -F", we therefore need to
+ // overwrite the arguments and add '-R'.
+ // We only do this for PAGER, since it is used in other contexts.
+ replace_arguments_to_less = true;
+ Some(pager)
+ }
+ _ => None,
+ };
+
+ 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.")?;
+
+ Ok(match pagerflags.split_first() {
+ Some((pager_name, args)) => {
+ let pager_path = PathBuf::from(pager_name);
+
+ let is_less = pager_path.file_stem() == Some(&OsString::from("less"));
+
+ let process = if is_less {
+ _make_process_from_less_path(
+ pager_path,
+ args,
+ replace_arguments_to_less,
+ quit_if_one_screen,
+ config,
+ )
+ } else {
+ _make_process_from_pager_path(pager_path, args)
+ };
+ if let Some(mut process) = process {
+ process
+ .stdin(Stdio::piped())
+ .spawn()
+ .map(OutputType::Pager)
+ .unwrap_or_else(|_| OutputType::stdout())
+ } else {
+ OutputType::stdout()
+ }
+ }
+ None => 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,
+ })
+ }
+}
+
+fn _make_process_from_less_path(
+ less_path: PathBuf,
+ args: &[String],
+ replace_arguments_to_less: bool,
+ quit_if_one_screen: bool,
+ config: &config::Config,
+) -> Option<Command> {
+ if let Ok(less_path) = grep_cli::resolve_binary(less_path) {
+ let mut p = Command::new(&less_path);
+ if args.is_empty() || replace_arguments_to_less {
+ p.args(vec!["--RAW-CONTROL-CHARS"]);
+
+ // Passing '--no-init' fixes a bug with '--quit-if-one-screen' in older
+ // versions of 'less'. Unfortunately, it also breaks mouse-wheel support.
+ //
+ // See: http://www.greenwoodsoftware.com/less/news.530.html
+ //
+ // For newer versions (530 or 558 on Windows), we omit '--no-init' as it
+ // is not needed anymore.
+ match retrieve_less_version() {
+ None => {
+ p.arg("--no-init");
+ }
+ Some(version) if (version < 530 || (cfg!(windows) && version < 558)) => {
+ p.arg("--no-init");
+ }
+ _ => {}
+ }
+
+ if quit_if_one_screen {
+ p.arg("--quit-if-one-screen");
+ }
+ } else {
+ p.args(args);
+ }
+ p.env("LESSCHARSET", "UTF-8");
+ p.env("LESSANSIENDCHARS", "mK");
+ if config.navigate {
+ if let Ok(hist_file) = navigate::copy_less_hist_file_and_append_navigate_regexp(config)
+ {
+ p.env("LESSHISTFILE", hist_file);
+ if config.show_themes {
+ p.arg("+n");
+ }
+ }
+ }
+ Some(p)
+ } else {
+ None
+ }
+}
+
+fn _make_process_from_pager_path(pager_path: PathBuf, args: &[String]) -> Option<Command> {
+ if pager_path.file_stem() == Some(&OsString::from("delta")) {
+ fatal(
+ "\
+It looks like you have set delta as the value of $PAGER. \
+This would result in a non-terminating recursion. \
+delta is not an appropriate value for $PAGER \
+(but it is an appropriate value for $GIT_PAGER).",
+ );
+ }
+ if let Ok(pager_path) = grep_cli::resolve_binary(pager_path) {
+ let mut p = Command::new(&pager_path);
+ p.args(args);
+ Some(p)
+ } else {
+ None
+ }
+}
+
+impl Drop for OutputType {
+ fn drop(&mut self) {
+ if let OutputType::Pager(ref mut command) = *self {
+ let _ = command.wait();
+ }
+ }
+}
diff --git a/src/utils/bat/terminal.rs b/src/utils/bat/terminal.rs
new file mode 100644
index 00000000..15c75d24
--- /dev/null
+++ b/src/utils/bat/terminal.rs
@@ -0,0 +1,83 @@
+use ansi_term::Color::{self, Fixed, RGB};
+use ansi_term::{self, Style};
+
+use syntect::highlighting::{self, FontStyle};
+
+pub fn to_ansi_color(color: highlighting::Color, true_color: bool) -> Option<ansi_term::Color> {
+ if color.a == 0 {
+ // Themes can specify one of the user-configurable terminal colors by
+ // encoding them as #RRGGBBAA with AA set to 00 (transparent) and RR set
+ // to the 8-bit color palette number. The built-in themes ansi, base16,
+ // and base16-256 use this.
+ Some(match color.r {
+ // For the first 8 colors, use the Color enum to produce ANSI escape
+ // sequences using codes 30-37 (foreground) and 40-47 (background).
+ // For example, red foreground is \x1b[31m. This works on terminals
+ // without 256-color support.
+ 0x00 => Color::Black,
+ 0x01 => Color::Red,
+ 0x02 => Color::Green,
+ 0x03 => Color::Yellow,
+ 0x04 => Color::Blue,
+ 0x05 => Color::Purple,
+ 0x06 => Color::Cyan,
+ 0x07 => Color::White,
+ // For all other colors, use Fixed to produce escape sequences using
+ // codes 38;5 (foreground) and 48;5 (background). For example,
+ // bright red foreground is \x1b[38;5;9m. This only works on
+ // terminals with 256-color support.
+ //
+ // TODO: When ansi_term adds support for bright variants using codes
+ // 90-97 (foreground) and 100-107 (background), we should use those
+ // for values 0x08 to 0x0f and only use Fixed for 0x10 to 0xff.
+ n => Fixed(n),
+ })
+ } else if color.a == 1 {
+ // Themes can specify the terminal's default foreground/background color
+ // (i.e. no escape sequence) using the encoding #RRGGBBAA with AA set to
+ // 01. The built-in theme ansi uses this.
+ None
+ } else if true_color {
+ Some(RGB(color.r, color.g, color.b))
+ } else {
+ Some(Fixed(ansi_colours::ansi256_from_rgb((
+ color.r, color.g, color.b,
+ ))))
+ }
+}
+
+#[allow(dead_code)]
+pub fn as_terminal_escaped(
+ style: highlighting::Style,
+ text: &str,
+ true_color: bool,
+ colored: bool,
+ italics: bool,
+ background_color: Option<highlighting::Color>,
+) -> String {
+ if text.is_empty() {
+ return text.to_string();
+ }
+
+ let mut style = if !colored {
+ Style::default()
+ } else {
+ let mut color = Style {
+ foreground: to_ansi_color(style.foreground, true_color),
+ ..Style::default()
+ };
+ if style.font_style.contains(FontStyle::BOLD) {
+ color = color.bold();
+ }
+ if style.font_style.contains(FontStyle::UNDERLINE) {
+ color = color.underline();
+ }
+ if italics && style.font_style.contains(FontStyle::ITALIC) {
+ color = color.italic();
+ }
+ color
+ };
+
+ style.background = background_color.and_then(|c| to_ansi_color(c, true_color));
+ style.paint(text).to_string()
+}