mirror of
https://github.com/sharkdp/bat.git
synced 2025-09-02 03:12:25 +01:00
Merge branch 'master' into fix_654_stdin_filename
This commit is contained in:
232
src/assets.rs
232
src/assets.rs
@@ -1,41 +1,35 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File};
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use syntect::dumps::{dump_to_file, from_binary, from_reader};
|
||||
use syntect::highlighting::{Theme, ThemeSet};
|
||||
use syntect::parsing::{SyntaxReference, SyntaxSet, SyntaxSetBuilder};
|
||||
|
||||
use crate::dirs::PROJECT_DIRS;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::inputfile::{InputFile, InputFileReader};
|
||||
use crate::syntax_mapping::SyntaxMapping;
|
||||
|
||||
pub const BAT_THEME_DEFAULT: &str = "Monokai Extended";
|
||||
use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HighlightingAssets {
|
||||
pub syntax_set: SyntaxSet,
|
||||
pub theme_set: ThemeSet,
|
||||
pub(crate) syntax_set: SyntaxSet,
|
||||
pub(crate) theme_set: ThemeSet,
|
||||
fallback_theme: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl HighlightingAssets {
|
||||
pub fn new() -> Self {
|
||||
Self::from_cache().unwrap_or_else(|_| Self::from_binary())
|
||||
pub fn default_theme() -> &'static str {
|
||||
"Monokai Extended"
|
||||
}
|
||||
|
||||
pub fn from_files(dir: Option<&Path>, start_empty: bool) -> Result<Self> {
|
||||
let source_dir = dir.unwrap_or_else(|| PROJECT_DIRS.config_dir());
|
||||
|
||||
let mut theme_set = if start_empty {
|
||||
pub fn from_files(source_dir: &Path, include_integrated_assets: bool) -> Result<Self> {
|
||||
let mut theme_set = if include_integrated_assets {
|
||||
Self::get_integrated_themeset()
|
||||
} else {
|
||||
ThemeSet {
|
||||
themes: BTreeMap::new(),
|
||||
}
|
||||
} else {
|
||||
Self::get_integrated_themeset()
|
||||
};
|
||||
|
||||
let theme_dir = source_dir.join("themes");
|
||||
@@ -48,7 +42,7 @@ impl HighlightingAssets {
|
||||
);
|
||||
}
|
||||
|
||||
let mut syntax_set_builder = if start_empty {
|
||||
let mut syntax_set_builder = if !include_integrated_assets {
|
||||
let mut builder = SyntaxSetBuilder::new();
|
||||
builder.add_plain_text_syntax();
|
||||
builder
|
||||
@@ -69,15 +63,15 @@ impl HighlightingAssets {
|
||||
Ok(HighlightingAssets {
|
||||
syntax_set: syntax_set_builder.build(),
|
||||
theme_set,
|
||||
fallback_theme: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_cache() -> Result<Self> {
|
||||
let theme_set_path = theme_set_path();
|
||||
let syntax_set_file = File::open(&syntax_set_path()).chain_err(|| {
|
||||
pub fn from_cache(theme_set_path: &Path, syntax_set_path: &Path) -> Result<Self> {
|
||||
let syntax_set_file = File::open(syntax_set_path).chain_err(|| {
|
||||
format!(
|
||||
"Could not load cached syntax set '{}'",
|
||||
syntax_set_path().to_string_lossy()
|
||||
syntax_set_path.to_string_lossy()
|
||||
)
|
||||
})?;
|
||||
let syntax_set: SyntaxSet = from_reader(BufReader::new(syntax_set_file))
|
||||
@@ -95,6 +89,7 @@ impl HighlightingAssets {
|
||||
Ok(HighlightingAssets {
|
||||
syntax_set,
|
||||
theme_set,
|
||||
fallback_theme: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,18 +101,18 @@ impl HighlightingAssets {
|
||||
from_binary(include_bytes!("../assets/themes.bin"))
|
||||
}
|
||||
|
||||
fn from_binary() -> Self {
|
||||
pub fn from_binary() -> Self {
|
||||
let syntax_set = Self::get_integrated_syntaxset();
|
||||
let theme_set = Self::get_integrated_themeset();
|
||||
|
||||
HighlightingAssets {
|
||||
syntax_set,
|
||||
theme_set,
|
||||
fallback_theme: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(&self, dir: Option<&Path>) -> Result<()> {
|
||||
let target_dir = dir.unwrap_or_else(|| PROJECT_DIRS.cache_dir());
|
||||
pub fn save_to_cache(&self, target_dir: &Path) -> Result<()> {
|
||||
let _ = fs::create_dir_all(target_dir);
|
||||
let theme_set_path = target_dir.join("themes.bin");
|
||||
let syntax_set_path = target_dir.join("syntaxes.bin");
|
||||
@@ -149,22 +144,36 @@ impl HighlightingAssets {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_theme(&self, theme: &str) -> &Theme {
|
||||
pub fn set_fallback_theme(&mut self, theme: &'static str) {
|
||||
self.fallback_theme = Some(theme);
|
||||
}
|
||||
|
||||
pub fn syntaxes(&self) -> &[SyntaxReference] {
|
||||
self.syntax_set.syntaxes()
|
||||
}
|
||||
|
||||
pub fn themes(&self) -> impl Iterator<Item = &String> {
|
||||
self.theme_set.themes.keys()
|
||||
}
|
||||
|
||||
pub(crate) fn get_theme(&self, theme: &str) -> &Theme {
|
||||
match self.theme_set.themes.get(theme) {
|
||||
Some(theme) => theme,
|
||||
None => {
|
||||
use ansi_term::Colour::Yellow;
|
||||
eprintln!(
|
||||
"{}: Unknown theme '{}', using default.",
|
||||
Yellow.paint("[bat warning]"),
|
||||
theme
|
||||
);
|
||||
&self.theme_set.themes[BAT_THEME_DEFAULT]
|
||||
if theme != "" {
|
||||
use ansi_term::Colour::Yellow;
|
||||
eprintln!(
|
||||
"{}: Unknown theme '{}', using default.",
|
||||
Yellow.paint("[bat warning]"),
|
||||
theme
|
||||
);
|
||||
}
|
||||
&self.theme_set.themes[self.fallback_theme.unwrap_or(Self::default_theme())]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_syntax(
|
||||
pub(crate) fn get_syntax(
|
||||
&self,
|
||||
language: Option<&str>,
|
||||
filename: InputFile,
|
||||
@@ -175,25 +184,28 @@ impl HighlightingAssets {
|
||||
(Some(language), _) => self.syntax_set.find_syntax_by_token(language),
|
||||
(None, InputFile::Ordinary(filename)) => {
|
||||
let path = Path::new(filename);
|
||||
|
||||
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||
let extension = path.extension().and_then(|x| x.to_str()).unwrap_or("");
|
||||
|
||||
let file_name = mapping.replace(file_name);
|
||||
let extension = mapping.replace(extension);
|
||||
|
||||
let ext_syntax = self
|
||||
.syntax_set
|
||||
.find_syntax_by_extension(&file_name)
|
||||
.or_else(|| self.syntax_set.find_syntax_by_extension(&extension));
|
||||
let line_syntax = if ext_syntax.is_none() {
|
||||
String::from_utf8(reader.first_line.clone())
|
||||
.ok()
|
||||
.and_then(|l| self.syntax_set.find_syntax_by_first_line(&l))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let line_syntax = String::from_utf8(reader.first_line.clone())
|
||||
.ok()
|
||||
.and_then(|l| self.syntax_set.find_syntax_by_first_line(&l));
|
||||
|
||||
ext_syntax.or(line_syntax)
|
||||
let absolute_path = path.canonicalize().ok().unwrap_or(path.to_owned());
|
||||
match mapping.get_syntax_for(absolute_path) {
|
||||
Some(MappingTarget::MapTo(syntax_name)) => {
|
||||
// TODO: we should probably return an error here if this syntax can not be
|
||||
// found. Currently, we just fall back to 'plain'.
|
||||
self.syntax_set.find_syntax_by_name(syntax_name)
|
||||
}
|
||||
Some(MappingTarget::MapToUnknown) => line_syntax,
|
||||
None => ext_syntax.or(line_syntax),
|
||||
}
|
||||
}
|
||||
(None, InputFile::StdIn) => String::from_utf8(reader.first_line.clone())
|
||||
.ok()
|
||||
@@ -205,28 +217,120 @@ impl HighlightingAssets {
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_set_path() -> PathBuf {
|
||||
PROJECT_DIRS.cache_dir().join("themes.bin")
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
fn syntax_set_path() -> PathBuf {
|
||||
PROJECT_DIRS.cache_dir().join("syntaxes.bin")
|
||||
}
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub fn config_dir() -> Cow<'static, str> {
|
||||
PROJECT_DIRS.config_dir().to_string_lossy()
|
||||
}
|
||||
use crate::assets::HighlightingAssets;
|
||||
use crate::inputfile::InputFile;
|
||||
use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
|
||||
|
||||
pub fn cache_dir() -> Cow<'static, str> {
|
||||
PROJECT_DIRS.cache_dir().to_string_lossy()
|
||||
}
|
||||
struct SyntaxDetectionTest<'a> {
|
||||
assets: HighlightingAssets,
|
||||
pub syntax_mapping: SyntaxMapping<'a>,
|
||||
temp_dir: TempDir,
|
||||
}
|
||||
|
||||
pub fn clear_assets() {
|
||||
print!("Clearing theme set cache ... ");
|
||||
fs::remove_file(theme_set_path()).ok();
|
||||
println!("okay");
|
||||
impl<'a> SyntaxDetectionTest<'a> {
|
||||
fn new() -> Self {
|
||||
SyntaxDetectionTest {
|
||||
assets: HighlightingAssets::from_binary(),
|
||||
syntax_mapping: SyntaxMapping::builtin(),
|
||||
temp_dir: TempDir::new("bat_syntax_detection_tests")
|
||||
.expect("creation of temporary directory"),
|
||||
}
|
||||
}
|
||||
|
||||
print!("Clearing syntax set cache ... ");
|
||||
fs::remove_file(syntax_set_path()).ok();
|
||||
println!("okay");
|
||||
fn synax_for_file_with_content(&self, file_name: &str, first_line: &str) -> String {
|
||||
let file_path = self.temp_dir.path().join(file_name);
|
||||
{
|
||||
let mut temp_file = File::create(&file_path).unwrap();
|
||||
writeln!(temp_file, "{}", first_line).unwrap();
|
||||
}
|
||||
|
||||
let input_file = InputFile::Ordinary(OsStr::new(&file_path));
|
||||
let syntax = self.assets.get_syntax(
|
||||
None,
|
||||
input_file,
|
||||
&mut input_file.get_reader(&io::stdin()).unwrap(),
|
||||
&self.syntax_mapping,
|
||||
);
|
||||
|
||||
syntax.name.clone()
|
||||
}
|
||||
|
||||
fn syntax_for_file(&self, file_name: &str) -> String {
|
||||
self.synax_for_file_with_content(file_name, "")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_detection_basic() {
|
||||
let test = SyntaxDetectionTest::new();
|
||||
|
||||
assert_eq!(test.syntax_for_file("test.rs"), "Rust");
|
||||
assert_eq!(test.syntax_for_file("test.cpp"), "C++");
|
||||
assert_eq!(test.syntax_for_file("test.build"), "NAnt Build File");
|
||||
assert_eq!(
|
||||
test.syntax_for_file("PKGBUILD"),
|
||||
"Bourne Again Shell (bash)"
|
||||
);
|
||||
assert_eq!(test.syntax_for_file(".bashrc"), "Bourne Again Shell (bash)");
|
||||
assert_eq!(test.syntax_for_file("Makefile"), "Makefile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_detection_well_defined_mapping_for_duplicate_extensions() {
|
||||
let test = SyntaxDetectionTest::new();
|
||||
|
||||
assert_eq!(test.syntax_for_file("test.h"), "C++");
|
||||
assert_eq!(test.syntax_for_file("test.sass"), "Sass");
|
||||
assert_eq!(test.syntax_for_file("test.hs"), "Haskell (improved)");
|
||||
assert_eq!(test.syntax_for_file("test.js"), "JavaScript (Babel)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_detection_first_line() {
|
||||
let test = SyntaxDetectionTest::new();
|
||||
|
||||
assert_eq!(
|
||||
test.synax_for_file_with_content("my_script", "#!/bin/bash"),
|
||||
"Bourne Again Shell (bash)"
|
||||
);
|
||||
assert_eq!(
|
||||
test.synax_for_file_with_content("build", "#!/bin/bash"),
|
||||
"Bourne Again Shell (bash)"
|
||||
);
|
||||
assert_eq!(
|
||||
test.synax_for_file_with_content("my_script", "<?php"),
|
||||
"PHP"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_detection_with_custom_mapping() {
|
||||
let mut test = SyntaxDetectionTest::new();
|
||||
|
||||
assert_eq!(test.syntax_for_file("test.h"), "C++");
|
||||
test.syntax_mapping
|
||||
.insert("*.h", MappingTarget::MapTo("C"))
|
||||
.ok();
|
||||
assert_eq!(test.syntax_for_file("test.h"), "C");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_detection_is_case_sensitive() {
|
||||
let mut test = SyntaxDetectionTest::new();
|
||||
|
||||
assert_ne!(test.syntax_for_file("README.MD"), "Markdown");
|
||||
test.syntax_mapping
|
||||
.insert("*.MD", MappingTarget::MapTo("Markdown"))
|
||||
.ok();
|
||||
assert_eq!(test.syntax_for_file("README.MD"), "Markdown");
|
||||
}
|
||||
}
|
||||
|
@@ -17,13 +17,12 @@ use console::Term;
|
||||
use ansi_term;
|
||||
|
||||
use bat::{
|
||||
assets::BAT_THEME_DEFAULT,
|
||||
config::{
|
||||
Config, HighlightedLineRanges, InputFile, LineRange, LineRanges, MappingTarget, OutputWrap,
|
||||
PagingMode, StyleComponent, StyleComponents, SyntaxMapping,
|
||||
},
|
||||
errors::*,
|
||||
inputfile::InputFile,
|
||||
line_range::{LineRange, LineRanges},
|
||||
style::{OutputComponent, OutputComponents, OutputWrap},
|
||||
syntax_mapping::SyntaxMapping,
|
||||
Config, PagingMode,
|
||||
HighlightingAssets,
|
||||
};
|
||||
|
||||
fn is_truecolor_terminal() -> bool {
|
||||
@@ -79,7 +78,7 @@ impl App {
|
||||
|
||||
pub fn config(&self) -> Result<Config> {
|
||||
let files = self.files();
|
||||
let output_components = self.output_components()?;
|
||||
let style_components = self.style_components()?;
|
||||
|
||||
let paging_mode = match self.matches.value_of("paging") {
|
||||
Some("always") => PagingMode::Always,
|
||||
@@ -105,17 +104,17 @@ impl App {
|
||||
}
|
||||
};
|
||||
|
||||
let mut syntax_mapping = SyntaxMapping::new();
|
||||
let mut syntax_mapping = SyntaxMapping::builtin();
|
||||
|
||||
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());
|
||||
return Err("Invalid syntax mapping. The format of the -m/--map-syntax option is '<glob-pattern>:<syntax-name>'. For example: '*.cpp:C++'.".into());
|
||||
}
|
||||
|
||||
syntax_mapping.insert(parts[0], parts[1]);
|
||||
syntax_mapping.insert(parts[0], MappingTarget::MapTo(parts[1]))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +157,7 @@ impl App {
|
||||
Some("character") => OutputWrap::Character,
|
||||
Some("never") => OutputWrap::None,
|
||||
Some("auto") | _ => {
|
||||
if output_components.plain() {
|
||||
if style_components.plain() {
|
||||
OutputWrap::None
|
||||
} else {
|
||||
OutputWrap::Character
|
||||
@@ -188,7 +187,7 @@ impl App {
|
||||
.or_else(|| env::var("BAT_TABS").ok())
|
||||
.and_then(|t| t.parse().ok())
|
||||
.unwrap_or(
|
||||
if output_components.plain() && paging_mode == PagingMode::Never {
|
||||
if style_components.plain() && paging_mode == PagingMode::Never {
|
||||
0
|
||||
} else {
|
||||
4
|
||||
@@ -201,33 +200,34 @@ impl App {
|
||||
.or_else(|| env::var("BAT_THEME").ok())
|
||||
.map(|s| {
|
||||
if s == "default" {
|
||||
String::from(BAT_THEME_DEFAULT)
|
||||
String::from(HighlightingAssets::default_theme())
|
||||
} else {
|
||||
s
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| String::from(BAT_THEME_DEFAULT)),
|
||||
line_ranges: LineRanges::from(
|
||||
self.matches
|
||||
.values_of("line-range")
|
||||
.map(|vs| vs.map(LineRange::from).collect())
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| vec![]),
|
||||
),
|
||||
output_components,
|
||||
.unwrap_or_else(|| String::from(HighlightingAssets::default_theme())),
|
||||
line_ranges: self
|
||||
.matches
|
||||
.values_of("line-range")
|
||||
.map(|vs| vs.map(LineRange::from).collect())
|
||||
.transpose()?
|
||||
.map(LineRanges::from)
|
||||
.unwrap_or_default(),
|
||||
style_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: LineRanges::from(
|
||||
self.matches
|
||||
.values_of("highlight-line")
|
||||
.map(|ws| ws.map(LineRange::from).collect())
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| vec![LineRange { lower: 0, upper: 0 }]),
|
||||
),
|
||||
highlighted_lines: self
|
||||
.matches
|
||||
.values_of("highlight-line")
|
||||
.map(|ws| ws.map(LineRange::from).collect())
|
||||
.transpose()?
|
||||
.map(LineRanges::from)
|
||||
.map(|lr| HighlightedLineRanges(lr))
|
||||
.unwrap_or_default(),
|
||||
filenames: self
|
||||
.matches
|
||||
.values_of("file-name")
|
||||
@@ -252,23 +252,23 @@ impl App {
|
||||
.unwrap_or_else(|| vec![InputFile::StdIn])
|
||||
}
|
||||
|
||||
fn output_components(&self) -> Result<OutputComponents> {
|
||||
fn style_components(&self) -> Result<StyleComponents> {
|
||||
let matches = &self.matches;
|
||||
Ok(OutputComponents(
|
||||
Ok(StyleComponents(
|
||||
if matches.value_of("decorations") == Some("never") {
|
||||
HashSet::new()
|
||||
} else if matches.is_present("number") {
|
||||
[OutputComponent::Numbers].iter().cloned().collect()
|
||||
[StyleComponent::Numbers].iter().cloned().collect()
|
||||
} else if matches.is_present("plain") {
|
||||
[OutputComponent::Plain].iter().cloned().collect()
|
||||
[StyleComponent::Plain].iter().cloned().collect()
|
||||
} else {
|
||||
let env_style_components: Option<Vec<OutputComponent>> = env::var("BAT_STYLE")
|
||||
let env_style_components: Option<Vec<StyleComponent>> = env::var("BAT_STYLE")
|
||||
.ok()
|
||||
.map(|style_str| {
|
||||
style_str
|
||||
.split(',')
|
||||
.map(|x| OutputComponent::from_str(&x))
|
||||
.collect::<Result<Vec<OutputComponent>>>()
|
||||
.map(|x| StyleComponent::from_str(&x))
|
||||
.collect::<Result<Vec<StyleComponent>>>()
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
@@ -277,12 +277,12 @@ impl App {
|
||||
.map(|styles| {
|
||||
styles
|
||||
.split(',')
|
||||
.map(|style| style.parse::<OutputComponent>())
|
||||
.map(|style| style.parse::<StyleComponent>())
|
||||
.filter_map(|style| style.ok())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.or(env_style_components)
|
||||
.unwrap_or_else(|| vec![OutputComponent::Full])
|
||||
.unwrap_or_else(|| vec![StyleComponent::Full])
|
||||
.into_iter()
|
||||
.map(|style| style.components(self.interactive_output))
|
||||
.fold(HashSet::new(), |mut acc, components| {
|
||||
|
38
src/bin/bat/assets.rs
Normal file
38
src/bin/bat/assets.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::directories::PROJECT_DIRS;
|
||||
|
||||
use bat::HighlightingAssets;
|
||||
|
||||
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 config_dir() -> Cow<'static, str> {
|
||||
PROJECT_DIRS.config_dir().to_string_lossy()
|
||||
}
|
||||
|
||||
pub fn cache_dir() -> Cow<'static, str> {
|
||||
PROJECT_DIRS.cache_dir().to_string_lossy()
|
||||
}
|
||||
|
||||
pub fn clear_assets() {
|
||||
print!("Clearing theme set cache ... ");
|
||||
fs::remove_file(theme_set_path()).ok();
|
||||
println!("okay");
|
||||
|
||||
print!("Clearing syntax set cache ... ");
|
||||
fs::remove_file(syntax_set_path()).ok();
|
||||
println!("okay");
|
||||
}
|
||||
|
||||
pub fn assets_from_cache_or_binary() -> HighlightingAssets {
|
||||
HighlightingAssets::from_cache(&theme_set_path(), &syntax_set_path())
|
||||
.unwrap_or(HighlightingAssets::from_binary())
|
||||
}
|
@@ -260,13 +260,13 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
|
||||
.multiple(true)
|
||||
.takes_value(true)
|
||||
.number_of_values(1)
|
||||
.value_name("from:to")
|
||||
.help("Map a file extension or name to an existing syntax.")
|
||||
.value_name("glob:syntax")
|
||||
.help("Use the specified syntax for files matching the glob pattern ('*.cpp:C++').")
|
||||
.long_help(
|
||||
"Map a file extension or file name to an existing syntax (specified by a file \
|
||||
extension or file name). For example, to highlight *.build files with the \
|
||||
Python syntax, use '-m build:py'. To highlight files named '.myignore' with \
|
||||
the Git Ignore syntax, use '-m .myignore:gitignore'.",
|
||||
"Map a glob pattern to an existing syntax name. The glob pattern is matched \
|
||||
on the full path and the filename. For example, to highlight *.build files \
|
||||
with the Python syntax, use -m '*.build:Python'. To highlight files named \
|
||||
'.myignore' with the Git Ignore syntax, use -m '.myignore:Git Ignore'.",
|
||||
)
|
||||
.takes_value(true),
|
||||
)
|
||||
@@ -293,7 +293,7 @@ pub fn build_app(interactive_output: bool) -> ClapApp<'static, 'static> {
|
||||
.arg(
|
||||
Arg::with_name("style")
|
||||
.long("style")
|
||||
.value_name("style-components")
|
||||
.value_name("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)
|
||||
|
@@ -5,7 +5,7 @@ use std::path::PathBuf;
|
||||
|
||||
use shell_words;
|
||||
|
||||
use bat::dirs::PROJECT_DIRS;
|
||||
use crate::directories::PROJECT_DIRS;
|
||||
|
||||
pub fn config_file() -> PathBuf {
|
||||
env::var("BAT_CONFIG_PATH")
|
||||
|
@@ -1,8 +1,8 @@
|
||||
use crate::dirs_rs;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::env;
|
||||
use dirs;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
/// 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
|
||||
@@ -14,25 +14,16 @@ pub struct BatProjectDirs {
|
||||
|
||||
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_rs::home_dir().map(|d| d.join(".cache")));
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let cache_dir_op = dirs_rs::cache_dir();
|
||||
|
||||
let cache_dir = cache_dir_op.map(|d| d.join("bat"))?;
|
||||
let cache_dir = BatProjectDirs::get_cache_dir()?;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let config_dir_op = env::var_os("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.filter(|p| p.is_absolute())
|
||||
.or_else(|| dirs_rs::home_dir().map(|d| d.join(".config")));
|
||||
.or_else(|| dirs::home_dir().map(|d| d.join(".config")));
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let config_dir_op = dirs_rs::config_dir();
|
||||
let config_dir_op = dirs::config_dir();
|
||||
|
||||
let config_dir = config_dir_op.map(|d| d.join("bat"))?;
|
||||
|
||||
@@ -42,6 +33,25 @@ impl BatProjectDirs {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_cache_dir() -> Option<PathBuf> {
|
||||
// on all OS prefer BAT_CACHE_PATH if set
|
||||
let cache_dir_op = env::var_os("BAT_CACHE_PATH").map(PathBuf::from);
|
||||
if cache_dir_op.is_some() {
|
||||
return cache_dir_op;
|
||||
}
|
||||
|
||||
#[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::home_dir().map(|d| d.join(".cache")));
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let cache_dir_op = dirs::cache_dir();
|
||||
|
||||
cache_dir_op.map(|d| d.join("bat"))
|
||||
}
|
||||
|
||||
pub fn cache_dir(&self) -> &Path {
|
||||
&self.cache_dir
|
||||
}
|
@@ -4,9 +4,13 @@
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
|
||||
extern crate dirs as dirs_rs;
|
||||
|
||||
mod app;
|
||||
mod assets;
|
||||
mod clap_app;
|
||||
mod config;
|
||||
mod directories;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsStr;
|
||||
@@ -19,25 +23,31 @@ use ansi_term::Colour::Green;
|
||||
use ansi_term::Style;
|
||||
|
||||
use crate::{app::App, config::config_file};
|
||||
use bat::controller::Controller;
|
||||
use assets::{assets_from_cache_or_binary, cache_dir, clear_assets, config_dir};
|
||||
use bat::Controller;
|
||||
use directories::PROJECT_DIRS;
|
||||
|
||||
use bat::{
|
||||
assets::{cache_dir, clear_assets, config_dir, HighlightingAssets},
|
||||
config::{Config, InputFile, StyleComponent, StyleComponents},
|
||||
errors::*,
|
||||
inputfile::InputFile,
|
||||
style::{OutputComponent, OutputComponents},
|
||||
Config,
|
||||
HighlightingAssets,
|
||||
};
|
||||
|
||||
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 source_dir = matches
|
||||
.value_of("source")
|
||||
.map(Path::new)
|
||||
.unwrap_or_else(|| PROJECT_DIRS.config_dir());
|
||||
let target_dir = matches
|
||||
.value_of("target")
|
||||
.map(Path::new)
|
||||
.unwrap_or_else(|| PROJECT_DIRS.cache_dir());
|
||||
|
||||
let blank = matches.is_present("blank");
|
||||
|
||||
let assets = HighlightingAssets::from_files(source_dir, blank)?;
|
||||
assets.save(target_dir)?;
|
||||
let assets = HighlightingAssets::from_files(source_dir, !blank)?;
|
||||
assets.save_to_cache(target_dir)?;
|
||||
} else if matches.is_present("clear") {
|
||||
clear_assets();
|
||||
}
|
||||
@@ -46,9 +56,8 @@ fn run_cache_subcommand(matches: &clap::ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn list_languages(config: &Config) -> Result<()> {
|
||||
let assets = HighlightingAssets::new();
|
||||
let assets = assets_from_cache_or_binary();
|
||||
let mut languages = assets
|
||||
.syntax_set
|
||||
.syntaxes()
|
||||
.iter()
|
||||
.filter(|syntax| !syntax.hidden && !syntax.file_extensions.is_empty())
|
||||
@@ -109,19 +118,18 @@ pub fn list_languages(config: &Config) -> Result<()> {
|
||||
}
|
||||
|
||||
pub fn list_themes(cfg: &Config) -> Result<()> {
|
||||
let assets = HighlightingAssets::new();
|
||||
let themes = &assets.theme_set.themes;
|
||||
let assets = assets_from_cache_or_binary();
|
||||
let mut config = cfg.clone();
|
||||
let mut style = HashSet::new();
|
||||
style.insert(OutputComponent::Plain);
|
||||
style.insert(StyleComponent::Plain);
|
||||
config.files = vec![InputFile::ThemePreviewFile];
|
||||
config.output_components = OutputComponents(style);
|
||||
config.style_components = StyleComponents(style);
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
if config.colored_output {
|
||||
for (theme, _) in themes.iter() {
|
||||
for theme in assets.themes() {
|
||||
writeln!(
|
||||
stdout,
|
||||
"Theme: {}\n",
|
||||
@@ -132,7 +140,7 @@ pub fn list_themes(cfg: &Config) -> Result<()> {
|
||||
writeln!(stdout)?;
|
||||
}
|
||||
} else {
|
||||
for (theme, _) in themes.iter() {
|
||||
for theme in assets.themes() {
|
||||
writeln!(stdout, "{}", theme)?;
|
||||
}
|
||||
}
|
||||
@@ -141,7 +149,7 @@ pub fn list_themes(cfg: &Config) -> Result<()> {
|
||||
}
|
||||
|
||||
fn run_controller(config: &Config) -> Result<bool> {
|
||||
let assets = HighlightingAssets::new();
|
||||
let assets = assets_from_cache_or_binary();
|
||||
let controller = Controller::new(&config, &assets);
|
||||
controller.run()
|
||||
}
|
||||
@@ -199,7 +207,7 @@ fn main() {
|
||||
|
||||
match result {
|
||||
Err(error) => {
|
||||
handle_error(&error);
|
||||
default_error_handler(&error);
|
||||
process::exit(1);
|
||||
}
|
||||
Ok(false) => {
|
||||
|
96
src/config.rs
Normal file
96
src/config.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
pub use crate::inputfile::InputFile;
|
||||
pub use crate::line_range::{HighlightedLineRanges, LineRange, LineRanges};
|
||||
pub use crate::style::{StyleComponent, StyleComponents};
|
||||
pub use crate::syntax_mapping::{MappingTarget, SyntaxMapping};
|
||||
pub use crate::wrap::OutputWrap;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum PagingMode {
|
||||
Always,
|
||||
QuitIfOneScreen,
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for PagingMode {
|
||||
fn default() -> Self {
|
||||
PagingMode::Never
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
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 style_components: StyleComponents,
|
||||
|
||||
/// 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<'a>,
|
||||
|
||||
/// Command to start the pager
|
||||
pub pager: Option<&'a str>,
|
||||
|
||||
/// Whether or not to use ANSI italics
|
||||
pub use_italic_text: bool,
|
||||
|
||||
/// Ranges of lines which should be highlighted with a special background color
|
||||
pub highlighted_lines: HighlightedLineRanges,
|
||||
|
||||
/// Name of file to display when printing
|
||||
pub filenames: Option<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_config_should_include_all_lines() {
|
||||
use crate::line_range::RangeCheckResult;
|
||||
|
||||
assert_eq!(
|
||||
Config::default().line_ranges.check(17),
|
||||
RangeCheckResult::InRange
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_config_should_highlight_no_lines() {
|
||||
use crate::line_range::RangeCheckResult;
|
||||
|
||||
assert_ne!(
|
||||
Config::default().highlighted_lines.0.check(17),
|
||||
RangeCheckResult::InRange
|
||||
);
|
||||
}
|
@@ -2,12 +2,12 @@ use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::assets::HighlightingAssets;
|
||||
use crate::config::{Config, PagingMode};
|
||||
use crate::errors::*;
|
||||
use crate::inputfile::{InputFile, InputFileReader};
|
||||
use crate::line_range::{LineRanges, RangeCheckResult};
|
||||
use crate::output::OutputType;
|
||||
use crate::printer::{InteractivePrinter, Printer, SimplePrinter};
|
||||
use crate::{Config, PagingMode};
|
||||
|
||||
pub struct Controller<'a> {
|
||||
config: &'a Config<'a>,
|
||||
@@ -20,6 +20,10 @@ impl<'b> Controller<'b> {
|
||||
}
|
||||
|
||||
pub fn run(&self) -> Result<bool> {
|
||||
self.run_with_error_handler(default_error_handler)
|
||||
}
|
||||
|
||||
pub fn run_with_error_handler(&self, handle_error: impl Fn(&Error)) -> 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 {
|
||||
@@ -93,7 +97,7 @@ impl<'b> Controller<'b> {
|
||||
input_file: InputFile<'a>,
|
||||
file_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
if !reader.first_line.is_empty() || self.config.output_components.header() {
|
||||
if !reader.first_line.is_empty() || self.config.style_components.header() {
|
||||
printer.print_header(writer, input_file, file_name)?;
|
||||
}
|
||||
|
||||
@@ -120,7 +124,7 @@ impl<'b> Controller<'b> {
|
||||
|
||||
while reader.read_line(&mut line_buffer)? {
|
||||
match line_ranges.check(line_number) {
|
||||
RangeCheckResult::OutsideRange => {
|
||||
RangeCheckResult::BeforeOrBetweenRanges => {
|
||||
// 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)?;
|
||||
@@ -128,7 +132,7 @@ impl<'b> Controller<'b> {
|
||||
}
|
||||
|
||||
RangeCheckResult::InRange => {
|
||||
if self.config.output_components.snip() {
|
||||
if self.config.style_components.snip() {
|
||||
if first_range {
|
||||
first_range = false;
|
||||
mid_range = true;
|
||||
|
25
src/errors.rs
Normal file
25
src/errors.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use error_chain::error_chain;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
Clap(::clap::Error);
|
||||
Io(::std::io::Error);
|
||||
SyntectError(::syntect::LoadingError);
|
||||
ParseIntError(::std::num::ParseIntError);
|
||||
GlobParsingError(::globset::Error);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_error_handler(error: &Error) {
|
||||
match error {
|
||||
Error(ErrorKind::Io(ref io_error), _)
|
||||
if io_error.kind() == ::std::io::ErrorKind::BrokenPipe =>
|
||||
{
|
||||
::std::process::exit(0);
|
||||
}
|
||||
_ => {
|
||||
use ansi_term::Colour::Red;
|
||||
eprintln!("{}: {}", Red.paint("[bat error]"), error);
|
||||
}
|
||||
};
|
||||
}
|
@@ -10,8 +10,8 @@ const THEME_PREVIEW_FILE: &[u8] = include_bytes!("../assets/theme_preview.rs");
|
||||
|
||||
pub struct InputFileReader<'a> {
|
||||
inner: Box<dyn BufRead + 'a>,
|
||||
pub first_line: Vec<u8>,
|
||||
pub content_type: Option<ContentType>,
|
||||
pub(crate) first_line: Vec<u8>,
|
||||
pub(crate) content_type: Option<ContentType>,
|
||||
}
|
||||
|
||||
impl<'a> InputFileReader<'a> {
|
||||
@@ -36,7 +36,7 @@ impl<'a> InputFileReader<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
|
||||
pub(crate) fn read_line(&mut self, buf: &mut Vec<u8>) -> io::Result<bool> {
|
||||
if self.first_line.is_empty() {
|
||||
let res = self.inner.read_until(b'\n', buf).map(|size| size > 0)?;
|
||||
|
||||
@@ -60,7 +60,7 @@ pub enum InputFile<'a> {
|
||||
}
|
||||
|
||||
impl<'a> InputFile<'a> {
|
||||
pub fn get_reader(&self, stdin: &'a io::Stdin) -> Result<InputFileReader> {
|
||||
pub(crate) fn get_reader(&self, stdin: &'a io::Stdin) -> Result<InputFileReader> {
|
||||
match self {
|
||||
InputFile::StdIn => Ok(InputFileReader::new(stdin.lock())),
|
||||
InputFile::Ordinary(filename) => {
|
||||
|
127
src/lib.rs
127
src/lib.rs
@@ -1,12 +1,6 @@
|
||||
// `error_chain!` can recurse deeply
|
||||
#![recursion_limit = "1024"]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
extern crate ansi_term;
|
||||
extern crate atty;
|
||||
extern crate console;
|
||||
@@ -18,118 +12,23 @@ extern crate shell_words;
|
||||
extern crate syntect;
|
||||
extern crate wild;
|
||||
|
||||
pub mod assets;
|
||||
pub mod controller;
|
||||
pub(crate) mod assets;
|
||||
pub mod config;
|
||||
pub(crate) mod controller;
|
||||
mod decorations;
|
||||
mod diff;
|
||||
pub mod dirs;
|
||||
pub mod inputfile;
|
||||
pub mod errors;
|
||||
pub(crate) mod inputfile;
|
||||
mod less;
|
||||
pub mod line_range;
|
||||
pub(crate) mod line_range;
|
||||
mod output;
|
||||
mod preprocessor;
|
||||
mod printer;
|
||||
pub mod style;
|
||||
pub mod syntax_mapping;
|
||||
pub(crate) mod printer;
|
||||
pub(crate) mod style;
|
||||
pub(crate) mod syntax_mapping;
|
||||
mod terminal;
|
||||
pub(crate) mod wrap;
|
||||
|
||||
pub 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() == ::std::io::ErrorKind::BrokenPipe =>
|
||||
{
|
||||
::std::process::exit(0);
|
||||
}
|
||||
_ => {
|
||||
use ansi_term::Colour::Red;
|
||||
eprintln!("{}: {}", Red.paint("[bat error]"), error);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum PagingMode {
|
||||
Always,
|
||||
QuitIfOneScreen,
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Default for PagingMode {
|
||||
fn default() -> Self {
|
||||
PagingMode::Never
|
||||
}
|
||||
}
|
||||
|
||||
use inputfile::InputFile;
|
||||
use line_range::LineRanges;
|
||||
use style::{OutputComponents, OutputWrap};
|
||||
use syntax_mapping::SyntaxMapping;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
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: LineRanges,
|
||||
|
||||
/// Name of file to display when printing
|
||||
pub filenames: Option<Vec<&'a str>>,
|
||||
}
|
||||
pub use assets::HighlightingAssets;
|
||||
pub use controller::Controller;
|
||||
pub use printer::{InteractivePrinter, Printer, SimplePrinter};
|
||||
|
@@ -6,20 +6,22 @@ pub struct LineRange {
|
||||
pub upper: usize,
|
||||
}
|
||||
|
||||
impl LineRange {
|
||||
pub fn from(range_raw: &str) -> Result<LineRange> {
|
||||
LineRange::parse_range(range_raw)
|
||||
}
|
||||
|
||||
pub fn new() -> LineRange {
|
||||
impl Default for LineRange {
|
||||
fn default() -> LineRange {
|
||||
LineRange {
|
||||
lower: usize::min_value(),
|
||||
upper: usize::max_value(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_range(range_raw: &str) -> Result<LineRange> {
|
||||
let mut new_range = LineRange::new();
|
||||
impl LineRange {
|
||||
pub fn from(range_raw: &str) -> Result<LineRange> {
|
||||
LineRange::parse_range(range_raw)
|
||||
}
|
||||
|
||||
fn parse_range(range_raw: &str) -> Result<LineRange> {
|
||||
let mut new_range = LineRange::default();
|
||||
|
||||
if range_raw.bytes().nth(0).ok_or("Empty line range")? == b':' {
|
||||
new_range.upper = range_raw[1..].parse()?;
|
||||
@@ -48,7 +50,7 @@ impl LineRange {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_inside(&self, line: usize) -> bool {
|
||||
pub(crate) fn is_inside(&self, line: usize) -> bool {
|
||||
line >= self.lower && line <= self.upper
|
||||
}
|
||||
}
|
||||
@@ -97,19 +99,27 @@ pub enum RangeCheckResult {
|
||||
InRange,
|
||||
|
||||
// Before the first range or within two ranges
|
||||
OutsideRange,
|
||||
BeforeOrBetweenRanges,
|
||||
|
||||
// Line number is outside of all ranges and larger than the last range.
|
||||
AfterLastRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LineRanges {
|
||||
ranges: Vec<LineRange>,
|
||||
largest_upper_bound: usize,
|
||||
}
|
||||
|
||||
impl LineRanges {
|
||||
pub fn none() -> LineRanges {
|
||||
LineRanges::from(vec![])
|
||||
}
|
||||
|
||||
pub fn all() -> LineRanges {
|
||||
LineRanges::from(vec![LineRange::default()])
|
||||
}
|
||||
|
||||
pub fn from(ranges: Vec<LineRange>) -> LineRanges {
|
||||
let largest_upper_bound = ranges
|
||||
.iter()
|
||||
@@ -122,17 +132,32 @@ impl LineRanges {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(&self, line: usize) -> RangeCheckResult {
|
||||
if self.ranges.is_empty() | self.ranges.iter().any(|r| r.is_inside(line)) {
|
||||
pub(crate) fn check(&self, line: usize) -> RangeCheckResult {
|
||||
if self.ranges.iter().any(|r| r.is_inside(line)) {
|
||||
RangeCheckResult::InRange
|
||||
} else if line < self.largest_upper_bound {
|
||||
RangeCheckResult::OutsideRange
|
||||
RangeCheckResult::BeforeOrBetweenRanges
|
||||
} else {
|
||||
RangeCheckResult::AfterLastRange
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LineRanges {
|
||||
fn default() -> Self {
|
||||
Self::all()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HighlightedLineRanges(pub LineRanges);
|
||||
|
||||
impl Default for HighlightedLineRanges {
|
||||
fn default() -> Self {
|
||||
HighlightedLineRanges(LineRanges::none())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ranges(rs: &[&str]) -> LineRanges {
|
||||
LineRanges::from(rs.iter().map(|r| LineRange::from(r).unwrap()).collect())
|
||||
@@ -142,7 +167,7 @@ fn ranges(rs: &[&str]) -> LineRanges {
|
||||
fn test_ranges_simple() {
|
||||
let ranges = ranges(&["3:8"]);
|
||||
|
||||
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(2));
|
||||
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(2));
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(5));
|
||||
assert_eq!(RangeCheckResult::AfterLastRange, ranges.check(9));
|
||||
}
|
||||
@@ -151,11 +176,11 @@ fn test_ranges_simple() {
|
||||
fn test_ranges_advanced() {
|
||||
let ranges = ranges(&["3:8", "11:20", "25:30"]);
|
||||
|
||||
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(2));
|
||||
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(2));
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(5));
|
||||
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(9));
|
||||
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(9));
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(11));
|
||||
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(22));
|
||||
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(22));
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(28));
|
||||
assert_eq!(RangeCheckResult::AfterLastRange, ranges.check(31));
|
||||
}
|
||||
@@ -174,15 +199,22 @@ fn test_ranges_open_low() {
|
||||
fn test_ranges_open_high() {
|
||||
let ranges = ranges(&["3:", "2:5"]);
|
||||
|
||||
assert_eq!(RangeCheckResult::OutsideRange, ranges.check(1));
|
||||
assert_eq!(RangeCheckResult::BeforeOrBetweenRanges, ranges.check(1));
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(3));
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(5));
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_empty() {
|
||||
let ranges = ranges(&[]);
|
||||
fn test_ranges_all() {
|
||||
let ranges = LineRanges::all();
|
||||
|
||||
assert_eq!(RangeCheckResult::InRange, ranges.check(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_none() {
|
||||
let ranges = LineRanges::none();
|
||||
|
||||
assert_ne!(RangeCheckResult::InRange, ranges.check(1));
|
||||
}
|
||||
|
@@ -6,9 +6,9 @@ use std::process::{Child, Command, Stdio};
|
||||
|
||||
use shell_words;
|
||||
|
||||
use crate::config::PagingMode;
|
||||
use crate::errors::*;
|
||||
use crate::less::retrieve_less_version;
|
||||
use crate::PagingMode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OutputType {
|
||||
|
@@ -20,6 +20,7 @@ use encoding::{DecoderTrap, Encoding};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use crate::assets::HighlightingAssets;
|
||||
use crate::config::Config;
|
||||
use crate::decorations::{
|
||||
Decoration, GridBorderDecoration, LineChangesDecoration, LineNumberDecoration,
|
||||
};
|
||||
@@ -29,9 +30,8 @@ use crate::errors::*;
|
||||
use crate::inputfile::{InputFile, InputFileReader};
|
||||
use crate::line_range::RangeCheckResult;
|
||||
use crate::preprocessor::{expand_tabs, replace_nonprintable};
|
||||
use crate::style::OutputWrap;
|
||||
use crate::terminal::{as_terminal_escaped, to_ansi_color};
|
||||
use crate::Config;
|
||||
use crate::wrap::OutputWrap;
|
||||
|
||||
pub trait Printer {
|
||||
fn print_header(
|
||||
@@ -126,11 +126,11 @@ impl<'a> InteractivePrinter<'a> {
|
||||
// Create decorations.
|
||||
let mut decorations: Vec<Box<dyn Decoration>> = Vec::new();
|
||||
|
||||
if config.output_components.numbers() {
|
||||
if config.style_components.numbers() {
|
||||
decorations.push(Box::new(LineNumberDecoration::new(&colors)));
|
||||
}
|
||||
|
||||
if config.output_components.changes() {
|
||||
if config.style_components.changes() {
|
||||
decorations.push(Box::new(LineChangesDecoration::new(&colors)));
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ impl<'a> InteractivePrinter<'a> {
|
||||
// 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() {
|
||||
if config.style_components.grid() && !decorations.is_empty() {
|
||||
decorations.push(Box::new(GridBorderDecoration::new(&colors)));
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ impl<'a> InteractivePrinter<'a> {
|
||||
None
|
||||
} else {
|
||||
// Get the Git modifications
|
||||
line_changes = if config.output_components.changes() {
|
||||
line_changes = if config.style_components.changes() {
|
||||
match file {
|
||||
InputFile::Ordinary(filename) => get_git_diff(filename),
|
||||
_ => None,
|
||||
@@ -216,7 +216,7 @@ impl<'a> InteractivePrinter<'a> {
|
||||
text_truncated,
|
||||
" ".repeat(self.panel_width - 1 - text_truncated.len())
|
||||
);
|
||||
if self.config.output_components.grid() {
|
||||
if self.config.style_components.grid() {
|
||||
format!("{} │ ", text_filled)
|
||||
} else {
|
||||
format!("{}", text_filled)
|
||||
@@ -240,7 +240,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||
file: InputFile,
|
||||
file_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
if !self.config.output_components.header() {
|
||||
if !self.config.style_components.header() {
|
||||
if Some(ContentType::BINARY) == self.content_type && !self.config.show_nonprintable {
|
||||
let input = match file {
|
||||
InputFile::Ordinary(filename) => format!(
|
||||
@@ -259,14 +259,14 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||
input
|
||||
)?;
|
||||
} else {
|
||||
if self.config.output_components.grid() {
|
||||
if self.config.style_components.grid() {
|
||||
self.print_horizontal_line(handle, '┬')?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.config.output_components.grid() {
|
||||
if self.config.style_components.grid() {
|
||||
self.print_horizontal_line(handle, '┬')?;
|
||||
|
||||
write!(
|
||||
@@ -305,7 +305,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||
mode
|
||||
)?;
|
||||
|
||||
if self.config.output_components.grid() {
|
||||
if self.config.style_components.grid() {
|
||||
if self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable {
|
||||
self.print_horizontal_line(handle, '┼')?;
|
||||
} else {
|
||||
@@ -317,7 +317,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||
}
|
||||
|
||||
fn print_footer(&mut self, handle: &mut dyn Write) -> Result<()> {
|
||||
if self.config.output_components.grid()
|
||||
if self.config.style_components.grid()
|
||||
&& (self.content_type.map_or(false, |c| c.is_text()) || self.config.show_nonprintable)
|
||||
{
|
||||
self.print_horizontal_line(handle, '┴')
|
||||
@@ -395,7 +395,7 @@ impl<'a> Printer for InteractivePrinter<'a> {
|
||||
|
||||
// Line highlighting
|
||||
let highlight_this_line =
|
||||
self.config.highlight_lines.check(line_number) == RangeCheckResult::InRange;
|
||||
self.config.highlighted_lines.0.check(line_number) == RangeCheckResult::InRange;
|
||||
|
||||
let background_color = self
|
||||
.background_color_highlight
|
||||
|
86
src/style.rs
86
src/style.rs
@@ -4,7 +4,7 @@ use std::str::FromStr;
|
||||
use crate::errors::*;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
|
||||
pub enum OutputComponent {
|
||||
pub enum StyleComponent {
|
||||
Auto,
|
||||
Changes,
|
||||
Grid,
|
||||
@@ -15,92 +15,80 @@ pub enum OutputComponent {
|
||||
Plain,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
|
||||
pub enum OutputWrap {
|
||||
Character,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for OutputWrap {
|
||||
fn default() -> Self {
|
||||
OutputWrap::None
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputComponent {
|
||||
pub fn components(self, interactive_terminal: bool) -> &'static [OutputComponent] {
|
||||
impl StyleComponent {
|
||||
pub fn components(self, interactive_terminal: bool) -> &'static [StyleComponent] {
|
||||
match self {
|
||||
OutputComponent::Auto => {
|
||||
StyleComponent::Auto => {
|
||||
if interactive_terminal {
|
||||
OutputComponent::Full.components(interactive_terminal)
|
||||
StyleComponent::Full.components(interactive_terminal)
|
||||
} else {
|
||||
OutputComponent::Plain.components(interactive_terminal)
|
||||
StyleComponent::Plain.components(interactive_terminal)
|
||||
}
|
||||
}
|
||||
OutputComponent::Changes => &[OutputComponent::Changes],
|
||||
OutputComponent::Grid => &[OutputComponent::Grid],
|
||||
OutputComponent::Header => &[OutputComponent::Header],
|
||||
OutputComponent::Numbers => &[OutputComponent::Numbers],
|
||||
OutputComponent::Snip => &[OutputComponent::Snip],
|
||||
OutputComponent::Full => &[
|
||||
OutputComponent::Changes,
|
||||
OutputComponent::Grid,
|
||||
OutputComponent::Header,
|
||||
OutputComponent::Numbers,
|
||||
OutputComponent::Snip,
|
||||
StyleComponent::Changes => &[StyleComponent::Changes],
|
||||
StyleComponent::Grid => &[StyleComponent::Grid],
|
||||
StyleComponent::Header => &[StyleComponent::Header],
|
||||
StyleComponent::Numbers => &[StyleComponent::Numbers],
|
||||
StyleComponent::Snip => &[StyleComponent::Snip],
|
||||
StyleComponent::Full => &[
|
||||
StyleComponent::Changes,
|
||||
StyleComponent::Grid,
|
||||
StyleComponent::Header,
|
||||
StyleComponent::Numbers,
|
||||
StyleComponent::Snip,
|
||||
],
|
||||
OutputComponent::Plain => &[],
|
||||
StyleComponent::Plain => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for OutputComponent {
|
||||
impl FromStr for StyleComponent {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"auto" => Ok(OutputComponent::Auto),
|
||||
"changes" => Ok(OutputComponent::Changes),
|
||||
"grid" => Ok(OutputComponent::Grid),
|
||||
"header" => Ok(OutputComponent::Header),
|
||||
"numbers" => Ok(OutputComponent::Numbers),
|
||||
"snip" => Ok(OutputComponent::Snip),
|
||||
"full" => Ok(OutputComponent::Full),
|
||||
"plain" => Ok(OutputComponent::Plain),
|
||||
"auto" => Ok(StyleComponent::Auto),
|
||||
"changes" => Ok(StyleComponent::Changes),
|
||||
"grid" => Ok(StyleComponent::Grid),
|
||||
"header" => Ok(StyleComponent::Header),
|
||||
"numbers" => Ok(StyleComponent::Numbers),
|
||||
"snip" => Ok(StyleComponent::Snip),
|
||||
"full" => Ok(StyleComponent::Full),
|
||||
"plain" => Ok(StyleComponent::Plain),
|
||||
_ => Err(format!("Unknown style '{}'", s).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct OutputComponents(pub HashSet<OutputComponent>);
|
||||
pub struct StyleComponents(pub HashSet<StyleComponent>);
|
||||
|
||||
impl OutputComponents {
|
||||
pub fn new(components: &[OutputComponent]) -> OutputComponents {
|
||||
OutputComponents(components.iter().cloned().collect())
|
||||
impl StyleComponents {
|
||||
pub fn new(components: &[StyleComponent]) -> StyleComponents {
|
||||
StyleComponents(components.iter().cloned().collect())
|
||||
}
|
||||
|
||||
pub fn changes(&self) -> bool {
|
||||
self.0.contains(&OutputComponent::Changes)
|
||||
self.0.contains(&StyleComponent::Changes)
|
||||
}
|
||||
|
||||
pub fn grid(&self) -> bool {
|
||||
self.0.contains(&OutputComponent::Grid)
|
||||
self.0.contains(&StyleComponent::Grid)
|
||||
}
|
||||
|
||||
pub fn header(&self) -> bool {
|
||||
self.0.contains(&OutputComponent::Header)
|
||||
self.0.contains(&StyleComponent::Header)
|
||||
}
|
||||
|
||||
pub fn numbers(&self) -> bool {
|
||||
self.0.contains(&OutputComponent::Numbers)
|
||||
self.0.contains(&StyleComponent::Numbers)
|
||||
}
|
||||
|
||||
pub fn snip(&self) -> bool {
|
||||
self.0.contains(&OutputComponent::Snip)
|
||||
self.0.contains(&StyleComponent::Snip)
|
||||
}
|
||||
|
||||
pub fn plain(&self) -> bool {
|
||||
self.0.iter().all(|c| c == &OutputComponent::Plain)
|
||||
self.0.iter().all(|c| c == &StyleComponent::Plain)
|
||||
}
|
||||
}
|
||||
|
@@ -1,35 +1,111 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::errors::Result;
|
||||
|
||||
use globset::{Candidate, GlobBuilder, GlobMatcher};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum MappingTarget<'a> {
|
||||
MapTo(&'a str),
|
||||
MapToUnknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SyntaxMapping(HashMap<String, String>);
|
||||
pub struct SyntaxMapping<'a> {
|
||||
mappings: Vec<(GlobMatcher, MappingTarget<'a>)>,
|
||||
}
|
||||
|
||||
impl SyntaxMapping {
|
||||
pub fn new() -> SyntaxMapping {
|
||||
impl<'a> SyntaxMapping<'a> {
|
||||
pub fn empty() -> SyntaxMapping<'a> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, from: impl Into<String>, to: impl Into<String>) -> Option<String> {
|
||||
self.0.insert(from.into(), to.into())
|
||||
pub fn builtin() -> SyntaxMapping<'a> {
|
||||
let mut mapping = Self::empty();
|
||||
mapping.insert("*.h", MappingTarget::MapTo("C++")).unwrap();
|
||||
mapping
|
||||
.insert("build", MappingTarget::MapToUnknown)
|
||||
.unwrap();
|
||||
mapping
|
||||
.insert("**/.ssh/config", MappingTarget::MapTo("SSH Config"))
|
||||
.unwrap();
|
||||
mapping
|
||||
.insert(
|
||||
"/etc/profile",
|
||||
MappingTarget::MapTo("Bourne Again Shell (bash)"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
mapping
|
||||
}
|
||||
|
||||
pub fn replace<'a>(&self, input: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
|
||||
let input = input.into();
|
||||
match self.0.get(input.as_ref()) {
|
||||
Some(s) => Cow::from(s.clone()),
|
||||
None => input,
|
||||
pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> {
|
||||
let glob = GlobBuilder::new(from)
|
||||
.case_insensitive(false)
|
||||
.literal_separator(true)
|
||||
.build()?;
|
||||
self.mappings.push((glob.compile_matcher(), to));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_syntax_for(&self, path: impl AsRef<Path>) -> Option<MappingTarget<'a>> {
|
||||
let candidate = Candidate::new(path.as_ref());
|
||||
let canddidate_filename = path.as_ref().file_name().map(Candidate::new);
|
||||
for (ref glob, ref syntax) in self.mappings.iter().rev() {
|
||||
if glob.is_match_candidate(&candidate)
|
||||
|| canddidate_filename
|
||||
.as_ref()
|
||||
.map_or(false, |filename| glob.is_match_candidate(filename))
|
||||
{
|
||||
return Some(*syntax);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let mut map = SyntaxMapping::new();
|
||||
map.insert("Cargo.lock", "toml");
|
||||
map.insert(".ignore", ".gitignore");
|
||||
let mut map = SyntaxMapping::empty();
|
||||
map.insert("/path/to/Cargo.lock", MappingTarget::MapTo("TOML"))
|
||||
.ok();
|
||||
map.insert("/path/to/.ignore", MappingTarget::MapTo("Git Ignore"))
|
||||
.ok();
|
||||
|
||||
assert_eq!("toml", map.replace("Cargo.lock"));
|
||||
assert_eq!("other.lock", map.replace("other.lock"));
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/path/to/Cargo.lock"),
|
||||
Some(MappingTarget::MapTo("TOML"))
|
||||
);
|
||||
assert_eq!(map.get_syntax_for("/path/to/other.lock"), None);
|
||||
|
||||
assert_eq!(".gitignore", map.replace(".ignore"));
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/path/to/.ignore"),
|
||||
Some(MappingTarget::MapTo("Git Ignore"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_can_override_builtin_mappings() {
|
||||
let mut map = SyntaxMapping::builtin();
|
||||
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/etc/profile"),
|
||||
Some(MappingTarget::MapTo("Bourne Again Shell (bash)"))
|
||||
);
|
||||
map.insert("/etc/profile", MappingTarget::MapTo("My Syntax"))
|
||||
.ok();
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/etc/profile"),
|
||||
Some(MappingTarget::MapTo("My Syntax"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_mappings() {
|
||||
let map = SyntaxMapping::builtin();
|
||||
|
||||
assert_eq!(
|
||||
map.get_syntax_for("/path/to/build"),
|
||||
Some(MappingTarget::MapToUnknown)
|
||||
);
|
||||
}
|
||||
|
11
src/wrap.rs
Normal file
11
src/wrap.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
|
||||
pub enum OutputWrap {
|
||||
Character,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for OutputWrap {
|
||||
fn default() -> Self {
|
||||
OutputWrap::None
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user