From 566f7c9f2c09ce1a577a8b8621a123c151fdb220 Mon Sep 17 00:00:00 2001 From: Denis Isidoro Date: Fri, 11 Sep 2020 11:59:34 -0300 Subject: [PATCH] Revamp preview window for argument selection (#398) --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cmds/alfred.rs | 4 +- src/cmds/core.rs | 117 ++++++++++++++++++++++++--------------- src/cmds/preview.rs | 7 +++ src/common/clipboard.rs | 4 +- src/common/url.rs | 4 +- src/display/terminal.rs | 117 ++++++++++++++++++++++++++++++++------- src/env_vars.rs | 18 ++++++ src/finder.rs | 12 +++- src/handler.rs | 6 +- src/lib.rs | 1 + src/structures/config.rs | 19 +++++-- tests/core.bash | 2 +- 14 files changed, 231 insertions(+), 84 deletions(-) create mode 100644 src/env_vars.rs diff --git a/Cargo.lock b/Cargo.lock index 34501da..fef4774 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,7 +172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "navi" -version = "2.10.0" +version = "2.11.0" dependencies = [ "anyhow 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 094eca2..3865ccf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "navi" -version = "2.10.0" +version = "2.11.0" authors = ["Denis Isidoro "] edition = "2018" description = "An interactive cheatsheet tool for the command-line" diff --git a/src/cmds/alfred.rs b/src/cmds/alfred.rs index 9379bae..b541c8d 100644 --- a/src/cmds/alfred.rs +++ b/src/cmds/alfred.rs @@ -28,7 +28,7 @@ pub fn main(config: Config) -> Result<(), Error> { Ok(()) } -fn prompt_with_suggestions(suggestion: &Suggestion) -> Result { +fn prompt_finder(suggestion: &Suggestion) -> Result { let (suggestion_command, _suggestion_opts) = suggestion; let child = Command::new("bash") @@ -77,7 +77,7 @@ pub fn suggestions(config: Config, dry_run: bool) -> Result<(), Error> { display::alfred::print_items_start(Some(varname)); let command = command.context("Invalid command")?; - let lines = prompt_with_suggestions(command).context("Invalid lines")?; + let lines = prompt_finder(command).context("Invalid lines")?; writer.reset(); diff --git a/src/cmds/core.rs b/src/cmds/core.rs index d95ac0c..3fcd734 100644 --- a/src/cmds/core.rs +++ b/src/cmds/core.rs @@ -2,6 +2,7 @@ use crate::cheatsh; use crate::common::clipboard; use crate::common::shell::BashSpawnError; use crate::display; +use crate::env_vars; use crate::fetcher::Fetcher; use crate::filesystem; use crate::finder::Finder; @@ -37,7 +38,7 @@ fn gen_core_finder_opts(config: &Config) -> Result { Ok(opts) } -fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, &str, &str), Error> { +fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, &str, &str, &str), Error> { let mut lines = raw_snippet.split('\n'); let key = if !is_single { lines.next().context("Key was promised but not present in `selections`")? @@ -48,31 +49,65 @@ fn extract_from_selections(raw_snippet: &str, is_single: bool) -> Result<(&str, let mut parts = lines.next().context("No more parts in `selections`")?.split(display::DELIMITER).skip(3); let tags = parts.next().unwrap_or(""); - parts.next(); - + let comment = parts.next().unwrap_or(""); let snippet = parts.next().unwrap_or(""); - Ok((key, tags, snippet)) + Ok((key, tags, comment, snippet)) } -fn prompt_with_suggestions(variable_name: &str, config: &Config, suggestion: &Suggestion, _snippet: String) -> Result { - let (suggestion_command, suggestion_opts) = suggestion; +fn prompt_finder(variable_name: &str, config: &Config, suggestion: Option<&Suggestion>, variable_count: usize) -> Result { + env::remove_var(env_vars::PREVIEW_COLUMN); + env::remove_var(env_vars::PREVIEW_DELIMITER); + env::remove_var(env_vars::PREVIEW_MAP); - let child = Command::new("bash") - .stdout(Stdio::piped()) - .arg("-c") - .arg(&suggestion_command) - .spawn() - .map_err(|e| BashSpawnError::new(suggestion_command, e))?; + let (suggestions, opts) = if let Some(s) = suggestion { + let (suggestion_command, suggestion_opts) = s; - let suggestions = String::from_utf8(child.wait_with_output().context("Failed to wait and collect output from bash")?.stdout) - .context("Suggestions are invalid utf8")?; + if let Some(sopts) = suggestion_opts { + if let Some(c) = &sopts.column { + env::set_var(env_vars::PREVIEW_COLUMN, c.to_string()); + } + if let Some(d) = &sopts.delimiter { + env::set_var(env_vars::PREVIEW_DELIMITER, d); + } + if let Some(m) = &sopts.map { + env::set_var(env_vars::PREVIEW_MAP, m); + } + } - let opts = suggestion_opts.clone().unwrap_or_default(); - let opts = FinderOpts { + let child = Command::new("bash") + .stdout(Stdio::piped()) + .arg("-c") + .arg(&suggestion_command) + .spawn() + .map_err(|e| BashSpawnError::new(suggestion_command, e))?; + + let text = String::from_utf8(child.wait_with_output().context("Failed to wait and collect output from bash")?.stdout) + .context("Suggestions are invalid utf8")?; + + (text, suggestion_opts) + } else { + ('\n'.to_string(), &None) + }; + + let mut opts = FinderOpts { autoselect: !config.get_no_autoselect(), overrides: config.fzf_overrides_var.clone(), - prompt: Some(display::terminal::variable_prompt(variable_name)), - ..opts + preview: Some(format!( + r#"navi preview-var "$(cat < Result { - let opts = FinderOpts { - autoselect: false, - prompt: Some(display::terminal::variable_prompt(variable_name)), - suggestion_type: SuggestionType::Disabled, - preview_window: Some("up:1".to_string()), - ..Default::default() - }; - - let (output, _) = config - .finder - .call(opts, |_stdin| Ok(None)) - .context("finder was unable to prompt without suggestions")?; - - Ok(output) +fn unique_result_count(results: &[&str]) -> usize { + let mut vars = results.to_owned(); + vars.sort(); + vars.dedup(); + vars.len() } fn replace_variables_from_snippet(snippet: &str, tags: &str, variables: VariableMap, config: &Config) -> Result { let mut interpolated_snippet = String::from(snippet); + let variables_found: Vec<&str> = display::VAR_REGEX.find_iter(snippet).map(|m| m.as_str()).collect(); + let variable_count = unique_result_count(&variables_found); - for captures in display::VAR_REGEX.captures_iter(snippet) { - let bracketed_variable_name = &captures[0]; + for bracketed_variable_name in variables_found { let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; let env_value = env::var(variable_name); let value = if let Ok(e) = env_value { e + } else if let Some(suggestion) = variables.get_suggestion(&tags, &variable_name) { + let mut new_suggestion = suggestion.clone(); + new_suggestion.0 = replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone(), config)?; + prompt_finder(variable_name, &config, Some(&new_suggestion), variable_count)? } else { - variables - .get_suggestion(&tags, &variable_name) - .ok_or_else(|| anyhow!("No suggestions")) - .and_then(|suggestion| { - let mut new_suggestion = suggestion.clone(); - new_suggestion.0 = replace_variables_from_snippet(&new_suggestion.0, tags, variables.clone(), config)?; - - prompt_with_suggestions(variable_name, &config, &new_suggestion, interpolated_snippet.clone()) - }) - .or_else(|_| prompt_without_suggestions(variable_name, &config))? + prompt_finder(variable_name, &config, None, variable_count)? }; env::set_var(variable_name, &value); @@ -166,7 +187,11 @@ pub fn main(config: Config) -> Result<(), Error> { }) .context("Failed getting selection and variables from finder")?; - let (key, tags, snippet) = extract_from_selections(&raw_selection[..], config.get_best_match())?; + let (key, tags, comment, snippet) = extract_from_selections(&raw_selection, config.get_best_match())?; + + env::set_var(env_vars::PREVIEW_INITIAL_SNIPPET, &snippet); + env::set_var(env_vars::PREVIEW_TAGS, &tags); + env::set_var(env_vars::PREVIEW_COMMENT, &comment); let interpolated_snippet = display::with_new_lines( replace_variables_from_snippet(snippet, tags, variables.expect("No variables received from finder"), &config) diff --git a/src/cmds/preview.rs b/src/cmds/preview.rs index 36aa3c2..c540c52 100644 --- a/src/cmds/preview.rs +++ b/src/cmds/preview.rs @@ -1,5 +1,7 @@ use crate::display; + use anyhow::Error; + use std::process; fn extract_elements(argstr: &str) -> (&str, &str, &str) { @@ -15,3 +17,8 @@ pub fn main(line: &str) -> Result<(), Error> { display::terminal::preview(comment, tags, snippet); process::exit(0) } + +pub fn main_var(selection: &str, query: &str, variable: &str) -> Result<(), Error> { + display::terminal::preview_var(selection, query, variable); + process::exit(0) +} diff --git a/src/common/clipboard.rs b/src/common/clipboard.rs index 1295afb..12b7c58 100644 --- a/src/common/clipboard.rs +++ b/src/common/clipboard.rs @@ -25,9 +25,9 @@ _copy() { .arg( format!( r#"{} - read -r -d '' x <<'EOF' + read -r -d '' x <<'NAVIEOF' {} -EOF +NAVIEOF echo -n "$x" | _copy"#, cmd, text diff --git a/src/common/url.rs b/src/common/url.rs index a37a49b..0856eb3 100644 --- a/src/common/url.rs +++ b/src/common/url.rs @@ -22,9 +22,9 @@ _open_url() { let cmd = format!( r#"{} -read -r -d '' url <<'EOF' +read -r -d '' url <<'NAVIEOF' {} -EOF +NAVIEOF _open_url "$url""#, code, url diff --git a/src/display/terminal.rs b/src/display/terminal.rs index 39fe379..d0968e8 100644 --- a/src/display/terminal.rs +++ b/src/display/terminal.rs @@ -1,42 +1,35 @@ use crate::common::terminal_width; use crate::display; +use crate::env_vars; +use crate::finder; use crate::structures::item::Item; use std::cmp::max; +use std::collections::HashSet; use std::env; +use std::str::FromStr; use termion::color; -fn parse_env_var_u8(varname: &str) -> Option { +// TODO: extract +pub fn parse_env_var(varname: &str) -> Option { if let Ok(x) = env::var(varname) { - x.parse::().ok() - } else { - None - } -} - -fn parse_env_var_u16(varname: &str) -> Option { - if let Ok(x) = env::var(varname) { - x.parse::().ok() + x.parse::().ok() } else { None } } lazy_static! { - pub static ref TAG_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var_u8("NAVI_TAG_COLOR").unwrap_or(14)); - pub static ref COMMENT_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var_u8("NAVI_COMMENT_COLOR").unwrap_or(4)); - pub static ref SNIPPET_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var_u8("NAVI_SNIPPET_COLOR").unwrap_or(7)); - pub static ref TAG_WIDTH_PERCENTAGE: u16 = parse_env_var_u16("NAVI_TAG_WIDTH").unwrap_or(20); - pub static ref COMMENT_WIDTH_PERCENTAGE: u16 = parse_env_var_u16("NAVI_COMMENT_WIDTH").unwrap_or(40); -} - -pub fn variable_prompt(varname: &str) -> String { - format!("{}: ", varname) + pub static ref TAG_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var(env_vars::TAG_COLOR).unwrap_or(14)); + pub static ref COMMENT_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var(env_vars::COMMENT_COLOR).unwrap_or(4)); + pub static ref SNIPPET_COLOR: color::AnsiValue = color::AnsiValue(parse_env_var(env_vars::SNIPPET_COLOR).unwrap_or(7)); + pub static ref TAG_WIDTH_PERCENTAGE: u16 = parse_env_var(env_vars::TAG_WIDTH).unwrap_or(20); + pub static ref COMMENT_WIDTH_PERCENTAGE: u16 = parse_env_var(env_vars::COMMENT_WIDTH).unwrap_or(40); } pub fn preview(comment: &str, tags: &str, snippet: &str) { println!( "{comment_color}{comment} {tag_color}{tags} \n{snippet_color}{snippet}", - comment = format!("# {}", comment), + comment = comment.to_string(), tags = format!("[{}]", tags), snippet = display::fix_newlines(snippet), comment_color = color::Fg(*COMMENT_COLOR), @@ -45,6 +38,90 @@ pub fn preview(comment: &str, tags: &str, snippet: &str) { ); } +pub fn wrapped_by_map(text: &str, map: Option<&str>) -> String { + if map.is_none() { + text.to_string() + } else { + format!("map({})", text) + } +} + +fn get_env_var(name: &str) -> String { + if let Ok(v) = env::var(name) { + v + } else { + panic!(format!("{} not set", name)) + } +} + +pub fn preview_var(selection: &str, query: &str, variable: &str) { + let snippet = &get_env_var(env_vars::PREVIEW_INITIAL_SNIPPET); + let tags = get_env_var(env_vars::PREVIEW_TAGS); + let comment = get_env_var(env_vars::PREVIEW_COMMENT); + let column = display::terminal::parse_env_var(env_vars::PREVIEW_COLUMN); + let delimiter = env::var(env_vars::PREVIEW_DELIMITER).ok(); + let map = env::var(env_vars::PREVIEW_MAP).ok(); + + let reset = color::Fg(color::Reset); + let active_color = color::Fg(*TAG_COLOR); + let inactive_color = color::Fg(*COMMENT_COLOR); + + let mut colored_snippet = String::from(snippet); + let mut variables = String::from(""); + let mut visited_vars: HashSet<&str> = HashSet::new(); + + for bracketed_variable_name in display::VAR_REGEX.find_iter(snippet).map(|m| m.as_str()) { + let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1]; + + if visited_vars.contains(variable_name) { + continue; + } else { + visited_vars.insert(variable_name); + } + + let is_current = variable_name == variable; + let variable_color = if is_current { active_color } else { inactive_color }; + + let value = if is_current { + let v = selection.trim_matches('\''); + if v.is_empty() { query.trim_matches('\'') } else { v }.to_string() + } else if let Ok(v) = env::var(&variable_name) { + v + } else { + "".to_string() + }; + + let replacement = format!( + "{color}{variable}{reset}", + color = variable_color, + variable = bracketed_variable_name, + reset = reset + ); + + colored_snippet = colored_snippet.replacen(bracketed_variable_name, &replacement, 999); + + variables = format!( + "{variables}\n{color}{variable}{reset} = {value}", + variables = variables, + color = variable_color, + variable = variable_name, + reset = reset, + value = wrapped_by_map(&finder::get_column(value, column, delimiter.as_deref()), map.as_deref()) + ); + } + + println!( + "{comment_color}{comment} {tag_color}{tags}{reset} \n{snippet}\n{variables}", + comment = comment, + tags = format!("[{}]", tags), + snippet = display::fix_newlines(&colored_snippet), + comment_color = color::Fg(*COMMENT_COLOR), + tag_color = color::Fg(*TAG_COLOR), + variables = variables, + reset = reset + ); +} + fn limit_str(text: &str, length: usize) -> String { if text.len() > length { format!("{}…", text.chars().take(length - 1).collect::()) diff --git a/src/env_vars.rs b/src/env_vars.rs new file mode 100644 index 0000000..0b280ea --- /dev/null +++ b/src/env_vars.rs @@ -0,0 +1,18 @@ +pub const PREVIEW_INITIAL_SNIPPET: &str = "NAVI_PREVIEW_INITIAL_SNIPPET"; +pub const PREVIEW_TAGS: &str = "NAVI_PREVIEW_TAGS"; +pub const PREVIEW_COMMENT: &str = "NAVI_PREVIEW_COMMENT"; +pub const PREVIEW_COLUMN: &str = "NAVI_PREVIEW_COLUMN"; +pub const PREVIEW_DELIMITER: &str = "NAVI_PREVIEW_DELIMITER"; +pub const PREVIEW_MAP: &str = "NAVI_PREVIEW_MAP"; + +pub const TAG_COLOR: &str = "NAVI_TAG_COLOR"; +pub const COMMENT_COLOR: &str = "NAVI_COMMENT_COLOR"; +pub const SNIPPET_COLOR: &str = "NAVI_SNIPPET_COLOR"; + +pub const TAG_WIDTH: &str = "NAVI_TAG_WIDTH"; +pub const COMMENT_WIDTH: &str = "NAVI_COMMENT_WIDTH"; + +pub const PATH: &str = "NAVI_PATH"; +pub const FZF_OVERRIDES: &str = "NAVI_FZF_OVERRIDES"; +pub const FZF_OVERRIDES_VAR: &str = "NAVI_FZF_OVERRIDES_VAR"; +pub const FINDER: &str = "NAVI_FINDER"; diff --git a/src/finder.rs b/src/finder.rs index b1f7d6b..d35a8ab 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -35,7 +35,8 @@ fn apply_map(text: String, map_fn: Option) -> String { } } -fn get_column(text: String, column: Option, delimiter: Option<&str>) -> String { +// TODO: extract +pub fn get_column(text: String, column: Option, delimiter: Option<&str>) -> String { if let Some(c) = column { let mut result = String::from(""); let re = regex::Regex::new(delimiter.unwrap_or(r"\s\s+")).expect("Invalid regex"); @@ -113,9 +114,14 @@ impl Finder for FinderChoice { let mut command = Command::new(&finder_str); let opts = finder_opts.clone(); + let preview_height = match self { + FinderChoice::Skim => 3, + _ => 2, + }; + command.args(&[ "--preview-window", - "up:2", + format!("up:{}", preview_height).as_str(), "--with-nth", "1,2,3", "--delimiter", @@ -135,7 +141,7 @@ impl Finder for FinderChoice { command.arg("--multi"); } SuggestionType::Disabled => { - command.args(&["--print-query", "--no-select-1", "--height", "1"]); + command.args(&["--print-query", "--no-select-1"]); } SuggestionType::SnippetSelection => { command.args(&["--expect", "ctrl-y,enter"]); diff --git a/src/handler.rs b/src/handler.rs index 9a6a976..b6914be 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,5 +1,5 @@ use crate::cmds; -use crate::structures::config::Command::{Alfred, Fn, Preview, Repo, Widget}; +use crate::structures::config::Command::{Alfred, Fn, Preview, PreviewVar, Repo, Widget}; use crate::structures::config::{AlfredCommand, Config, RepoCommand}; use anyhow::Context; use anyhow::Error; @@ -9,7 +9,9 @@ pub fn handle_config(config: Config) -> Result<(), Error> { None => cmds::core::main(config), Some(c) => match c { - Preview { line } => cmds::preview::main(&line[..]), + Preview { line } => cmds::preview::main(&line), + + PreviewVar { selection, query, variable } => cmds::preview::main_var(&selection, &query, &variable), Widget { shell } => cmds::shell::main(&shell).context("Failed to print shell widget code"), diff --git a/src/lib.rs b/src/lib.rs index b116e7d..637a826 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ mod cheatsh; mod cmds; mod common; mod display; +mod env_vars; mod fetcher; mod filesystem; mod finder; diff --git a/src/structures/config.rs b/src/structures/config.rs index 2d775f5..317199a 100644 --- a/src/structures/config.rs +++ b/src/structures/config.rs @@ -1,3 +1,4 @@ +use crate::env_vars; use crate::finder::FinderChoice; use anyhow::Error; use structopt::{clap::AppSettings, StructOpt}; @@ -37,7 +38,7 @@ EXAMPLES: #[structopt(setting = AppSettings::AllowLeadingHyphen)] pub struct Config { /// List of :-separated paths containing .cheat files - #[structopt(short, long, env = "NAVI_PATH")] + #[structopt(short, long, env = env_vars::PATH)] pub path: Option, /// [Experimental] Instead of executing a snippet, saves it to a file @@ -73,15 +74,15 @@ pub struct Config { query: Option, /// finder overrides for cheat selection - #[structopt(long, env = "NAVI_FZF_OVERRIDES")] + #[structopt(long, env = env_vars::FZF_OVERRIDES)] pub fzf_overrides: Option, /// finder overrides for variable selection - #[structopt(long, env = "NAVI_FZF_OVERRIDES_VAR")] + #[structopt(long, env = env_vars::FZF_OVERRIDES_VAR)] pub fzf_overrides_var: Option, /// which finder application to use - #[structopt(long, env = "NAVI_FINDER", default_value = "fzf", parse(try_from_str = parse_finder))] + #[structopt(long, env = env_vars::FINDER, default_value = "fzf", parse(try_from_str = parse_finder))] pub finder: FinderChoice, #[structopt(subcommand)] @@ -122,6 +123,16 @@ pub enum Command { /// Selection line line: String, }, + /// Used for fzf's preview window + #[structopt(setting = AppSettings::Hidden)] + PreviewVar { + /// Selection line + selection: String, + /// Query match + query: String, + /// Typed text + variable: String, + }, /// Shows the path for shell widget files Widget { /// bash, zsh or fish diff --git a/tests/core.bash b/tests/core.bash index fc1f711..865c1fa 100644 --- a/tests/core.bash +++ b/tests/core.bash @@ -63,4 +63,4 @@ test::finish() { log::success "All ${PASSED} tests passed! :)" exit 0 fi -} \ No newline at end of file +}