diff --git a/Cargo.lock b/Cargo.lock index 0e1be91f..8ce8afae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,13 +245,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "navi" -version = "2.2.0" +version = "2.3.0" dependencies = [ "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "raw_tty 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "shellwords 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", "terminal_size 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -443,6 +444,15 @@ name = "semver-parser" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "shellwords" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "smallvec" version = "1.2.0" @@ -698,6 +708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum same-file 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum shellwords 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "685f0e9b0efe23d26e60a780d8dcd3ac95e90975814de9bc6f48e5d609b5d0f5" "checksum smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5c2fb2ec9bcd216a5b0d0ccf31ab17b5ed1d627960edff65bbe95d3ce221cefc" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum structopt 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe43617218c0805c6eb37160119dc3c548110a67786da7218d1c6555212f073" diff --git a/Cargo.toml b/Cargo.toml index d59fc2d1..45595aed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "navi" -version = "2.2.0" +version = "2.3.0" authors = ["Denis Isidoro "] edition = "2018" description = "An interactive cheatsheet tool for the command-line" @@ -23,6 +23,7 @@ lazy_static = "1.4.0" dirs = "2.0.0" terminal_size = "0.1.10" walkdir = "2" +shellwords = "1.0.0" [dependencies.git2] version = "0.10.0" diff --git a/README.md b/README.md index 75cd1d14..946d8df4 100644 --- a/README.md +++ b/README.md @@ -202,9 +202,15 @@ $ image_id: docker images --- --column 3 --header-lines 1 --delimiter '\s\s+' The supported parameters are: - `--prevent-extra` *(experimental)*: limits the user to select one of the suggestions; - `--column `: extracts a single column from the selected result; -- `--delimiter `: delimits columns + forwarded option to `fzf`; -- `--multi`: forwarded option to `fzf`; -- `--header-lines `: forwarded option to `fzf`; + +In addition, it's possible to forward the following parameters to `fzf`: +- `--header-lines `; +- `--delimiter `; +- `--query `; +- `--filter `; +- `--header `; +- `--preview `; +- `--preview-window `. ### Variable dependency diff --git a/src/flows/core.rs b/src/flows/core.rs index 7bb6eb3d..c2814ed4 100644 --- a/src/flows/core.rs +++ b/src/flows/core.rs @@ -4,8 +4,8 @@ use crate::flows; use crate::fzf; use crate::handler; use crate::parser; -use crate::structures::cheat::{Suggestion, SuggestionType, VariableMap}; -use crate::structures::fzf::Opts as FzfOpts; +use crate::structures::cheat::{Suggestion, VariableMap}; +use crate::structures::fzf::{Opts as FzfOpts, SuggestionType}; use crate::structures::option; use crate::structures::option::Config; use regex::Regex; @@ -73,7 +73,7 @@ fn prompt_with_suggestions( for (key, value) in values.iter() { vars_cmd.push_str(format!("{}=\"{}\"; ", key, value).as_str()); } - let (suggestion_command, suggestion_options) = &suggestion; + let (suggestion_command, suggestion_opts) = suggestion; let command = format!("{} {}", vars_cmd, suggestion_command); let child = Command::new("bash") @@ -85,18 +85,12 @@ fn prompt_with_suggestions( let suggestions = String::from_utf8(child.wait_with_output().unwrap().stdout).unwrap(); - let mut opts = FzfOpts { + let opts = suggestion_opts.clone().unwrap_or_default(); + let opts = FzfOpts { autoselect: !config.no_autoselect, overrides: config.fzf_overrides_var.clone(), prompt: Some(display::variable_prompt(varname)), - ..Default::default() - }; - - if let Some(o) = &suggestion_options { - opts.suggestion_type = o.suggestion_type; - opts.header_lines = o.header_lines; - opts.column = o.column; - opts.delimiter = o.delimiter.clone(); + ..opts }; let (output, _) = fzf::call(opts, |stdin| { @@ -162,9 +156,9 @@ fn with_new_lines(txt: String) -> String { pub fn main(variant: Variant, config: Config, contains_key: bool) -> Result<(), Box> { let _ = display::WIDTHS; - let (raw_selection, variables) = fzf::call(gen_core_fzf_opts(variant, &config), |stdin| { - Some(parser::read_all(&config, stdin)) - }); + let opts = gen_core_fzf_opts(variant, &config); + let (raw_selection, variables) = + fzf::call(opts, |stdin| Some(parser::read_all(&config, stdin))); let (key, tags, snippet) = extract_from_selections(&raw_selection[..], contains_key); diff --git a/src/flows/repo.rs b/src/flows/repo.rs index e1aa13f7..71eca4fd 100644 --- a/src/flows/repo.rs +++ b/src/flows/repo.rs @@ -1,8 +1,7 @@ use crate::filesystem; use crate::fzf; use crate::git; -use crate::structures::cheat::SuggestionType; -use crate::structures::fzf::Opts as FzfOpts; +use crate::structures::fzf::{Opts as FzfOpts, SuggestionType}; use git2::Repository; use std::error::Error; use std::fs; @@ -66,14 +65,13 @@ pub fn add(uri: String) -> Result<(), Box> { .collect::>() .join("\n"); - let overrides = "--preview-window right:30%".to_string(); let opts = FzfOpts { suggestion_type: SuggestionType::MultipleSelections, preview: Some(format!("cat '{}/{{}}'", tmp_path_str)), header: Some( "Select the cheatsheets you want to import with then hit ".to_string(), ), - overrides: Some(overrides), + preview_window: Some("--preview-window right:30%".to_string()), ..Default::default() }; diff --git a/src/fzf.rs b/src/fzf.rs index 70bf2cb0..eca0537f 100644 --- a/src/fzf.rs +++ b/src/fzf.rs @@ -1,6 +1,6 @@ use crate::display; -use crate::structures::cheat::{SuggestionType, VariableMap}; -use crate::structures::fzf::Opts; +use crate::structures::cheat::VariableMap; +use crate::structures::fzf::{Opts, SuggestionType}; use std::process; use std::process::{Command, Stdio}; @@ -86,6 +86,10 @@ where fzf_command.args(&["--prompt", &p]); } + if let Some(pw) = opts.preview_window { + fzf_command.args(&["--preview-window", &pw]); + } + if opts.header_lines > 0 { fzf_command.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]); } @@ -114,12 +118,12 @@ where }; let stdin = child.stdin.as_mut().unwrap(); - let result = stdin_fn(stdin); + let result_map = stdin_fn(stdin); let out = child.wait_with_output().unwrap(); let text = match out.status.code() { - Some(0) | Some(1) => String::from_utf8(out.stdout).unwrap(), + Some(0) | Some(1) | Some(2) => String::from_utf8(out.stdout).unwrap(), Some(130) => process::exit(130), _ => { let err = String::from_utf8(out.stderr) @@ -128,14 +132,13 @@ where } }; - ( - get_column( - parse_output_single(text, opts.suggestion_type), - opts.column, - opts.delimiter.as_deref(), - ), - result, - ) + let out = get_column( + parse_output_single(text, opts.suggestion_type), + opts.column, + opts.delimiter.as_deref(), + ); + + (out, result_map) } fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> String { diff --git a/src/parser.rs b/src/parser.rs index 30470db9..9fdceb94 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,7 +1,8 @@ use crate::display; use crate::filesystem; -use crate::structures::cheat::{SuggestionOpts, SuggestionType, VariableMap}; +use crate::structures::cheat::VariableMap; use crate::structures::fnv::HashLine; +use crate::structures::fzf::{Opts as FzfOpts, SuggestionType}; use crate::structures::option::Config; use crate::welcome; use regex::Regex; @@ -9,47 +10,43 @@ use std::collections::HashSet; use std::fs; use std::io::Write; -fn remove_quotes(txt: &str) -> String { - txt.replace('"', "").replace('\'', "") -} - -fn parse_opts(text: &str) -> SuggestionOpts { - let mut header_lines: u8 = 0; - let mut column: Option = None; +fn parse_opts(text: &str) -> FzfOpts { let mut multi = false; let mut prevent_extra = false; - let mut delimiter: Option = None; - - let mut parts = text.split(' '); + let mut opts = FzfOpts::default(); + let parts_vec = shellwords::split(text).unwrap(); + let mut parts = parts_vec.into_iter(); while let Some(p) = parts.next() { - match p { + match p.as_str() { "--multi" => multi = true, "--prevent-extra" => prevent_extra = true, - "--header" | "--headers" | "--header-lines" => { - header_lines = remove_quotes(parts.next().unwrap()).parse::().unwrap() - } - "--column" => { - column = Some(remove_quotes(parts.next().unwrap()).parse::().unwrap()) + "--headers" | "--header-lines" => { + opts.header_lines = parts.next().unwrap().parse::().unwrap() } - "--delimiter" => delimiter = Some(remove_quotes(parts.next().unwrap()).to_string()), + "--column" => opts.column = Some(parts.next().unwrap().parse::().unwrap()), + "--delimiter" => opts.delimiter = Some(parts.next().unwrap().to_string()), + "--query" => opts.query = Some(parts.next().unwrap().to_string()), + "--filter" => opts.filter = Some(parts.next().unwrap().to_string()), + "--preview" => opts.preview = Some(parts.next().unwrap().to_string()), + "--preview-window" => opts.preview_window = Some(parts.next().unwrap().to_string()), + "--header" => opts.header = Some(parts.next().unwrap().to_string()), + "--overrides" => opts.overrides = Some(parts.next().unwrap().to_string()), _ => (), } } - SuggestionOpts { - header_lines, - column, - delimiter, - suggestion_type: match (multi, prevent_extra) { - (true, _) => SuggestionType::MultipleSelections, // multi wins over allow-extra - (false, false) => SuggestionType::SingleRecommendation, - (false, true) => SuggestionType::SingleSelection, - }, - } + let suggestion_type = match (multi, prevent_extra) { + (true, _) => SuggestionType::MultipleSelections, // multi wins over allow-extra + (false, false) => SuggestionType::SingleRecommendation, + (false, true) => SuggestionType::SingleSelection, + }; + opts.suggestion_type = suggestion_type; + + opts } -fn parse_variable_line(line: &str) -> (&str, &str, Option) { +fn parse_variable_line(line: &str) -> (&str, &str, Option) { let re = Regex::new(r"^\$\s*([^:]+):(.*)").unwrap(); let caps = re.captures(line).unwrap(); let variable = caps.get(1).unwrap().as_str().trim(); @@ -200,11 +197,12 @@ mod tests { assert_eq!(variable, "user"); assert_eq!( command_options, - Some(SuggestionOpts { + Some(FzfOpts { header_lines: 0, column: None, delimiter: None, - suggestion_type: SuggestionType::SingleRecommendation + suggestion_type: SuggestionType::SingleRecommendation, + ..Default::default() }) ); } @@ -220,11 +218,12 @@ mod tests { read_file(path, &mut variables, &mut visited_lines, child_stdin); let expected_suggestion = ( r#" echo -e "$(whoami)\nroot" "#.to_string(), - Some(SuggestionOpts { + Some(FzfOpts { header_lines: 0, column: None, delimiter: None, suggestion_type: SuggestionType::SingleRecommendation, + ..Default::default() }), ); let actual_suggestion = variables.get("ssh", "user"); diff --git a/src/structures/cheat.rs b/src/structures/cheat.rs index a79f1d63..ef9230a5 100644 --- a/src/structures/cheat.rs +++ b/src/structures/cheat.rs @@ -1,29 +1,8 @@ use crate::structures::fnv::HashLine; +use crate::structures::fzf::Opts; use std::collections::HashMap; -#[derive(Debug, PartialEq)] -pub struct SuggestionOpts { - pub header_lines: u8, - pub column: Option, - pub delimiter: Option, - pub suggestion_type: SuggestionType, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SuggestionType { - /// fzf will not print any suggestions - Disabled, - /// fzf will only select one of the suggestions - SingleSelection, - /// fzf will select multiple suggestions - MultipleSelections, - /// fzf will select one of the suggestions or use the query - SingleRecommendation, - /// initial snippet selection - SnippetSelection, -} - -pub type Suggestion = (String, Option); +pub type Suggestion = (String, Option); fn gen_key(tags: &str, variable: &str) -> u64 { format!("{};{}", tags, variable).hash_line() diff --git a/src/structures/fzf.rs b/src/structures/fzf.rs index c24e1433..8549233f 100644 --- a/src/structures/fzf.rs +++ b/src/structures/fzf.rs @@ -1,10 +1,10 @@ -use crate::structures::cheat::SuggestionType; - +#[derive(Debug, PartialEq, Clone)] pub struct Opts { pub query: Option, pub filter: Option, pub prompt: Option, pub preview: Option, + pub preview_window: Option, pub autoselect: bool, pub overrides: Option, pub header_lines: u8, @@ -21,6 +21,7 @@ impl Default for Opts { filter: None, autoselect: true, preview: None, + preview_window: None, overrides: None, header_lines: 0, header: None, @@ -31,3 +32,17 @@ impl Default for Opts { } } } + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SuggestionType { + /// fzf will not print any suggestions + Disabled, + /// fzf will only select one of the suggestions + SingleSelection, + /// fzf will select multiple suggestions + MultipleSelections, + /// fzf will select one of the suggestions or use the query + SingleRecommendation, + /// initial snippet selection + SnippetSelection, +} diff --git a/tests/cheats/more_cases.cheat b/tests/cheats/more_cases.cheat index dc3003cb..651001a0 100644 --- a/tests/cheats/more_cases.cheat +++ b/tests/cheats/more_cases.cheat @@ -32,6 +32,9 @@ echo "I like these examples: "$(printf '%s' "" | sed 's/^..*$/"&"/' | # multiple replacements -> "foo" echo " " +# with preview +cat "" + $ x: echo '1 2 3' | tr ' ' '\n' $ y: echo 'a b c' | tr ' ' '\n' $ z: echo 'foo bar' | tr ' ' '\n' @@ -39,8 +42,9 @@ $ table_elem: echo -e '0 rust rust-lang.org\n1 clojure clojure.org' --- $ table_elem2: echo -e '0;rust;rust-lang.org\n1;clojure;clojure.org' --- --column 2 --delimiter ';' $ multi_col: ls -la | awk '{print $1, $9}' --- --column 2 --delimiter '\s' --multi $ langs: echo 'clojure rust javascript' | tr ' ' '\n' --- --multi -$ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --multi -$ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz' +$ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --mult +$ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz'i +$ file: ls . --- --preview 'cat {}' --preview-window '50%' # this should be displayed echo hi \ No newline at end of file