Revamp preview window for argument selection (#398)

This commit is contained in:
Denis Isidoro 2020-09-11 11:59:34 -03:00
parent 579eef7c74
commit 566f7c9f2c
14 changed files with 231 additions and 84 deletions

2
Cargo.lock generated
View file

@ -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)",

View file

@ -1,6 +1,6 @@
[package]
name = "navi"
version = "2.10.0"
version = "2.11.0"
authors = ["Denis Isidoro <denis_isidoro@live.com>"]
edition = "2018"
description = "An interactive cheatsheet tool for the command-line"

View file

@ -28,7 +28,7 @@ pub fn main(config: Config) -> Result<(), Error> {
Ok(())
}
fn prompt_with_suggestions(suggestion: &Suggestion) -> Result<String, Error> {
fn prompt_finder(suggestion: &Suggestion) -> Result<String, Error> {
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();

View file

@ -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<FinderOpts, Error> {
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,14 +49,30 @@ 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<String, Error> {
let (suggestion_command, suggestion_opts) = suggestion;
fn prompt_finder(variable_name: &str, config: &Config, suggestion: Option<&Suggestion>, variable_count: usize) -> Result<String, Error> {
env::remove_var(env_vars::PREVIEW_COLUMN);
env::remove_var(env_vars::PREVIEW_DELIMITER);
env::remove_var(env_vars::PREVIEW_MAP);
let (suggestions, opts) = if let Some(s) = suggestion {
let (suggestion_command, suggestion_opts) = s;
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 child = Command::new("bash")
.stdout(Stdio::piped())
@ -64,15 +81,33 @@ fn prompt_with_suggestions(variable_name: &str, config: &Config, suggestion: &Su
.spawn()
.map_err(|e| BashSpawnError::new(suggestion_command, e))?;
let suggestions = String::from_utf8(child.wait_with_output().context("Failed to wait and collect output from bash")?.stdout)
let text = String::from_utf8(child.wait_with_output().context("Failed to wait and collect output from bash")?.stdout)
.context("Suggestions are invalid utf8")?;
let opts = suggestion_opts.clone().unwrap_or_default();
let opts = FinderOpts {
(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 <<NAVIEOF
{{}}
NAVIEOF
)" "$(cat <<NAVIEOF
{{q}}
NAVIEOF
)" "{}""#,
variable_name
)),
preview_window: Some(format!("up:{}", variable_count + 3)),
..opts.clone().unwrap_or_default()
};
if suggestion.is_none() {
opts.suggestion_type = SuggestionType::Disabled;
};
let (output, _) = config
@ -86,45 +121,31 @@ fn prompt_with_suggestions(variable_name: &str, config: &Config, suggestion: &Su
Ok(output)
}
fn prompt_without_suggestions(variable_name: &str, config: &Config) -> Result<String, Error> {
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<String, Error> {
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 {
variables
.get_suggestion(&tags, &variable_name)
.ok_or_else(|| anyhow!("No suggestions"))
.and_then(|suggestion| {
} 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_with_suggestions(variable_name, &config, &new_suggestion, interpolated_snippet.clone())
})
.or_else(|_| prompt_without_suggestions(variable_name, &config))?
prompt_finder(variable_name, &config, Some(&new_suggestion), variable_count)?
} else {
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)

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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<u8> {
// TODO: extract
pub fn parse_env_var<T: FromStr>(varname: &str) -> Option<T> {
if let Ok(x) = env::var(varname) {
x.parse::<u8>().ok()
} else {
None
}
}
fn parse_env_var_u16(varname: &str) -> Option<u16> {
if let Ok(x) = env::var(varname) {
x.parse::<u16>().ok()
x.parse::<T>().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::<String>())

18
src/env_vars.rs Normal file
View file

@ -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";

View file

@ -35,7 +35,8 @@ fn apply_map(text: String, map_fn: Option<String>) -> String {
}
}
fn get_column(text: String, column: Option<u8>, delimiter: Option<&str>) -> String {
// TODO: extract
pub fn get_column(text: String, column: Option<u8>, 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"]);

View file

@ -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"),

View file

@ -7,6 +7,7 @@ mod cheatsh;
mod cmds;
mod common;
mod display;
mod env_vars;
mod fetcher;
mod filesystem;
mod finder;

View file

@ -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<String>,
/// [Experimental] Instead of executing a snippet, saves it to a file
@ -73,15 +74,15 @@ pub struct Config {
query: Option<String>,
/// finder overrides for cheat selection
#[structopt(long, env = "NAVI_FZF_OVERRIDES")]
#[structopt(long, env = env_vars::FZF_OVERRIDES)]
pub fzf_overrides: Option<String>,
/// 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<String>,
/// 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