summaryrefslogtreecommitdiffstats
path: root/src/bat_utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/bat_utils')
-rw-r--r--src/bat_utils/LICENSE26
-rw-r--r--src/bat_utils/assets.rs144
-rw-r--r--src/bat_utils/dirs.rs41
-rw-r--r--src/bat_utils/less.rs62
-rw-r--r--src/bat_utils/mod.rs5
-rw-r--r--src/bat_utils/output.rs160
-rw-r--r--src/bat_utils/terminal.rs53
7 files changed, 491 insertions, 0 deletions
diff --git a/src/bat_utils/LICENSE b/src/bat_utils/LICENSE
new file mode 100644
index 00000000..8af3b8b8
--- /dev/null
+++ b/src/bat_utils/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/bat_utils/assets.rs b/src/bat_utils/assets.rs
new file mode 100644
index 00000000..6715a821
--- /dev/null
+++ b/src/bat_utils/assets.rs
@@ -0,0 +1,144 @@
+// Based on code from https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468
+// See src/bat_utils/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::bat_utils::dirs::PROJECT_DIRS;
+use crate::errors::*;
+
+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/bat_utils/dirs.rs b/src/bat_utils/dirs.rs
new file mode 100644
index 00000000..1706f10a
--- /dev/null
+++ b/src/bat_utils/dirs.rs
@@ -0,0 +1,41 @@
+// Based on code from https://github.com/sharkdp/bat e981e974076a926a38f124b7d8746de2ca5f0a28
+// See src/bat_utils/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().expect("Could not get home directory");
+}
diff --git a/src/bat_utils/less.rs b/src/bat_utils/less.rs
new file mode 100644
index 00000000..f7429333
--- /dev/null
+++ b/src/bat_utils/less.rs
@@ -0,0 +1,62 @@
+use std::process::Command;
+
+pub fn retrieve_less_version() -> Option<usize> {
+ let cmd = Command::new("less").arg("--version").output().ok()?;
+ parse_less_version(&cmd.stdout)
+}
+
+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(' ')?;
+ 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/bat_utils/mod.rs b/src/bat_utils/mod.rs
new file mode 100644
index 00000000..7812e7c0
--- /dev/null
+++ b/src/bat_utils/mod.rs
@@ -0,0 +1,5 @@
+pub mod assets;
+pub mod dirs;
+mod less;
+pub mod output;
+pub mod terminal;
diff --git a/src/bat_utils/output.rs b/src/bat_utils/output.rs
new file mode 100644
index 00000000..eef00cd5
--- /dev/null
+++ b/src/bat_utils/output.rs
@@ -0,0 +1,160 @@
+// https://github.com/sharkdp/bat a1b9334a44a2c652f52dddaa83dbacba57372468
+// src/output.rs
+// See src/bat_utils/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::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<&str>,
+ 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<&str>,
+ 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,
+ };
+
+ 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 pager_path = PathBuf::from(pager_name);
+
+ 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"]);
+
+ // 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
+ } else {
+ let mut p = Command::new(&pager_path);
+ p.args(args);
+ p
+ };
+ if config.navigate {
+ process.args(&["--pattern", &navigate::make_navigate_regexp(&config)]);
+ }
+ Ok(process
+ .env("LESSANSIENDCHARS", "mK")
+ .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();
+ }
+ }
+}
diff --git a/src/bat_utils/terminal.rs b/src/bat_utils/terminal.rs
new file mode 100644
index 00000000..50351c48
--- /dev/null
+++ b/src/bat_utils/terminal.rs
@@ -0,0 +1,53 @@
+extern crate ansi_colours;
+
+use ansi_term::Colour::{Fixed, RGB};
+use ansi_term::{self, Style};
+
+use syntect::highlighting::{self, FontStyle};
+
+pub fn to_ansi_color(color: highlighting::Color, true_color: bool) -> ansi_term::Colour {
+ 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 color palette number. The built-in themes ansi-light,
+ // ansi-dark, and base16 use this.
+ Fixed(color.r)
+ } else if true_color {
+ RGB(color.r, color.g, color.b)
+ } else {
+ 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 color = to_ansi_color(style.foreground, true_color);
+
+ if style.font_style.contains(FontStyle::BOLD) {
+ color.bold()
+ } else if style.font_style.contains(FontStyle::UNDERLINE) {
+ color.underline()
+ } else if italics && style.font_style.contains(FontStyle::ITALIC) {
+ color.italic()
+ } else {
+ color.normal()
+ }
+ };
+
+ style.background = background_color.map(|c| to_ansi_color(c, true_color));
+ style.paint(text).to_string()
+}