Skip to content
Snippets Groups Projects
Commit a89d8995 authored by Junegunn Choi's avatar Junegunn Choi
Browse files

Add execute-multi action

Close #413
parent dbc854d5
No related branches found
No related tags found
No related merge requests found
......@@ -223,6 +223,7 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBdown\fR \fIctrl-j ctrl-n down\fR
\fBend-of-line\fR \fIctrl-e end\fR
\fBexecute(...)\fR (see below for the details)
\fBexecute-multi(...)\fR (see below for the details)
\fBforward-char\fR \fIctrl-f right\fR
\fBforward-word\fR \fIalt-f shift-right\fR
\fBignore\fR
......@@ -276,6 +277,12 @@ This is the special form that frees you from parse errors as it does not expect
the closing character. The catch is that it should be the last one in the
comma-separated list.
.RE
\fBexecute-multi(...)\fR is an alternative action that executes the command
with the selected entries when multi-select is enabled (\fB--multi\fR). With
this action, \fB{}\fR is replaced with the double-quoted strings of the
selected entries separated by spaces.
.RE
.TP
.BI "--history=" "HISTORY_FILE"
......
......@@ -466,10 +466,13 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
// Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile(
"(?s):execute:.*|:execute(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
}
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
return ":execute(" + strings.Repeat(" ", len(src)-10) + ")"
if strings.HasPrefix(src, ":execute-multi") {
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")"
}
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")"
})
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
......@@ -565,11 +568,18 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
toggleSort = true
default:
if isExecuteAction(actLower) {
keymap[key] = actExecute
if act[7] == ':' {
execmap[key] = act[8:]
var offset int
if strings.HasPrefix(actLower, "execute-multi") {
keymap[key] = actExecuteMulti
offset = len("execute-multi")
} else {
keymap[key] = actExecute
offset = len("execute")
}
if act[offset] == ':' {
execmap[key] = act[offset+1:]
} else {
execmap[key] = act[8 : len(act)-1]
execmap[key] = act[offset+1 : len(act)-1]
}
} else {
errorExit("unknown action: " + act)
......@@ -580,10 +590,16 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, toggleSort b
}
func isExecuteAction(str string) bool {
if !strings.HasPrefix(str, "execute") || len(str) < 9 {
if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") {
return false
}
b := str[7]
b := str[len("execute")]
if strings.HasPrefix(str, "execute-multi") {
if len(str) < len("execute-multi()") {
return false
}
b = str[len("execute-multi")]
}
e := str[len(str)-1]
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
......
......@@ -132,6 +132,7 @@ const (
actPreviousHistory
actNextHistory
actExecute
actExecuteMulti
)
func defaultKeymap() map[int]actionType {
......@@ -305,18 +306,22 @@ func (t *Terminal) output() bool {
found = true
}
} else {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
for _, sel := range sels {
for _, sel := range t.sortSelected() {
fmt.Println(*sel.text)
}
}
return found
}
func (t *Terminal) sortSelected() []selectedItem {
sels := make([]selectedItem, 0, len(t.selected))
for _, sel := range t.selected {
sels = append(sels, sel)
}
sort.Sort(byTimeOrder(sels))
return sels
}
func runeWidth(r rune, prefixWidth int) int {
if r == '\t' {
return 8 - prefixWidth%8
......@@ -698,8 +703,12 @@ 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)
func quoteEntry(entry string) string {
return fmt.Sprintf("%q", entry)
}
func executeCommand(template string, replacement string) {
command := strings.Replace(template, "{}", replacement, -1)
cmd := exec.Command("sh", "-c", command)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
......@@ -858,7 +867,17 @@ func (t *Terminal) Loop() {
case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() {
item := t.merger.Get(t.cy)
executeCommand(t.execmap[mapkey], item.AsString(t.ansi))
executeCommand(t.execmap[mapkey], quoteEntry(item.AsString(t.ansi)))
}
case actExecuteMulti:
if len(t.selected) > 0 {
sels := make([]string, len(t.selected))
for i, sel := range t.sortSelected() {
sels[i] = quoteEntry(*sel.text)
}
executeCommand(t.execmap[mapkey], strings.Join(sels, " "))
} else {
return doAction(actExecute, mapkey)
}
case actInvalid:
t.mutex.Unlock()
......
......@@ -713,6 +713,24 @@ class TestGoFZF < TestBase
File.unlink output rescue nil
end
def test_execute_multi
output = '/tmp/fzf-test-execute-multi'
opts = %[--multi --bind \\"alt-a:execute-multi(echo '[{}], @{}@' >> #{output})\\"]
tmux.send_keys "seq 100 | #{fzf opts}", :Enter
tmux.until { |lines| lines[-2].include? '100/100' }
tmux.send_keys :Escape, :a
tmux.send_keys :BTab, :BTab, :BTab
tmux.send_keys :Escape, :a
tmux.send_keys :Tab, :Tab
tmux.send_keys :Escape, :a
tmux.send_keys :Enter
readonce
assert_equal ['["1"], @"1"@', '["1" "2" "3"], @"1" "2" "3"@', '["1" "2" "4"], @"1" "2" "4"@'],
File.readlines(output).map(&:chomp)
ensure
File.unlink output rescue nil
end
def test_cycle
tmux.send_keys "seq 8 | #{fzf :cycle}", :Enter
tmux.until { |lines| lines[-2].include? '8/8' }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment