summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2015-06-14 12:25:08 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2015-06-14 12:25:08 +0900
commit6c99cc1700fda0c04500ee03b7e5f3ca22c7710c (patch)
treea6a671d49f562937a50d15c7439fe7682e7033d5
parentfe5b190a7d8612bb90de272d26470bd02dc76a64 (diff)
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 {}']"
-rw-r--r--src/item.go9
-rw-r--r--src/options.go46
-rw-r--r--src/options_test.go28
-rw-r--r--src/terminal.go33
-rw-r--r--test/test_go.rb21
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