mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-12-23 11:29:01 +00:00
Add bind action for executing arbitrary command (#265)
e.g. fzf --bind "ctrl-m:execute(less {})" fzf --bind "ctrl-t:execute[tmux new-window -d 'vim {}']"
This commit is contained in:
parent
fe5b190a7d
commit
6c99cc1700
@ -86,10 +86,15 @@ func (i *Item) Rank(cache bool) Rank {
|
|||||||
|
|
||||||
// AsString returns the original string
|
// AsString returns the original string
|
||||||
func (i *Item) AsString() string {
|
func (i *Item) AsString() string {
|
||||||
|
return *i.StringPtr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringPtr returns the pointer to the original string
|
||||||
|
func (i *Item) StringPtr() *string {
|
||||||
if i.origText != nil {
|
if i.origText != nil {
|
||||||
return *i.origText
|
return i.origText
|
||||||
}
|
}
|
||||||
return *i.text
|
return i.text
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
|
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {
|
||||||
|
@ -117,6 +117,7 @@ type Options struct {
|
|||||||
ToggleSort bool
|
ToggleSort bool
|
||||||
Expect []int
|
Expect []int
|
||||||
Keymap map[int]actionType
|
Keymap map[int]actionType
|
||||||
|
Execmap map[int]string
|
||||||
PrintQuery bool
|
PrintQuery bool
|
||||||
ReadZero bool
|
ReadZero bool
|
||||||
Sync bool
|
Sync bool
|
||||||
@ -157,6 +158,7 @@ func defaultOptions() *Options {
|
|||||||
ToggleSort: false,
|
ToggleSort: false,
|
||||||
Expect: []int{},
|
Expect: []int{},
|
||||||
Keymap: defaultKeymap(),
|
Keymap: defaultKeymap(),
|
||||||
|
Execmap: make(map[int]string),
|
||||||
PrintQuery: false,
|
PrintQuery: false,
|
||||||
ReadZero: false,
|
ReadZero: false,
|
||||||
Sync: false,
|
Sync: false,
|
||||||
@ -375,12 +377,22 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
|
|||||||
return theme
|
return theme
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) {
|
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
|
||||||
for _, pairStr := range strings.Split(str, ",") {
|
rx := regexp.MustCompile(
|
||||||
|
":execute(\\([^)]*\\)|\\[[^\\]]*\\]|/[^/]*/|:[^:]*:|;[^;]*;|@[^@]*@|~[^~]*~|%[^%]*%|\\?[^?]*\\?)")
|
||||||
|
masked := rx.ReplaceAllStringFunc(str, func(src string) string {
|
||||||
|
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
|
||||||
|
})
|
||||||
|
|
||||||
|
idx := 0
|
||||||
|
for _, pairStr := range strings.Split(masked, ",") {
|
||||||
|
pairStr = str[idx : idx+len(pairStr)]
|
||||||
|
idx += len(pairStr) + 1
|
||||||
|
|
||||||
fail := func() {
|
fail := func() {
|
||||||
errorExit("invalid key binding: " + pairStr)
|
errorExit("invalid key binding: " + pairStr)
|
||||||
}
|
}
|
||||||
pair := strings.Split(pairStr, ":")
|
pair := strings.SplitN(pairStr, ":", 2)
|
||||||
if len(pair) != 2 {
|
if len(pair) != 2 {
|
||||||
fail()
|
fail()
|
||||||
}
|
}
|
||||||
@ -455,10 +467,28 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
|
|||||||
keymap[key] = actToggleSort
|
keymap[key] = actToggleSort
|
||||||
toggleSort = true
|
toggleSort = true
|
||||||
default:
|
default:
|
||||||
|
if isExecuteAction(act) {
|
||||||
|
keymap[key] = actExecute
|
||||||
|
execmap[key] = pair[1][8 : len(act)-1]
|
||||||
|
} else {
|
||||||
errorExit("unknown action: " + act)
|
errorExit("unknown action: " + act)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return keymap, toggleSort
|
}
|
||||||
|
return keymap, execmap, toggleSort
|
||||||
|
}
|
||||||
|
|
||||||
|
func isExecuteAction(str string) bool {
|
||||||
|
if !strings.HasPrefix(str, "execute") || len(str) < 9 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
b := str[7]
|
||||||
|
e := str[len(str)-1]
|
||||||
|
if b == e && strings.ContainsAny(string(b), "/:;@~%?") ||
|
||||||
|
b == '(' && e == ')' || b == '[' && e == ']' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
|
func checkToggleSort(keymap map[int]actionType, str string) map[int]actionType {
|
||||||
@ -515,7 +545,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
case "--tiebreak":
|
case "--tiebreak":
|
||||||
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
|
||||||
case "--bind":
|
case "--bind":
|
||||||
keymap, opts.ToggleSort = parseKeymap(keymap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
|
keymap, opts.Execmap, opts.ToggleSort =
|
||||||
|
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, nextString(allArgs, &i, "bind expression required"))
|
||||||
case "--color":
|
case "--color":
|
||||||
spec := optionalNextString(allArgs, &i)
|
spec := optionalNextString(allArgs, &i)
|
||||||
if len(spec) == 0 {
|
if len(spec) == 0 {
|
||||||
@ -629,7 +660,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
|||||||
} else if match, value := optString(arg, "--color="); match {
|
} else if match, value := optString(arg, "--color="); match {
|
||||||
opts.Theme = parseTheme(opts.Theme, value)
|
opts.Theme = parseTheme(opts.Theme, value)
|
||||||
} else if match, value := optString(arg, "--bind="); match {
|
} else if match, value := optString(arg, "--bind="); match {
|
||||||
keymap, opts.ToggleSort = parseKeymap(keymap, opts.ToggleSort, value)
|
keymap, opts.Execmap, opts.ToggleSort =
|
||||||
|
parseKeymap(keymap, opts.Execmap, opts.ToggleSort, value)
|
||||||
} else if match, value := optString(arg, "--history="); match {
|
} else if match, value := optString(arg, "--history="); match {
|
||||||
setHistory(value)
|
setHistory(value)
|
||||||
} else if match, value := optString(arg, "--history-max="); match {
|
} else if match, value := optString(arg, "--history-max="); match {
|
||||||
|
@ -136,11 +136,19 @@ func TestBind(t *testing.T) {
|
|||||||
t.Errorf("%d != %d", action, expected)
|
t.Errorf("%d != %d", action, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
checkString := func(action string, expected string) {
|
||||||
|
if action != expected {
|
||||||
|
t.Errorf("%d != %d", action, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
keymap := defaultKeymap()
|
keymap := defaultKeymap()
|
||||||
|
execmap := make(map[int]string)
|
||||||
check(actBeginningOfLine, keymap[curses.CtrlA])
|
check(actBeginningOfLine, keymap[curses.CtrlA])
|
||||||
keymap, toggleSort :=
|
keymap, execmap, toggleSort :=
|
||||||
parseKeymap(keymap, false,
|
parseKeymap(keymap, execmap, false,
|
||||||
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down")
|
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+
|
||||||
|
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute:less {}:,"+
|
||||||
|
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};")
|
||||||
if !toggleSort {
|
if !toggleSort {
|
||||||
t.Errorf("toggleSort not set")
|
t.Errorf("toggleSort not set")
|
||||||
}
|
}
|
||||||
@ -148,8 +156,18 @@ func TestBind(t *testing.T) {
|
|||||||
check(actToggleSort, keymap[curses.CtrlB])
|
check(actToggleSort, keymap[curses.CtrlB])
|
||||||
check(actPageUp, keymap[curses.AltZ+'c'])
|
check(actPageUp, keymap[curses.AltZ+'c'])
|
||||||
check(actPageDown, keymap[curses.AltZ])
|
check(actPageDown, keymap[curses.AltZ])
|
||||||
|
check(actExecute, keymap[curses.F1])
|
||||||
|
check(actExecute, keymap[curses.F2])
|
||||||
|
check(actExecute, keymap[curses.F3])
|
||||||
|
check(actExecute, keymap[curses.F4])
|
||||||
|
checkString("ls {}", execmap[curses.F1])
|
||||||
|
checkString("echo {}, {}, {}", execmap[curses.F2])
|
||||||
|
checkString("echo '({})'", execmap[curses.F3])
|
||||||
|
checkString("less {}", execmap[curses.F4])
|
||||||
|
checkString("echo (,),[,],/,:,;,%,{}", execmap[curses.AltA])
|
||||||
|
checkString("echo (,),[,],/,:,@,%,{}", execmap[curses.AltB])
|
||||||
|
|
||||||
keymap, toggleSort = parseKeymap(keymap, false, "f1:abort")
|
keymap, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort")
|
||||||
if toggleSort {
|
if toggleSort {
|
||||||
t.Errorf("toggleSort set")
|
t.Errorf("toggleSort set")
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@ -34,6 +35,7 @@ type Terminal struct {
|
|||||||
toggleSort bool
|
toggleSort bool
|
||||||
expect []int
|
expect []int
|
||||||
keymap map[int]actionType
|
keymap map[int]actionType
|
||||||
|
execmap map[int]string
|
||||||
pressed int
|
pressed int
|
||||||
printQuery bool
|
printQuery bool
|
||||||
history *History
|
history *History
|
||||||
@ -119,6 +121,7 @@ const (
|
|||||||
actToggleSort
|
actToggleSort
|
||||||
actPreviousHistory
|
actPreviousHistory
|
||||||
actNextHistory
|
actNextHistory
|
||||||
|
actExecute
|
||||||
)
|
)
|
||||||
|
|
||||||
func defaultKeymap() map[int]actionType {
|
func defaultKeymap() map[int]actionType {
|
||||||
@ -187,6 +190,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
|||||||
toggleSort: opts.ToggleSort,
|
toggleSort: opts.ToggleSort,
|
||||||
expect: opts.Expect,
|
expect: opts.Expect,
|
||||||
keymap: opts.Keymap,
|
keymap: opts.Keymap,
|
||||||
|
execmap: opts.Execmap,
|
||||||
pressed: 0,
|
pressed: 0,
|
||||||
printQuery: opts.PrintQuery,
|
printQuery: opts.PrintQuery,
|
||||||
history: opts.History,
|
history: opts.History,
|
||||||
@ -587,6 +591,17 @@ func keyMatch(key int, event C.Event) bool {
|
|||||||
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
|
return event.Type == key || event.Type == C.Rune && int(event.Char) == key-C.AltZ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func executeCommand(template string, current string) {
|
||||||
|
command := strings.Replace(template, "{}", fmt.Sprintf("%q", current), -1)
|
||||||
|
cmd := exec.Command("sh", "-c", command)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
C.Endwin()
|
||||||
|
cmd.Run()
|
||||||
|
C.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
// Loop is called to start Terminal I/O
|
// Loop is called to start Terminal I/O
|
||||||
func (t *Terminal) Loop() {
|
func (t *Terminal) Loop() {
|
||||||
<-t.startChan
|
<-t.startChan
|
||||||
@ -677,13 +692,7 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
selectItem := func(item *Item) bool {
|
selectItem := func(item *Item) bool {
|
||||||
if _, found := t.selected[item.index]; !found {
|
if _, found := t.selected[item.index]; !found {
|
||||||
var strptr *string
|
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()}
|
||||||
if item.origText != nil {
|
|
||||||
strptr = item.origText
|
|
||||||
} else {
|
|
||||||
strptr = item.text
|
|
||||||
}
|
|
||||||
t.selected[item.index] = selectedItem{time.Now(), strptr}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -709,14 +718,20 @@ func (t *Terminal) Loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action := t.keymap[event.Type]
|
action := t.keymap[event.Type]
|
||||||
|
mapkey := event.Type
|
||||||
if event.Type == C.Rune {
|
if event.Type == C.Rune {
|
||||||
code := int(event.Char) + int(C.AltZ)
|
mapkey = int(event.Char) + int(C.AltZ)
|
||||||
if act, prs := t.keymap[code]; prs {
|
if act, prs := t.keymap[mapkey]; prs {
|
||||||
action = act
|
action = act
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch action {
|
switch action {
|
||||||
case actIgnore:
|
case actIgnore:
|
||||||
|
case actExecute:
|
||||||
|
if t.cy >= 0 && t.cy < t.merger.Length() {
|
||||||
|
item := t.merger.Get(t.cy)
|
||||||
|
executeCommand(t.execmap[mapkey], item.AsString())
|
||||||
|
}
|
||||||
case actInvalid:
|
case actInvalid:
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
continue
|
continue
|
||||||
|
@ -594,6 +594,27 @@ class TestGoFZF < TestBase
|
|||||||
File.unlink history_file
|
File.unlink history_file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_execute
|
||||||
|
output = '/tmp/fzf-test-execute'
|
||||||
|
opts = %[--bind \\"alt-a:execute(echo '[{}]' >> #{output}),alt-b:execute[echo '({}), ({})' >> #{output}],C:execute:echo '({}), [{}], @{}@' >> #{output}:\\"]
|
||||||
|
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
|
||||||
|
tmux.until { |lines| lines[-2].include? '100/100' }
|
||||||
|
tmux.send_keys :Escape, :a, :Escape, :a
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.send_keys :Escape, :b, :Escape, :b
|
||||||
|
tmux.send_keys :Up
|
||||||
|
tmux.send_keys :C
|
||||||
|
tmux.send_keys 'foobar'
|
||||||
|
tmux.until { |lines| lines[-2].include? '0/100' }
|
||||||
|
tmux.send_keys :Escape, :a, :Escape, :b, :Escape, :c
|
||||||
|
tmux.send_keys :Enter
|
||||||
|
readonce
|
||||||
|
assert_equal ['["1"]', '["1"]', '("2"), ("2")', '("2"), ("2")', '("3"), ["3"], @"3"@'],
|
||||||
|
File.readlines(output).map(&:chomp)
|
||||||
|
ensure
|
||||||
|
File.unlink output rescue nil
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def writelines path, lines
|
def writelines path, lines
|
||||||
File.unlink path while File.exists? path
|
File.unlink path while File.exists? path
|
||||||
|
Loading…
Reference in New Issue
Block a user