From 6ea38b44384e7a09a3863465dc3cc7b93cd7e781 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 11 Feb 2023 20:21:10 +0900 Subject: [PATCH] Add become(...) action that replaces current fzf process Close #3159 --- CHANGELOG.md | 9 +++++++++ man/man1/fzf.1 | 9 +++++++++ src/options.go | 9 ++++++++- src/terminal.go | 29 +++++++++++++++++++++++------ test/test_go.rb | 7 +++++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc11c49..4aa2be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ CHANGELOG 0.38.0 ------ - New actions + - `become(...)` - Replace the current fzf process with the specified + command using `execve(2)` system call. This action enables a simpler + alternative to using `--expect` and checking the output in the wrapping + script. + ```sh + # Open selected files in different editors + fzf --multi --bind 'enter:become($EDITOR {+}),ctrl-n:become(nano {+})' + ``` + - This action is not supported on Windows - `show-preview` - `hide-preview` - Bug fixes diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 3ce32be..873e348 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -999,6 +999,7 @@ A key or an event can be bound to one or more of the following actions. \fBbackward-delete-char/eof\fR (same as \fBbackward-delete-char\fR except aborts fzf if query is empty) \fBbackward-kill-word\fR \fIalt-bs\fR \fBbackward-word\fR \fIalt-b shift-left\fR + \fBbecome(...)\fR (replace fzf process with the specified command; see below for the details) \fBbeginning-of-line\fR \fIctrl-a home\fR \fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) @@ -1143,6 +1144,14 @@ On *nix systems, fzf runs the command with \fB$SHELL -c\fR if \fBSHELL\fR is set, otherwise with \fBsh -c\fR, so in this case make sure that the command is POSIX-compliant. +\fBbecome(...)\fR action is similar to \fBexecute(...)\fR, but it replaces the +current fzf process with the specifed command using \fBexecve(2)\fR system +call. + + \fBfzf --bind "enter:become(vim {})"\fR + +\fBbecome(...)\fR is not supported on Windows. + .SS RELOAD INPUT \fBreload(...)\fR action is used to dynamically update the input list diff --git a/src/options.go b/src/options.go index e30ba2c..9be2910 100644 --- a/src/options.go +++ b/src/options.go @@ -10,6 +10,7 @@ import ( "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" + "github.com/junegunn/fzf/src/util" "github.com/mattn/go-runewidth" "github.com/mattn/go-shellwords" @@ -921,7 +922,7 @@ const ( func init() { executeRegexp = regexp.MustCompile( - `(?si)[:+](execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) + `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) splitRegexp = regexp.MustCompile("[,:]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") } @@ -1171,6 +1172,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA actions = append(actions, &action{t: t, a: actionArg}) } switch t { + case actBecome: + if util.IsWindows() { + exit("become action is not supported on Windows") + } case actUnbind, actRebind: parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit) case actChangePreviewWindow: @@ -1223,6 +1228,8 @@ func isExecuteAction(str string) actionType { prefix := actionNameRegexp.FindString(str) switch prefix { + case "become": + return actBecome case "reload": return actReload case "reload-sync": diff --git a/src/terminal.go b/src/terminal.go index 0dc711a..13968a3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "math" "os" + "os/exec" "os/signal" "regexp" "sort" @@ -387,6 +388,7 @@ const ( actDeselect actUnbind actRebind + actBecome ) type placeholderFlags struct { @@ -2237,7 +2239,7 @@ func (t *Terminal) redraw() { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string { line := "" - valid, list := t.buildPlusList(template, forcePlus, false) + valid, list := t.buildPlusList(template, forcePlus) // captureFirstLine is used for transform-{prompt,query} and we don't want to // return an empty string in those cases if !valid && !captureFirstLine { @@ -2297,10 +2299,10 @@ func (t *Terminal) currentItem() *Item { return nil } -func (t *Terminal) buildPlusList(template string, forcePlus bool, forceEvaluation bool) (bool, []*Item) { +func (t *Terminal) buildPlusList(template string, forcePlus bool) (bool, []*Item) { current := t.currentItem() slot, plus, query := hasPreviewFlags(template) - if !forceEvaluation && !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) { + if !(!slot || query || (forcePlus || plus) && len(t.selected) > 0) { return current != nil, []*Item{current, current} } @@ -2625,7 +2627,7 @@ func (t *Terminal) Loop() { refreshPreview := func(command string) { if len(command) > 0 && t.canPreview() { - _, list := t.buildPlusList(command, false, false) + _, list := t.buildPlusList(command, false) t.cancelPreview() t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list}) } @@ -2860,6 +2862,21 @@ func (t *Terminal) Loop() { doAction = func(a *action) bool { switch a.t { case actIgnore: + case actBecome: + _, list := t.buildPlusList(a.a, false) + command := t.replacePlaceholder(a.a, false, string(t.input), list) + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = "sh" + } + shellPath, err := exec.LookPath(shell) + if err == nil { + t.tui.Close() + if t.history != nil { + t.history.append(string(t.input)) + } + syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ()) + } case actExecute, actExecuteSilent: t.executeCommand(a.a, false, a.t == actExecuteSilent, false) case actExecuteMulti: @@ -2881,7 +2898,7 @@ func (t *Terminal) Loop() { t.activePreviewOpts.Toggle() updatePreviewWindow(false) if t.canPreview() { - valid, list := t.buildPlusList(t.previewOpts.command, false, false) + valid, list := t.buildPlusList(t.previewOpts.command, false) if valid { t.cancelPreview() t.previewBox.Set(reqPreviewEnqueue, @@ -3360,7 +3377,7 @@ func (t *Terminal) Loop() { case actReload, actReloadSync: t.failed = nil - valid, list := t.buildPlusList(a.a, false, false) + valid, list := t.buildPlusList(a.a, false) if !valid { // We run the command even when there's no match // 1. If the template doesn't have any slots diff --git a/test/test_go.rb b/test/test_go.rb index 1816191..bcc18dc 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2643,6 +2643,13 @@ class TestGoFZF < TestBase tmux.send_keys :Space tmux.until { |lines| assert_includes lines, '/1/1/' } end + + def test_become + tmux.send_keys "seq 10 | #{FZF} --bind 'enter:become:seq 100 | #{FZF}'", :Enter + tmux.until { |lines| assert_equal 10, lines.item_count } + tmux.send_keys :Enter + tmux.until { |lines| assert_equal 100, lines.item_count } + end end module TestShell