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
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+'
$ mapped: echo 'false true' | tr ' ' '\n' --- --map "[[ $0 == t* ]] && echo 1 || echo 0"
```
The supported parameters are:
- `--prevent-extra` *(experimental)*: limits the user to select one of the suggestions;
- `--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`:
- `--multi`;
@ -205,7 +210,7 @@ In addition, it's possible to forward the following parameters to `fzf`:
- `--query <text>`;
- `--filter <text>`;
- `--header <text>`;
- `--preview <code>`;
- `--preview <bash_code>`;
- `--preview-window <text>`.
### 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::structures::cheat::VariableMap;
use crate::structures::finder::{Opts, SuggestionType};
use anyhow::Context;
use anyhow::Error;
use std::process;
use std::process::{Command, Stdio};
use std::process::{self, Command, Stdio};
#[derive(Debug)]
pub struct 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
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",
"up:2",
"--with-nth",
@ -31,51 +35,51 @@ impl Finder for FzfFinder {
]);
if opts.autoselect {
fzf_command.arg("--select-1");
command.arg("--select-1");
}
match opts.suggestion_type {
SuggestionType::MultipleSelections => {
fzf_command.arg("--multi");
command.arg("--multi");
}
SuggestionType::Disabled => {
fzf_command.args(&["--print-query", "--no-select-1", "--height", "1"]);
command.args(&["--print-query", "--no-select-1", "--height", "1"]);
}
SuggestionType::SnippetSelection => {
fzf_command.args(&["--expect", "ctrl-y,enter"]);
command.args(&["--expect", "ctrl-y,enter"]);
}
SuggestionType::SingleRecommendation => {
fzf_command.args(&["--print-query", "--expect", "tab,enter"]);
command.args(&["--print-query", "--expect", "tab,enter"]);
}
_ => {}
}
if let Some(p) = opts.preview {
fzf_command.args(&["--preview", &p]);
command.args(&["--preview", &p]);
}
if let Some(q) = opts.query {
fzf_command.args(&["--query", &q]);
command.args(&["--query", &q]);
}
if let Some(f) = opts.filter {
fzf_command.args(&["--filter", &f]);
command.args(&["--filter", &f]);
}
if let Some(h) = opts.header {
fzf_command.args(&["--header", &h]);
command.args(&["--header", &h]);
}
if let Some(p) = opts.prompt {
fzf_command.args(&["--prompt", &p]);
command.args(&["--prompt", &p]);
}
if let Some(pw) = opts.preview_window {
fzf_command.args(&["--preview-window", &pw]);
command.args(&["--preview-window", &pw]);
}
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 {
@ -84,14 +88,11 @@ impl Finder for FzfFinder {
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.for_each(|s| {
fzf_command.arg(s);
command.arg(s);
});
}
let child = fzf_command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn();
let child = command.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn();
let mut child = match child {
Ok(x) => x,
@ -109,24 +110,7 @@ impl Finder for FzfFinder {
let out = child.wait_with_output().context("Failed to wait for fzf")?;
let text = match out.status.code() {
Some(0) | Some(1) | Some(2) => {
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))
let output = parse(out, finder_opts).context("Unable to get output")?;
Ok((output, result_map))
}
}

View file

@ -9,7 +9,8 @@ use crate::structures::finder::Opts;
use crate::structures::finder::SuggestionType;
use anyhow::Context;
use anyhow::Error;
use std::process;
use std::process::{self, Output};
use std::process::{Command, Stdio};
#[derive(Debug)]
pub enum FinderChoice {
@ -35,6 +36,22 @@ pub trait Finder {
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 {
if let Some(c) = column {
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)]
mod tests {
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::structures::cheat::VariableMap;
use crate::structures::finder::{Opts, SuggestionType};
use anyhow::Context;
use anyhow::Error;
use std::process;
use std::process::{Command, Stdio};
use std::process::{self, Command, Stdio};
#[derive(Debug)]
pub struct 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
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",
"up:3",
"--with-nth",
@ -33,51 +37,51 @@ impl Finder for SkimFinder {
if opts.autoselect {
// TODO skim doesn't support this yet
// this option does nothing
skim_command.arg("--select-1");
command.arg("--select-1");
}
match opts.suggestion_type {
SuggestionType::MultipleSelections => {
skim_command.arg("--multi");
command.arg("--multi");
}
SuggestionType::Disabled => {
skim_command.args(&["--print-query", /*"--no-select-1",*/ "--height", "1"]);
command.args(&["--print-query", /*"--no-select-1",*/ "--height", "1"]);
}
SuggestionType::SnippetSelection => {
skim_command.args(&["--expect", "ctrl-y,enter"]);
command.args(&["--expect", "ctrl-y,enter"]);
}
SuggestionType::SingleRecommendation => {
skim_command.args(&["--print-query", "--expect", "tab,enter"]);
command.args(&["--print-query", "--expect", "tab,enter"]);
}
_ => {}
}
if let Some(p) = opts.preview {
skim_command.args(&["--preview", &p]);
command.args(&["--preview", &p]);
}
if let Some(q) = opts.query {
skim_command.args(&["--query", &q]);
command.args(&["--query", &q]);
}
if let Some(f) = opts.filter {
skim_command.args(&["--filter", &f]);
command.args(&["--filter", &f]);
}
if let Some(h) = opts.header {
skim_command.args(&["--header", &h]);
command.args(&["--header", &h]);
}
if let Some(p) = opts.prompt {
skim_command.args(&["--prompt", &p]);
command.args(&["--prompt", &p]);
}
if let Some(pw) = opts.preview_window {
skim_command.args(&["--preview-window", &pw]);
command.args(&["--preview-window", &pw]);
}
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 {
@ -86,14 +90,11 @@ impl Finder for SkimFinder {
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.for_each(|s| {
skim_command.arg(s);
command.arg(s);
});
}
let child = skim_command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn();
let child = command.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn();
let mut child = match child {
Ok(x) => x,
@ -113,24 +114,7 @@ impl Finder for SkimFinder {
.wait_with_output()
.context("Failed to wait for skim")?;
let text = match out.status.code() {
Some(0) | Some(1) | Some(2) => {
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))
let output = parse(out, finder_opts).context("Unable to get output")?;
Ok((output, result_map))
}
}

View file

@ -20,6 +20,7 @@ fn parse_opts(text: &str) -> Result<FinderOpts, Error> {
let mut multi = false;
let mut prevent_extra = false;
let mut opts = FinderOpts::default();
let parts = shellwords::split(text)
.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")?,
)
}
"--map" => opts.map = Some(value.to_string()),
"--delimiter" => opts.delimiter = Some(value.to_string()),
"--query" => opts.query = 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 delimiter: Option<String>,
pub column: Option<u8>,
pub map: Option<String>,
}
impl Default for Opts {
@ -29,6 +30,7 @@ impl Default for Opts {
suggestion_type: SuggestionType::SingleRecommendation,
column: None,
delimiter: None,
map: None,
}
}
}

View file

@ -35,6 +35,9 @@ echo "<x> <y> <x> <z>"
# with preview
cat "<file>"
# with map
echo "<mapped>"
# 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 ';'
$ 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' --- --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
$ file: ls . --- --preview 'cat {}' --preview-window '50%'