Add support for post-choice manipulation (#335)

Fixes #325
This commit is contained in:
Denis Isidoro 2020-04-10 23:01:35 -03:00 committed by GitHub
parent 1f5db34902
commit 6678cda032
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 104 additions and 87 deletions

View file

@ -191,12 +191,17 @@ For lines starting with `$` you can use `---` to customize the behavior of `fzf`
# This will pick the 3rd column and use the first line as header # This will pick the 3rd column and use the first line as header
docker rmi <image_id> docker rmi <image_id>
# Even though "false/true" is displayed, this will print "0/1"
echo <mapped>
$ image_id: docker images --- --column 3 --header-lines 1 --delimiter '\s\s+' $ image_id: docker images --- --column 3 --header-lines 1 --delimiter '\s\s+'
$ mapped: echo 'false true' | tr ' ' '\n' --- --map "[[ $0 == t* ]] && echo 1 || echo 0"
``` ```
The supported parameters are: The supported parameters are:
- `--prevent-extra` *(experimental)*: limits the user to select one of the suggestions; - `--prevent-extra` *(experimental)*: limits the user to select one of the suggestions;
- `--column <number>`: extracts a single column from the selected result; - `--column <number>`: extracts a single column from the selected result;
- `--map <bash_code>` *(experimental)*: applies a map function to the selected variable value;
In addition, it's possible to forward the following parameters to `fzf`: In addition, it's possible to forward the following parameters to `fzf`:
- `--multi`; - `--multi`;
@ -205,7 +210,7 @@ In addition, it's possible to forward the following parameters to `fzf`:
- `--query <text>`; - `--query <text>`;
- `--filter <text>`; - `--filter <text>`;
- `--header <text>`; - `--header <text>`;
- `--preview <code>`; - `--preview <bash_code>`;
- `--preview-window <text>`. - `--preview-window <text>`.
### Variable dependency ### Variable dependency

View file

@ -1,23 +1,27 @@
use super::{get_column, parse_output_single, Finder}; use super::{parse, Finder};
use crate::display; use crate::display;
use crate::structures::cheat::VariableMap; use crate::structures::cheat::VariableMap;
use crate::structures::finder::{Opts, SuggestionType}; use crate::structures::finder::{Opts, SuggestionType};
use anyhow::Context; use anyhow::Context;
use anyhow::Error; use anyhow::Error;
use std::process; use std::process::{self, Command, Stdio};
use std::process::{Command, Stdio};
#[derive(Debug)] #[derive(Debug)]
pub struct FzfFinder; pub struct FzfFinder;
impl Finder for FzfFinder { impl Finder for FzfFinder {
fn call<F>(&self, opts: Opts, stdin_fn: F) -> Result<(String, Option<VariableMap>), Error> fn call<F>(
&self,
finder_opts: Opts,
stdin_fn: F,
) -> Result<(String, Option<VariableMap>), Error>
where where
F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>, F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>,
{ {
let mut fzf_command = Command::new("fzf"); let mut command = Command::new("fzf");
let opts = finder_opts.clone();
fzf_command.args(&[ command.args(&[
"--preview-window", "--preview-window",
"up:2", "up:2",
"--with-nth", "--with-nth",
@ -31,51 +35,51 @@ impl Finder for FzfFinder {
]); ]);
if opts.autoselect { if opts.autoselect {
fzf_command.arg("--select-1"); command.arg("--select-1");
} }
match opts.suggestion_type { match opts.suggestion_type {
SuggestionType::MultipleSelections => { SuggestionType::MultipleSelections => {
fzf_command.arg("--multi"); command.arg("--multi");
} }
SuggestionType::Disabled => { SuggestionType::Disabled => {
fzf_command.args(&["--print-query", "--no-select-1", "--height", "1"]); command.args(&["--print-query", "--no-select-1", "--height", "1"]);
} }
SuggestionType::SnippetSelection => { SuggestionType::SnippetSelection => {
fzf_command.args(&["--expect", "ctrl-y,enter"]); command.args(&["--expect", "ctrl-y,enter"]);
} }
SuggestionType::SingleRecommendation => { SuggestionType::SingleRecommendation => {
fzf_command.args(&["--print-query", "--expect", "tab,enter"]); command.args(&["--print-query", "--expect", "tab,enter"]);
} }
_ => {} _ => {}
} }
if let Some(p) = opts.preview { if let Some(p) = opts.preview {
fzf_command.args(&["--preview", &p]); command.args(&["--preview", &p]);
} }
if let Some(q) = opts.query { if let Some(q) = opts.query {
fzf_command.args(&["--query", &q]); command.args(&["--query", &q]);
} }
if let Some(f) = opts.filter { if let Some(f) = opts.filter {
fzf_command.args(&["--filter", &f]); command.args(&["--filter", &f]);
} }
if let Some(h) = opts.header { if let Some(h) = opts.header {
fzf_command.args(&["--header", &h]); command.args(&["--header", &h]);
} }
if let Some(p) = opts.prompt { if let Some(p) = opts.prompt {
fzf_command.args(&["--prompt", &p]); command.args(&["--prompt", &p]);
} }
if let Some(pw) = opts.preview_window { if let Some(pw) = opts.preview_window {
fzf_command.args(&["--preview-window", &pw]); command.args(&["--preview-window", &pw]);
} }
if opts.header_lines > 0 { if opts.header_lines > 0 {
fzf_command.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]); command.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]);
} }
if let Some(o) = opts.overrides { if let Some(o) = opts.overrides {
@ -84,14 +88,11 @@ impl Finder for FzfFinder {
.map(|s| s.to_string()) .map(|s| s.to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.for_each(|s| { .for_each(|s| {
fzf_command.arg(s); command.arg(s);
}); });
} }
let child = fzf_command let child = command.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn();
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn();
let mut child = match child { let mut child = match child {
Ok(x) => x, Ok(x) => x,
@ -109,24 +110,7 @@ impl Finder for FzfFinder {
let out = child.wait_with_output().context("Failed to wait for fzf")?; let out = child.wait_with_output().context("Failed to wait for fzf")?;
let text = match out.status.code() { let output = parse(out, finder_opts).context("Unable to get output")?;
Some(0) | Some(1) | Some(2) => { Ok((output, result_map))
String::from_utf8(out.stdout).context("Invalid utf8 received from fzf")?
}
Some(130) => process::exit(130),
_ => {
let err = String::from_utf8(out.stderr)
.unwrap_or_else(|_| "<stderr contains invalid UTF-8>".to_owned());
panic!("External command failed:\n {}", err)
}
};
let out = get_column(
parse_output_single(text, opts.suggestion_type)?,
opts.column,
opts.delimiter.as_deref(),
);
Ok((out, result_map))
} }
} }

View file

@ -9,7 +9,8 @@ use crate::structures::finder::Opts;
use crate::structures::finder::SuggestionType; use crate::structures::finder::SuggestionType;
use anyhow::Context; use anyhow::Context;
use anyhow::Error; use anyhow::Error;
use std::process; use std::process::{self, Output};
use std::process::{Command, Stdio};
#[derive(Debug)] #[derive(Debug)]
pub enum FinderChoice { pub enum FinderChoice {
@ -35,6 +36,22 @@ pub trait Finder {
F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>; F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>;
} }
fn apply_map(text: String, map_fn: Option<String>) -> String {
if let Some(m) = map_fn {
let output = Command::new("bash")
.arg("-c")
.arg(m.as_str())
.arg(text.as_str())
.stderr(Stdio::inherit())
.output()
.expect("Failed to execute map function");
String::from_utf8(output.stdout).expect("Invalid utf8 output for map function")
} else {
text
}
}
fn get_column(text: String, column: Option<u8>, delimiter: Option<&str>) -> String { fn get_column(text: String, column: Option<u8>, delimiter: Option<&str>) -> String {
if let Some(c) = column { if let Some(c) = column {
let mut result = String::from(""); let mut result = String::from("");
@ -96,6 +113,25 @@ fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> Res
}) })
} }
fn parse(out: Output, opts: Opts) -> Result<String, Error> {
let text = match out.status.code() {
Some(0) | Some(1) | Some(2) => {
String::from_utf8(out.stdout).context("Invalid utf8 received from finder")?
}
Some(130) => process::exit(130),
_ => {
let err = String::from_utf8(out.stderr)
.unwrap_or_else(|_| "<stderr contains invalid UTF-8>".to_owned());
panic!("External command failed:\n {}", err)
}
};
let output = parse_output_single(text, opts.suggestion_type)?;
let output = get_column(output, opts.column, opts.delimiter.as_deref());
let output = apply_map(output, opts.map);
Ok(output)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -1,23 +1,27 @@
use super::{get_column, parse_output_single, Finder}; use super::{parse, Finder};
use crate::display; use crate::display;
use crate::structures::cheat::VariableMap; use crate::structures::cheat::VariableMap;
use crate::structures::finder::{Opts, SuggestionType}; use crate::structures::finder::{Opts, SuggestionType};
use anyhow::Context; use anyhow::Context;
use anyhow::Error; use anyhow::Error;
use std::process; use std::process::{self, Command, Stdio};
use std::process::{Command, Stdio};
#[derive(Debug)] #[derive(Debug)]
pub struct SkimFinder; pub struct SkimFinder;
impl Finder for SkimFinder { impl Finder for SkimFinder {
fn call<F>(&self, opts: Opts, stdin_fn: F) -> Result<(String, Option<VariableMap>), Error> fn call<F>(
&self,
finder_opts: Opts,
stdin_fn: F,
) -> Result<(String, Option<VariableMap>), Error>
where where
F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>, F: Fn(&mut process::ChildStdin) -> Result<Option<VariableMap>, Error>,
{ {
let mut skim_command = Command::new("sk"); let mut command = Command::new("sk");
let opts = finder_opts.clone();
skim_command.args(&[ command.args(&[
"--preview-window", "--preview-window",
"up:3", "up:3",
"--with-nth", "--with-nth",
@ -33,51 +37,51 @@ impl Finder for SkimFinder {
if opts.autoselect { if opts.autoselect {
// TODO skim doesn't support this yet // TODO skim doesn't support this yet
// this option does nothing // this option does nothing
skim_command.arg("--select-1"); command.arg("--select-1");
} }
match opts.suggestion_type { match opts.suggestion_type {
SuggestionType::MultipleSelections => { SuggestionType::MultipleSelections => {
skim_command.arg("--multi"); command.arg("--multi");
} }
SuggestionType::Disabled => { SuggestionType::Disabled => {
skim_command.args(&["--print-query", /*"--no-select-1",*/ "--height", "1"]); command.args(&["--print-query", /*"--no-select-1",*/ "--height", "1"]);
} }
SuggestionType::SnippetSelection => { SuggestionType::SnippetSelection => {
skim_command.args(&["--expect", "ctrl-y,enter"]); command.args(&["--expect", "ctrl-y,enter"]);
} }
SuggestionType::SingleRecommendation => { SuggestionType::SingleRecommendation => {
skim_command.args(&["--print-query", "--expect", "tab,enter"]); command.args(&["--print-query", "--expect", "tab,enter"]);
} }
_ => {} _ => {}
} }
if let Some(p) = opts.preview { if let Some(p) = opts.preview {
skim_command.args(&["--preview", &p]); command.args(&["--preview", &p]);
} }
if let Some(q) = opts.query { if let Some(q) = opts.query {
skim_command.args(&["--query", &q]); command.args(&["--query", &q]);
} }
if let Some(f) = opts.filter { if let Some(f) = opts.filter {
skim_command.args(&["--filter", &f]); command.args(&["--filter", &f]);
} }
if let Some(h) = opts.header { if let Some(h) = opts.header {
skim_command.args(&["--header", &h]); command.args(&["--header", &h]);
} }
if let Some(p) = opts.prompt { if let Some(p) = opts.prompt {
skim_command.args(&["--prompt", &p]); command.args(&["--prompt", &p]);
} }
if let Some(pw) = opts.preview_window { if let Some(pw) = opts.preview_window {
skim_command.args(&["--preview-window", &pw]); command.args(&["--preview-window", &pw]);
} }
if opts.header_lines > 0 { if opts.header_lines > 0 {
skim_command.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]); command.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]);
} }
if let Some(o) = opts.overrides { if let Some(o) = opts.overrides {
@ -86,14 +90,11 @@ impl Finder for SkimFinder {
.map(|s| s.to_string()) .map(|s| s.to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.for_each(|s| { .for_each(|s| {
skim_command.arg(s); command.arg(s);
}); });
} }
let child = skim_command let child = command.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn();
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn();
let mut child = match child { let mut child = match child {
Ok(x) => x, Ok(x) => x,
@ -113,24 +114,7 @@ impl Finder for SkimFinder {
.wait_with_output() .wait_with_output()
.context("Failed to wait for skim")?; .context("Failed to wait for skim")?;
let text = match out.status.code() { let output = parse(out, finder_opts).context("Unable to get output")?;
Some(0) | Some(1) | Some(2) => { Ok((output, result_map))
String::from_utf8(out.stdout).context("Invalid utf8 received from skim")?
}
Some(130) => process::exit(130),
_ => {
let err = String::from_utf8(out.stderr)
.unwrap_or_else(|_| "<stderr contains invalid UTF-8>".to_owned());
panic!("External command failed:\n {}", err)
}
};
let out = get_column(
parse_output_single(text, opts.suggestion_type)?,
opts.column,
opts.delimiter.as_deref(),
);
Ok((out, result_map))
} }
} }

View file

@ -20,6 +20,7 @@ fn parse_opts(text: &str) -> Result<FinderOpts, Error> {
let mut multi = false; let mut multi = false;
let mut prevent_extra = false; let mut prevent_extra = false;
let mut opts = FinderOpts::default(); let mut opts = FinderOpts::default();
let parts = shellwords::split(text) let parts = shellwords::split(text)
.map_err(|_| anyhow!("Given options are missing a closing quote"))?; .map_err(|_| anyhow!("Given options are missing a closing quote"))?;
@ -56,6 +57,7 @@ fn parse_opts(text: &str) -> Result<FinderOpts, Error> {
.context("Value for `--column` is invalid u8")?, .context("Value for `--column` is invalid u8")?,
) )
} }
"--map" => opts.map = Some(value.to_string()),
"--delimiter" => opts.delimiter = Some(value.to_string()), "--delimiter" => opts.delimiter = Some(value.to_string()),
"--query" => opts.query = Some(value.to_string()), "--query" => opts.query = Some(value.to_string()),
"--filter" => opts.filter = Some(value.to_string()), "--filter" => opts.filter = Some(value.to_string()),

View file

@ -12,6 +12,7 @@ pub struct Opts {
pub suggestion_type: SuggestionType, pub suggestion_type: SuggestionType,
pub delimiter: Option<String>, pub delimiter: Option<String>,
pub column: Option<u8>, pub column: Option<u8>,
pub map: Option<String>,
} }
impl Default for Opts { impl Default for Opts {
@ -29,6 +30,7 @@ impl Default for Opts {
suggestion_type: SuggestionType::SingleRecommendation, suggestion_type: SuggestionType::SingleRecommendation,
column: None, column: None,
delimiter: None, delimiter: None,
map: None,
} }
} }
} }

View file

@ -35,6 +35,9 @@ echo "<x> <y> <x> <z>"
# with preview # with preview
cat "<file>" cat "<file>"
# with map
echo "<mapped>"
# fzf # fzf
ls / | fzf ls / | fzf
@ -45,7 +48,8 @@ $ 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 ';' $ 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 $ multi_col: ls -la | awk '{print $1, $9}' --- --column 2 --delimiter '\s' --multi
$ langs: echo 'clojure rust javascript' | tr ' ' '\n' --- --multi $ langs: echo 'clojure rust javascript' | tr ' ' '\n' --- --multi
$ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --mult $ mapped: echo 'true false' | tr ' ' '\n' --- --map "[[ $0 == t* ]] && echo 1 || echo 0"
$ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --multi
$ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz'i $ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz'i
$ file: ls . --- --preview 'cat {}' --preview-window '50%' $ file: ls . --- --preview 'cat {}' --preview-window '50%'