use crate::{
args::{self, Args, OpenStrategy, REPEAT_FILE, generate_command},
errors::Message,
mes, style,
};
use clap::{ArgMatches, Error, FromArgMatches};
use crossterm::{event::KeyCode, style::Color};
use std::{
collections::HashSet,
ffi::OsString,
path::{Component, Path, PathBuf},
sync::{Arc, OnceLock},
};
static CONFIG: OnceLock<Arc<Config>> = OnceLock::new();
pub fn base_config() -> Arc<Config> {
Arc::clone(CONFIG.get().unwrap())
}
pub fn set_config(c: Config) {
CONFIG.set(Arc::new(c)).ok().unwrap();
}
#[derive(Clone)]
pub struct KeyBindings {
pub down: Vec<KeyCode>,
pub up: Vec<KeyCode>,
pub big_down: Vec<KeyCode>,
pub big_up: Vec<KeyCode>,
pub down_path: Vec<KeyCode>,
pub up_path: Vec<KeyCode>,
pub down_same_depth: Vec<KeyCode>,
pub up_same_depth: Vec<KeyCode>,
pub top: Vec<KeyCode>,
pub bottom: Vec<KeyCode>,
pub page_down: Vec<KeyCode>,
pub page_up: Vec<KeyCode>,
pub cycle_view: Vec<KeyCode>,
pub help: Vec<KeyCode>,
pub quit: Vec<KeyCode>,
pub open: Vec<KeyCode>,
pub fold: Vec<KeyCode>,
pub filter: Vec<KeyCode>,
pub search: Vec<KeyCode>,
pub submit_search: Vec<KeyCode>,
}
#[derive(Clone)]
pub struct Characters {
pub bl: char,
pub br: char,
pub tl: char,
pub tr: char,
pub v: char,
pub h: char,
pub tee: char,
pub match_with_next: String,
pub match_no_next: String,
pub spacer_vert: String,
pub spacer: String,
pub selected_indicator: String,
pub selected_indicator_clear: String,
pub ellipsis: String,
pub search_prompt: String,
pub search_prompt_inactive: String,
pub filter_prompt: String,
}
#[derive(Clone)]
pub struct Colors {
pub file: Color,
pub dir: Color,
pub line_number: Color,
pub text: Option<Color>,
pub branch: Option<Color>,
pub selected_indicator: Option<Color>,
pub selected_bg: Color,
pub matches: Vec<Color>,
pub filter_highlight: Color,
}
impl args::Color {
fn get(&self) -> Color {
match *self {
args::Color::Black => Color::Black,
args::Color::White => Color::White,
args::Color::Red => Color::Red,
args::Color::Green => Color::Green,
args::Color::Yellow => Color::Yellow,
args::Color::Blue => Color::Blue,
args::Color::Magenta => Color::Magenta,
args::Color::Cyan => Color::Cyan,
args::Color::Grey => Color::Grey,
args::Color::Rgb(r, g, b) => Color::Rgb { r, g, b },
args::Color::Ansi(value) => Color::AnsiValue(value),
}
}
}
#[derive(Clone)]
pub struct CoreConfig {
pub selection_file: Option<PathBuf>,
pub repeat_file: Option<PathBuf>,
pub select: bool,
pub menu: bool,
pub live: bool,
pub auto_open: bool,
pub repeat: bool,
pub threads: usize,
pub editor: Option<String>,
pub open_like: Option<OpenStrategy>,
pub completion_target: Option<clap_complete::Shell>,
pub keys: KeyBindings,
pub all_args: Vec<OsString>,
}
#[derive(Clone)]
pub struct Config {
pub path: PathBuf,
pub is_dir: bool,
pub regexps: Vec<String>,
pub globs: Vec<String>,
pub hidden: bool,
pub line_number: bool,
pub files: bool,
pub count: bool,
pub links: bool,
pub trim: bool,
pub ignore: bool,
pub max_depth: Option<usize>,
pub max_length: Option<usize>,
pub before_context: usize,
pub after_context: usize,
pub overview: bool,
pub with_bold: bool,
pub branch_each: usize,
pub with_colors: bool,
pub prefix_len: usize,
pub chars: Characters,
pub colors: Colors,
pub core: CoreConfig,
}
fn canonicalize(p: &Path) -> Result<PathBuf, Message> {
dunce::canonicalize(p)
.map_err(|_| mes!("failed to canonicalize path `{}`", p.to_string_lossy()))
}
fn process_path(input: &Path, check_exists: bool) -> Result<PathBuf, Message> {
let mut components = input.components();
let mut path = PathBuf::new();
match components.next() {
Some(Component::Normal(c)) => {
if c == "~" {
path.push(std::env::var("HOME").map_err(|e| mes!("{}", e))?);
} else {
path.push(c);
}
}
Some(c) => path.push(c),
_ => {}
}
for c in components {
path.push(c);
}
if check_exists {
path.exists()
.then(|| canonicalize(&path))
.ok_or_else(|| mes!("failed to find path `{}`", path.to_string_lossy()))?
} else {
Ok(path)
}
}
fn get_all_args(mut args: Vec<OsString>) -> Vec<OsString> {
if let Some(env_args) = std::env::var_os(args::DEFAULT_OPTS_ENV_NAME) {
args.splice(
0..0,
env_args
.into_string()
.unwrap_or_default()
.split_whitespace()
.map(OsString::from),
);
}
args
}
pub fn get_matches(args: Vec<OsString>) -> Result<(ArgMatches, Vec<OsString>), Error> {
generate_command()
.try_get_matches_from(get_all_args(args.clone()))
.map(|m| (m, args))
}
fn apply_matches(
core: CoreConfig,
matches: &ArgMatches,
only_explicit: bool,
) -> Result<Config, Message> {
let args = Args::from_arg_matches(matches).map_err(|e| mes!("{}", e))?;
if only_explicit {
let applies =
|id: &str| matches.value_source(id) == Some(clap::parser::ValueSource::CommandLine);
let mut c = (*base_config()).clone();
c.core = core;
if let Some(p) = args.positional_path.as_ref().or(args.path.as_ref()) {
c.path = process_path(p, true)?;
c.is_dir = c.path.is_dir();
}
if applies(args::EXPRESSION_POSITIONAL) || applies(args::EXPRESSION) {
c.regexps.clear();
if let Some(expr) = &args.positional_regexp {
c.regexps.push(expr.clone());
}
c.regexps.extend(args.regexp.iter().cloned());
}
if applies(args::GLOB) {
c.globs = args.glob.clone();
}
if applies(args::HIDDEN) {
c.hidden = args.hidden;
}
if applies(args::LINE_NUMBER) {
c.line_number = args.line_number;
}
if applies(args::FILES) {
c.files = args.files;
}
if applies(args::COUNT) {
c.count = args.count;
}
if applies(args::LINKS) {
c.links = args.links;
}
if applies(args::TRIM_LEFT) {
c.trim = args.trim;
}
if applies(args::NO_IGNORE) {
c.ignore = !args.no_ignore;
}
if applies(args::MAX_DEPTH) {
c.max_depth = args.max_depth;
}
if applies(args::MAX_LENGTH) {
c.max_length = args.max_length;
}
if applies(args::LONG_BRANCHES_EACH) {
c.branch_each = args.branch_each;
}
let context_both = if applies(args::CONTEXT) {
args.context.unwrap_or(0)
} else {
0
};
if applies(args::BEFORE_CONTEXT) || applies(args::CONTEXT) {
c.before_context = args.before_context.unwrap_or(context_both);
}
if applies(args::AFTER_CONTEXT) || applies(args::CONTEXT) {
c.after_context = args.after_context.unwrap_or(context_both);
}
if applies(args::NO_COLORS) {
c.with_colors = !args.no_color;
}
if applies(args::NO_BOLD) {
c.with_bold = !args.no_bold;
}
if applies(args::OVERVIEW) {
c.overview = args.overview;
}
if applies(args::PREFIX_LEN) {
c.prefix_len = args.prefix_len;
}
if applies(args::CHAR_VERTICAL) {
c.chars.v = args.char_vertical;
}
if applies(args::CHAR_HORIZONTAL) {
c.chars.h = args.char_horizontal;
}
if applies(args::CHAR_TOP_LEFT) {
c.chars.tl = args.char_top_left;
}
if applies(args::CHAR_TOP_RIGHT) {
c.chars.tr = args.char_top_right;
}
if applies(args::CHAR_BOTTOM_LEFT) {
c.chars.bl = args.char_bottom_left;
}
if applies(args::CHAR_BOTTOM_RIGHT) {
c.chars.br = args.char_bottom_right;
}
if applies(args::CHAR_TEE) {
c.chars.tee = args.char_tee;
}
let s = c.prefix_len;
c.chars.match_with_next = format!("{}{}", c.chars.tee, style::repeat(c.chars.h, s - 1));
c.chars.match_no_next = format!("{}{}", c.chars.bl, style::repeat(c.chars.h, s - 1));
c.chars.spacer_vert = format!("{}{}", c.chars.v, style::repeat(' ', s - 1));
c.chars.spacer = " ".repeat(s);
if applies(args::SELECTED_INDICATOR) {
c.chars.selected_indicator_clear = " ".repeat(args.selected_indicator.chars().count());
c.chars.selected_indicator = args.selected_indicator.clone();
}
if applies(args::ELLIPSES) {
c.chars.ellipsis = args.ellipsis.clone();
}
if applies(args::SEARCH_PROMPT) {
c.chars.search_prompt = args.search_prompt.clone();
}
if applies(args::SEARCH_PROMPT_INACTIVE) {
c.chars.search_prompt_inactive = args.search_prompt_inactive.clone();
}
if applies(args::FILTER_PROMPT) {
c.chars.filter_prompt = args.filter_prompt.clone();
}
if applies("file_color")
&& let Some(v) = &args.file_color
{
c.colors.file = v.get();
}
if applies("dir_color")
&& let Some(v) = &args.dir_color
{
c.colors.dir = v.get();
}
if applies("text_color") {
c.colors.text = args.text_color.as_ref().map(|v| v.get());
}
if applies("branch_color") {
c.colors.branch = args.branch_color.as_ref().map(|v| v.get());
}
if applies("line_number_color")
&& let Some(v) = &args.line_number_color
{
c.colors.line_number = v.get();
}
if applies("match_colors") && !args.match_colors.is_empty() {
c.colors.matches = args.match_colors.iter().map(|v| v.get()).collect();
}
if applies("selected_indicator_color") {
c.colors.selected_indicator = args.selected_indicator_color.as_ref().map(|v| v.get());
}
if applies("selected_bg_color")
&& let Some(v) = &args.selected_bg_color
{
c.colors.selected_bg = v.get();
}
if applies("filter_highlight_color")
&& let Some(v) = &args.filter_highlight_color
{
c.colors.filter_highlight = v.get();
}
return Ok(c);
}
let (path, is_dir) = match args.positional_path.as_ref().or(args.path.as_ref()) {
Some(p) => {
let p = process_path(p, true)?;
let d = p.is_dir();
(p, d)
}
None => {
let p = canonicalize(
&std::env::current_dir()
.map_err(|_| mes!("failed to get current working directory"))?,
)?;
let d = p.is_dir();
(p, d)
}
};
let mut regexps = Vec::new();
if let Some(expr) = &args.positional_regexp {
regexps.push(expr.clone());
}
regexps.extend(args.regexp.iter().cloned());
let context = args.context.unwrap_or(0);
let prefix_len = args.prefix_len;
let chars = Config::get_characters(&args);
Ok(Config {
path,
is_dir,
regexps,
globs: args.glob.clone(),
hidden: args.hidden,
line_number: args.line_number,
files: args.files,
count: args.count,
links: args.links,
trim: args.trim,
ignore: !args.no_ignore,
max_depth: args.max_depth,
max_length: args.max_length,
before_context: args.before_context.unwrap_or(context),
after_context: args.after_context.unwrap_or(context),
overview: args.overview,
branch_each: args.branch_each,
with_bold: !args.no_bold,
with_colors: !args.no_color,
prefix_len,
chars,
colors: Config::get_colors(&args),
core,
})
}
impl Config {
pub fn get_styling(matches: &ArgMatches) -> (bool, bool) {
let args = Args::from_arg_matches(matches).expect("failed to parse args");
(!args.no_bold, !args.no_color)
}
fn read_repeat_file(f: &Path) -> Result<Vec<OsString>, Message> {
let data = std::fs::read(f).map_err(|e| mes!("{}", e))?;
let mut pos = 0;
let mut args = Vec::new();
while pos < data.len() {
let len = u32::from_le_bytes(
data[pos..pos + size_of::<u32>()]
.try_into()
.map_err(|e| mes!("{}", e))?,
) as usize;
pos += size_of::<u32>();
let bytes = &data[pos..pos + len];
pos += len;
unsafe {
args.push(OsString::from_encoded_bytes_unchecked(bytes.to_vec()));
}
}
Ok(args)
}
pub fn handle_repeat(&self) -> Result<Option<Self>, Message> {
if let Some(f) = &self.core.repeat_file {
if self.core.repeat {
let args = Self::read_repeat_file(f)?;
let (matches, all_args) = get_matches(args).map_err(|e| mes!("{}", e))?;
let mut c = Self::get_config(matches, all_args)?;
c.core.selection_file = self.core.selection_file.clone();
c.core.repeat_file = self.core.repeat_file.clone();
c.core.live = self.core.live;
c.core.menu = self.core.menu;
c.core.select = self.core.select;
c.core.editor = self.core.editor.clone();
c.core.open_like = self.core.open_like.clone();
Ok(Some(c))
} else {
if !self.regexps.is_empty() || self.files {
save_to_repeat_file(f, &self.core.all_args)?;
}
Ok(None)
}
} else if self.core.repeat {
Err(mes!("cannot repeat without a {} specified", REPEAT_FILE))
} else {
Ok(None)
}
}
pub fn get_config(matches: ArgMatches, all_args: Vec<OsString>) -> Result<Self, Message> {
let args = Args::from_arg_matches(&matches).map_err(|e| mes!("{}", e))?;
let threads = args.threads.unwrap_or_else(|| {
std::thread::available_parallelism()
.map_or(1, |n| n.get())
.min(12)
});
let selection_file = args
.selection_file
.as_ref()
.map(|p| process_path(p, false))
.transpose()?;
let repeat_file = args
.repeat_file
.as_ref()
.map(|p| process_path(p, false))
.transpose()?;
let core = CoreConfig {
select: args.select,
menu: args.menu,
live: args.live,
auto_open: args.auto_open,
repeat: args.repeat,
threads,
editor: args.editor.clone(),
open_like: args.open_like.clone(),
selection_file,
repeat_file,
completion_target: args.completions,
all_args,
keys: Config::get_key_bindings(&args, &matches),
};
apply_matches(core, &matches, false)
}
fn get_colors(args: &Args) -> Colors {
Colors {
file: args
.file_color
.as_ref()
.map(|v| v.get())
.unwrap_or(style::FILE_COLOR_DEFAULT),
dir: args
.dir_color
.as_ref()
.map(|v| v.get())
.unwrap_or(style::DIR_COLOR_DEFAULT),
line_number: args
.line_number_color
.as_ref()
.map(|v| v.get())
.unwrap_or(style::LINE_NUMBER_COLOR_DEFAULT),
text: args.text_color.as_ref().map(|v| v.get()),
branch: args.branch_color.as_ref().map(|v| v.get()),
selected_bg: args
.selected_bg_color
.as_ref()
.map(|v| v.get())
.unwrap_or(style::SELECTED_BG_DEFAULT),
selected_indicator: args.selected_indicator_color.as_ref().map(|v| v.get()),
filter_highlight: args
.filter_highlight_color
.as_ref()
.map(|v| v.get())
.unwrap_or(style::FILTER_HIGHLIGHT_DEFAULT),
matches: if args.match_colors.is_empty() {
style::MATCHED_COLORS_DEFAULT.to_vec()
} else {
args.match_colors.iter().map(|v| v.get()).collect()
},
}
}
fn get_characters(args: &Args) -> Characters {
let v = args.char_vertical;
let h = args.char_horizontal;
let tl = args.char_top_left;
let tr = args.char_top_right;
let bl = args.char_bottom_left;
let br = args.char_bottom_right;
let tee = args.char_tee;
let spacer = args.prefix_len;
let ellipsis = args.ellipsis.clone();
let selected_indicator = args.selected_indicator.clone();
let selected_indicator_clear = " ".repeat(selected_indicator.chars().count());
let search_prompt = args.search_prompt.clone();
let search_prompt_inactive = args.search_prompt_inactive.clone();
let filter_prompt = args.filter_prompt.clone();
Characters {
bl,
br,
tl,
tr,
v,
h,
tee,
selected_indicator,
selected_indicator_clear,
match_with_next: format!("{}{}", tee, style::repeat(h, spacer - 1)),
match_no_next: format!("{}{}", bl, style::repeat(h, spacer - 1)),
spacer_vert: format!("{}{}", v, style::repeat(' ', spacer - 1)),
spacer: " ".repeat(spacer),
ellipsis,
search_prompt,
search_prompt_inactive,
filter_prompt,
}
}
fn get_key_bindings(args: &Args, matches: &ArgMatches) -> KeyBindings {
let all_ids = [
args::KEY_DOWN,
args::KEY_UP,
args::KEY_BIG_DOWN,
args::KEY_BIG_UP,
args::KEY_DOWN_PATH,
args::KEY_UP_PATH,
args::KEY_DOWN_SAME_DEPTH,
args::KEY_UP_SAME_DEPTH,
args::KEY_TOP,
args::KEY_BOTTOM,
args::KEY_PAGE_DOWN,
args::KEY_PAGE_UP,
args::KEY_CYCLE_VIEW,
args::KEY_HELP,
args::KEY_QUIT,
args::KEY_OPEN,
args::KEY_FOLD,
args::KEY_FILTER,
args::KEY_SEARCH,
];
let is_user_set = |id: &str| -> bool {
matches.value_source(id) == Some(clap::parser::ValueSource::CommandLine)
};
let overrides: HashSet<KeyCode> = all_ids
.iter()
.filter(|id| is_user_set(id))
.flat_map(|id| key_field_by_id(args, id))
.copied()
.collect();
let binding = |id: &str, keys: &[KeyCode]| -> Vec<KeyCode> {
if is_user_set(id) {
keys.to_vec()
} else {
keys.iter()
.copied()
.filter(|k| !overrides.contains(k))
.collect()
}
};
KeyBindings {
down: binding(args::KEY_DOWN, &args.key_down),
up: binding(args::KEY_UP, &args.key_up),
big_down: binding(args::KEY_BIG_DOWN, &args.key_big_down),
big_up: binding(args::KEY_BIG_UP, &args.key_big_up),
down_path: binding(args::KEY_DOWN_PATH, &args.key_down_path),
up_path: binding(args::KEY_UP_PATH, &args.key_up_path),
down_same_depth: binding(args::KEY_DOWN_SAME_DEPTH, &args.key_down_same_depth),
up_same_depth: binding(args::KEY_UP_SAME_DEPTH, &args.key_up_same_depth),
top: binding(args::KEY_TOP, &args.key_top),
bottom: binding(args::KEY_BOTTOM, &args.key_bottom),
page_down: binding(args::KEY_PAGE_DOWN, &args.key_page_down),
page_up: binding(args::KEY_PAGE_UP, &args.key_page_up),
cycle_view: binding(args::KEY_CYCLE_VIEW, &args.key_cycle_view),
help: binding(args::KEY_HELP, &args.key_help),
quit: binding(args::KEY_QUIT, &args.key_quit),
open: binding(args::KEY_OPEN, &args.key_open),
fold: binding(args::KEY_FOLD, &args.key_fold),
filter: binding(args::KEY_FILTER, &args.key_filter),
search: binding(args::KEY_SEARCH, &args.key_search),
submit_search: binding(args::KEY_SUBMIT_SEARCH, &args.key_submit_search),
}
}
}
fn key_field_by_id<'a>(args: &'a Args, id: &str) -> &'a [KeyCode] {
match id {
_ if id == args::KEY_DOWN => &args.key_down,
_ if id == args::KEY_UP => &args.key_up,
_ if id == args::KEY_BIG_DOWN => &args.key_big_down,
_ if id == args::KEY_BIG_UP => &args.key_big_up,
_ if id == args::KEY_DOWN_PATH => &args.key_down_path,
_ if id == args::KEY_UP_PATH => &args.key_up_path,
_ if id == args::KEY_DOWN_SAME_DEPTH => &args.key_down_same_depth,
_ if id == args::KEY_UP_SAME_DEPTH => &args.key_up_same_depth,
_ if id == args::KEY_TOP => &args.key_top,
_ if id == args::KEY_BOTTOM => &args.key_bottom,
_ if id == args::KEY_PAGE_DOWN => &args.key_page_down,
_ if id == args::KEY_PAGE_UP => &args.key_page_up,
_ if id == args::KEY_CYCLE_VIEW => &args.key_cycle_view,
_ if id == args::KEY_HELP => &args.key_help,
_ if id == args::KEY_QUIT => &args.key_quit,
_ if id == args::KEY_OPEN => &args.key_open,
_ if id == args::KEY_FOLD => &args.key_fold,
_ if id == args::KEY_FILTER => &args.key_filter,
_ if id == args::KEY_SEARCH => &args.key_search,
_ => &[],
}
}
const MENU_UNSUPPORTED: &[&str] = &[
args::SELECT,
args::MENU,
args::REPEAT,
args::REPEAT_FILE,
args::SELECTION_FILE,
args::AUTO_OPEN,
args::COMPLETIONS,
args::KEY_DOWN,
args::KEY_UP,
args::KEY_BIG_DOWN,
args::KEY_BIG_UP,
args::KEY_DOWN_PATH,
args::KEY_UP_PATH,
args::KEY_DOWN_SAME_DEPTH,
args::KEY_UP_SAME_DEPTH,
args::KEY_TOP,
args::KEY_BOTTOM,
args::KEY_PAGE_DOWN,
args::KEY_PAGE_UP,
args::KEY_CYCLE_VIEW,
args::KEY_HELP,
args::KEY_QUIT,
args::KEY_OPEN,
args::KEY_FOLD,
args::KEY_FILTER,
args::KEY_SEARCH,
args::KEY_SUBMIT_SEARCH,
];
pub fn save_to_repeat_file(file: &std::path::Path, args: &[OsString]) -> Result<(), Message> {
let mut buffer = Vec::new();
for arg in args {
let bytes = arg.as_encoded_bytes();
let len = bytes.len() as u32;
buffer.extend_from_slice(&len.to_le_bytes());
buffer.extend_from_slice(bytes);
}
std::fs::write(file, buffer).map_err(|e| mes!("{}", e))
}
pub fn parse_menu_query(query: &str) -> Result<Config, Message> {
let base = base_config();
let tokens = shlex::split(query).ok_or_else(|| mes!("invalid query syntax"))?;
let full_args: Vec<OsString> = tokens.iter().map(OsString::from).collect();
let matches = generate_command()
.try_get_matches_from(full_args)
.map_err(|mut e| {
use clap::error::ContextKind;
e.remove(ContextKind::Usage);
e.remove(ContextKind::Suggested);
mes!("{}", e.render().to_string().trim_end())
})?;
let unsupported: Vec<&str> = matches
.ids()
.map(|id| id.as_str())
.filter(|id| {
matches.value_source(id) == Some(clap::parser::ValueSource::CommandLine)
&& MENU_UNSUPPORTED.contains(id)
})
.collect();
if !unsupported.is_empty() {
return Err(mes!(
"unsupported in menu search: {}",
unsupported
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(", ")
));
}
let args = Args::from_arg_matches(&matches).map_err(|e| mes!("{}", e))?;
let explicit =
|id: &str| matches.value_source(id) == Some(clap::parser::ValueSource::CommandLine);
let mut core = base.core.clone();
core.select = false;
core.menu = true;
let mut c = apply_matches(core, &matches, true)?;
let explicit_pattern = explicit(args::EXPRESSION_POSITIONAL) || explicit(args::EXPRESSION);
if explicit(args::FILES) && !explicit_pattern {
c.regexps.clear();
}
if explicit_pattern && !explicit(args::FILES) {
c.files = false;
}
drop(args);
Ok(c)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::args;
static EXAMPLE_LONG_OPTS: &[&str] = &[
"posexpr",
"--line-number",
"--max-depth=5",
"--max-length=20",
"--no-ignore",
"--hidden",
"--threads=8",
"--count",
"--links",
"--trim",
"--select",
"--files",
"--overview",
"--glob=*.rs",
"--regexp=regexp1",
"--regexp=regexp2",
"--after-context=2",
"--before-context=3",
"--context=1",
];
pub fn get_config_from<I, T>(args: I) -> Config
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let matches = generate_command().get_matches_from(args);
Config::get_config(matches, Vec::new()).ok().unwrap()
}
#[test]
fn test_env_opts() {
unsafe { std::env::set_var(args::DEFAULT_OPTS_ENV_NAME, EXAMPLE_LONG_OPTS.join(" ")) };
let (matches, all_args) = get_matches(Vec::new()).unwrap();
let config = Config::get_config(matches, all_args).ok().unwrap();
check_parsed_config_from_example_opts(config);
}
#[test]
fn test_longs() {
let config = get_config_from(EXAMPLE_LONG_OPTS);
check_parsed_config_from_example_opts(config);
}
fn check_parsed_config_from_example_opts(config: Config) {
assert!(config.line_number);
assert_eq!(config.max_depth, Some(5));
assert_eq!(config.max_length, Some(20));
assert!(!config.ignore);
assert!(config.hidden);
assert!(config.core.threads == 8);
assert!(config.count);
assert!(config.links);
assert!(config.trim);
assert!(config.with_colors);
assert!(config.core.select);
assert!(config.files);
assert!(config.overview);
assert_eq!(config.globs, vec!["*.rs"]);
assert_eq!(config.before_context, 3);
assert_eq!(config.after_context, 2);
assert_eq!(config.regexps, vec!["posexpr", "regexp1", "regexp2"]);
}
#[test]
fn test_shorts() {
let config = get_config_from([
"posexpr",
"-n.csfl",
"-e=regexp1",
"-e=regexp2",
"-d=5",
"-g=*.rs",
"-o",
"-A=2",
"-B=3",
"-C=1",
]);
assert!(config.line_number);
assert!(config.hidden);
assert!(config.count);
assert!(config.core.select);
assert!(config.files);
assert!(config.links);
assert!(config.overview);
assert_eq!(config.max_depth, Some(5));
assert_eq!(config.globs, vec!["*.rs"]);
assert_eq!(config.after_context, 2);
assert_eq!(config.before_context, 3);
assert_eq!(config.regexps, vec!["posexpr", "regexp1", "regexp2"]);
}
#[test]
fn test_longs_files() {
let config = get_config_from([
"--files",
"--max-depth=5",
"--no-ignore",
"--hidden",
"--links",
"--select",
]);
assert_eq!(config.max_depth, Some(5));
assert!(!config.ignore);
assert!(config.hidden);
assert!(config.links);
assert!(config.with_colors);
assert!(config.core.select);
}
#[test]
fn test_key_binding_hardware_named() {
let config = get_config_from(["expression", "--key-quit=f10", "--key-down=x"]);
assert!(config.core.keys.quit.contains(&KeyCode::F(10)));
assert!(config.core.keys.down.contains(&KeyCode::Char('x')));
assert!(!config.core.keys.down.contains(&KeyCode::Char('j')));
assert!(!config.core.keys.down.contains(&KeyCode::Down));
}
#[test]
fn test_shorts_files() {
let config = get_config_from(["-.fslo", "-d=3"]);
assert!(config.hidden);
assert!(config.links);
assert!(config.overview);
assert_eq!(config.max_depth, Some(3));
assert!(config.core.select);
}
}