diff options
Diffstat (limited to 'src/bat_utils')
-rw-r--r-- | src/bat_utils/LICENSE | 26 | ||||
-rw-r--r-- | src/bat_utils/assets.rs | 144 | ||||
-rw-r--r-- | src/bat_utils/dirs.rs | 41 | ||||
-rw-r--r-- | src/bat_utils/less.rs | 62 | ||||
-rw-r--r-- | src/bat_utils/mod.rs | 5 | ||||
-rw-r--r-- | src/bat_utils/output.rs | 160 | ||||
-rw-r--r-- | src/bat_utils/terminal.rs | 53 |
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() +} |