From 6c99cc1700fda0c04500ee03b7e5f3ca22c7710c Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 14 Jun 2015 12:25:08 +0900 Subject: 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 {}']" --- src/item.go | 9 +++++++-- src/options.go | 46 +++++++++++++++++++++++++++++++++++++++------- src/options_test.go | 28 +++++++++++++++++++++++----- src/terminal.go | 33 ++++++++++++++++++++++++--------- test/test_go.rb | 21 +++++++++++++++++++++ 5 files changed, 114 insertions(+), 23 deletions(-) diff --git a/src/item.go b/src/item.go index 7c2f94d5..1eeb1802 100644 --- a/src/item.go +++ b/src/item.go @@ -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 { diff --git a/src/options.go b/src/options.go index d969e51d..948857c2 100644 --- a/src/options.go +++ b/src/options.go @@ -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 { diff --git a/src/options_test.go b/src/options_test.go index d3562108..91e3754b 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -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]) - - keymap, toggleSort = parseKeymap(keymap, false, "f1:abort") + 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, execmap, toggleSort = parseKeymap(keymap, execmap, false, "f1:abort") if toggleSort { t.Errorf("toggleSort set") } diff --git a/src/terminal.go b/src/terminal.go index 50d380f8..b0812fe6 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -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 diff --git a/test/test_go.rb b/test/test_go.rb index 5521002d..6a62731e 100644 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -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 -- cgit v1.2.3