m/fzf
1
0
mirror of https://github.com/junegunn/fzf.git synced 2025-11-14 14:23:47 -05:00

Add become(...) action that replaces current fzf process

Close #3159
This commit is contained in:
Junegunn Choi
2023-02-11 20:21:10 +09:00
parent f7447aece1
commit 6ea38b4438
5 changed files with 56 additions and 7 deletions

View File

@@ -4,6 +4,15 @@ CHANGELOG
0.38.0 0.38.0
------ ------
- New actions - 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` - `show-preview`
- `hide-preview` - `hide-preview`
- Bug fixes - Bug fixes

View File

@@ -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-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-kill-word\fR \fIalt-bs\fR
\fBbackward-word\fR \fIalt-b shift-left\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 \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) \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 set, otherwise with \fBsh -c\fR, so in this case make sure that the command is
POSIX-compliant. 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 .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list

View File

@@ -10,6 +10,7 @@ import (
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
"github.com/junegunn/fzf/src/util"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
@@ -921,7 +922,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( 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("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") 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}) actions = append(actions, &action{t: t, a: actionArg})
} }
switch t { switch t {
case actBecome:
if util.IsWindows() {
exit("become action is not supported on Windows")
}
case actUnbind, actRebind: case actUnbind, actRebind:
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit) parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
case actChangePreviewWindow: case actChangePreviewWindow:
@@ -1223,6 +1228,8 @@ func isExecuteAction(str string) actionType {
prefix := actionNameRegexp.FindString(str) prefix := actionNameRegexp.FindString(str)
switch prefix { switch prefix {
case "become":
return actBecome
case "reload": case "reload":
return actReload return actReload
case "reload-sync": case "reload-sync":

View File

@@ -6,6 +6,7 @@ import (
"io/ioutil" "io/ioutil"
"math" "math"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
@@ -387,6 +388,7 @@ const (
actDeselect actDeselect
actUnbind actUnbind
actRebind actRebind
actBecome
) )
type placeholderFlags struct { type placeholderFlags struct {
@@ -2237,7 +2239,7 @@ func (t *Terminal) redraw() {
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string {
line := "" 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 // captureFirstLine is used for transform-{prompt,query} and we don't want to
// return an empty string in those cases // return an empty string in those cases
if !valid && !captureFirstLine { if !valid && !captureFirstLine {
@@ -2297,10 +2299,10 @@ func (t *Terminal) currentItem() *Item {
return nil 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() current := t.currentItem()
slot, plus, query := hasPreviewFlags(template) 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} return current != nil, []*Item{current, current}
} }
@@ -2625,7 +2627,7 @@ func (t *Terminal) Loop() {
refreshPreview := func(command string) { refreshPreview := func(command string) {
if len(command) > 0 && t.canPreview() { if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false, false) _, list := t.buildPlusList(command, false)
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list}) t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
} }
@@ -2860,6 +2862,21 @@ func (t *Terminal) Loop() {
doAction = func(a *action) bool { doAction = func(a *action) bool {
switch a.t { switch a.t {
case actIgnore: 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: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false) t.executeCommand(a.a, false, a.t == actExecuteSilent, false)
case actExecuteMulti: case actExecuteMulti:
@@ -2881,7 +2898,7 @@ func (t *Terminal) Loop() {
t.activePreviewOpts.Toggle() t.activePreviewOpts.Toggle()
updatePreviewWindow(false) updatePreviewWindow(false)
if t.canPreview() { if t.canPreview() {
valid, list := t.buildPlusList(t.previewOpts.command, false, false) valid, list := t.buildPlusList(t.previewOpts.command, false)
if valid { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
@@ -3360,7 +3377,7 @@ func (t *Terminal) Loop() {
case actReload, actReloadSync: case actReload, actReloadSync:
t.failed = nil t.failed = nil
valid, list := t.buildPlusList(a.a, false, false) valid, list := t.buildPlusList(a.a, false)
if !valid { if !valid {
// We run the command even when there's no match // We run the command even when there's no match
// 1. If the template doesn't have any slots // 1. If the template doesn't have any slots

View File

@@ -2643,6 +2643,13 @@ class TestGoFZF < TestBase
tmux.send_keys :Space tmux.send_keys :Space
tmux.until { |lines| assert_includes lines, '/1/1/' } tmux.until { |lines| assert_includes lines, '/1/1/' }
end 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 end
module TestShell module TestShell