Skip to content

Commit

Permalink
Revamp preview window for argument selection (#398)
Browse files Browse the repository at this point in the history
  • Loading branch information
denisidoro committed Sep 11, 2020
1 parent 579eef7 commit 566f7c9
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 84 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "navi"
version = "2.10.0"
version = "2.11.0"
authors = ["Denis Isidoro <[email protected]>"]
edition = "2018"
description = "An interactive cheatsheet tool for the command-line"
Expand Down
4 changes: 2 additions & 2 deletions src/cmds/alfred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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();

Expand Down
117 changes: 71 additions & 46 deletions src/cmds/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`")?
Expand All @@ -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<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 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 <<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
Expand All @@ -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 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);
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/cmds/preview.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::display;

use anyhow::Error;

use std::process;

fn extract_elements(argstr: &str) -> (&str, &str, &str) {
Expand All @@ -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)
}
4 changes: 2 additions & 2 deletions src/common/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/common/url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 97 additions & 20 deletions src/display/terminal.rs
Original file line number Diff line number Diff line change
@@ -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),
Expand All @@ -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>())
Expand Down
Loading

0 comments on commit 566f7c9

Please sign in to comment.