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:
Junegunn Choi 2015-06-14 12:25:08 +09:00
parent fe5b190a7d
commit 6c99cc1700
5 changed files with 113 additions and 22 deletions

View File

@ -86,10 +86,15 @@ func (i *Item) Rank(cache bool) Rank {
// AsString returns the original 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 {
return *i.origText
return i.origText
}
return *i.text
return i.text
}
func (item *Item) colorOffsets(color int, bold bool, current bool) []colorOffset {

View File

@ -117,6 +117,7 @@ type Options struct {
ToggleSort bool
Expect []int
Keymap map[int]actionType
Execmap map[int]string
PrintQuery bool
ReadZero bool
Sync bool
@ -157,6 +158,7 @@ func defaultOptions() *Options {
ToggleSort: false,
Expect: []int{},
Keymap: defaultKeymap(),
Execmap: make(map[int]string),
PrintQuery: false,
ReadZero: false,
Sync: false,
@ -375,12 +377,22 @@ func parseTheme(defaultTheme *curses.ColorTheme, str string) *curses.ColorTheme
return theme
}
func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[int]actionType, bool) {
for _, pairStr := range strings.Split(str, ",") {
func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort bool, str string) (map[int]actionType, map[int]string, bool) {
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() {
errorExit("invalid key binding: " + pairStr)
}
pair := strings.Split(pairStr, ":")
pair := strings.SplitN(pairStr, ":", 2)
if len(pair) != 2 {
fail()
}
@ -455,10 +467,28 @@ func parseKeymap(keymap map[int]actionType, toggleSort bool, str string) (map[in
keymap[key] = actToggleSort
toggleSort = true
default:
errorExit("unknown action: " + act)
if isExecuteAction(act) {
keymap[key] = actExecute
execmap[key] = pair[1][8 : len(act)-1]
} else {
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 {
@ -515,7 +545,8 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak":
opts.Tiebreak = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
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":
spec := optionalNextString(allArgs, &i)
if len(spec) == 0 {
@ -629,7 +660,8 @@ func parseOptions(opts *Options, allArgs []string) {
} else if match, value := optString(arg, "--color="); match {
opts.Theme = parseTheme(opts.Theme, value)
} 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 {
setHistory(value)
} else if match, value := optString(arg, "--history-max="); match {

View File

@ -136,11 +136,19 @@ func TestBind(t *testing.T) {
t.Errorf("%d != %d", action, expected)
}
}
checkString := func(action string, expected string) {
if action != expected {
t.Errorf("%d != %d", action, expected)
}
}
keymap := defaultKeymap()
execmap := make(map[int]string)
check(actBeginningOfLine, keymap[curses.CtrlA])
keymap, toggleSort :=
parseKeymap(keymap, false,
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down")
keymap, execmap, toggleSort :=
parseKeymap(keymap, execmap, false,
"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 {
t.Errorf("toggleSort not set")
}
@ -148,8 +156,18 @@ func TestBind(t *testing.T) {
check(actToggleSort, keymap[curses.CtrlB])
check(actPageUp, keymap[curses.AltZ+'c'])
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 {
t.Errorf("toggleSort set")
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"os"
"os/exec"
"os/signal"
"regexp"
"sort"
@ -34,6 +35,7 @@ type Terminal struct {
toggleSort bool
expect []int
keymap map[int]actionType
execmap map[int]string
pressed int
printQuery bool
history *History
@ -119,6 +121,7 @@ const (
actToggleSort
actPreviousHistory
actNextHistory
actExecute
)
func defaultKeymap() map[int]actionType {
@ -187,6 +190,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
toggleSort: opts.ToggleSort,
expect: opts.Expect,
keymap: opts.Keymap,
execmap: opts.Execmap,
pressed: 0,
printQuery: opts.PrintQuery,
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
}
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
func (t *Terminal) Loop() {
<-t.startChan
@ -677,13 +692,7 @@ func (t *Terminal) Loop() {
}
selectItem := func(item *Item) bool {
if _, found := t.selected[item.index]; !found {
var strptr *string
if item.origText != nil {
strptr = item.origText
} else {
strptr = item.text
}
t.selected[item.index] = selectedItem{time.Now(), strptr}
t.selected[item.index] = selectedItem{time.Now(), item.StringPtr()}
return true
}
return false
@ -709,14 +718,20 @@ func (t *Terminal) Loop() {
}
action := t.keymap[event.Type]
mapkey := event.Type
if event.Type == C.Rune {
code := int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[code]; prs {
mapkey = int(event.Char) + int(C.AltZ)
if act, prs := t.keymap[mapkey]; prs {
action = act
}
}
switch action {
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:
t.mutex.Unlock()
continue

View File

@ -594,6 +594,27 @@ class TestGoFZF < TestBase
File.unlink history_file
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
def writelines path, lines
File.unlink path while File.exists? path