Composable actions in --bind

Close #816
This commit is contained in:
Junegunn Choi 2017-01-22 02:32:49 +09:00
parent a06ccc928f
commit 131aa5dd15
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 294 additions and 239 deletions

View File

@ -1,6 +1,12 @@
CHANGELOG CHANGELOG
========= =========
0.16.2
------
- Added support for composite actions in `--bind`. Multiple actions can be
chained using `+` separator.
- e.g. `fzf --bind 'ctrl-y:execute(echo -n {} | pbcopy)+abort'`
0.16.1 0.16.1
------ ------
- Fixed `--height` option to properly fill the window with the background - Fixed `--height` option to properly fill the window with the background

View File

@ -481,17 +481,21 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR
\fBselect-all\fR \fBselect-all\fR
\fBtoggle\fR \fBtoggle\fR
\fBtoggle-all\fR \fBtoggle-all\fR
\fBtoggle-down\fR \fIctrl-i (tab)\fR \fBtoggle+down\fR \fIctrl-i (tab)\fR
\fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle-up\fR : \fBtoggle-down\fR) \fBtoggle-in\fR (\fB--reverse\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
\fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle-down\fR : \fBtoggle-up\fR) \fBtoggle-out\fR (\fB--reverse\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
\fBtoggle-preview\fR \fBtoggle-preview\fR
\fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR) \fBtoggle-sort\fR (equivalent to \fB--toggle-sort\fR)
\fBtoggle-up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBunix-line-discard\fR \fIctrl-u\fR \fBunix-line-discard\fR \fIctrl-u\fR
\fBunix-word-rubout\fR \fIctrl-w\fR \fBunix-word-rubout\fR \fIctrl-w\fR
\fBup\fR \fIctrl-k ctrl-p up\fR \fBup\fR \fIctrl-k ctrl-p up\fR
\fByank\fR \fIctrl-y\fR \fByank\fR \fIctrl-y\fR
Multiple actions can be chained using \fB+\fR separator.
\fBfzf --bind 'ctrl-a:select-all+accept'\fR
With \fBexecute(...)\fR action, you can execute arbitrary commands without With \fBexecute(...)\fR action, you can execute arbitrary commands without
leaving fzf. For example, you can turn fzf into a simple file browser by leaving fzf. For example, you can turn fzf into a simple file browser by
binding \fBenter\fR key to \fBless\fR command like follows. binding \fBenter\fR key to \fBless\fR command like follows.

View File

@ -171,8 +171,7 @@ type Options struct {
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[int]string Expect map[int]string
Keymap map[int]actionType Keymap map[int][]action
Execmap map[int]string
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
@ -220,8 +219,7 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[int]string),
Keymap: make(map[int]actionType), Keymap: make(map[int][]action),
Execmap: make(map[int]string),
Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false},
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@ -578,23 +576,25 @@ func firstKey(keymap map[int]string) int {
const ( const (
escapedColon = 0 escapedColon = 0
escapedComma = 1 escapedComma = 1
escapedPlus = 2
) )
func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string) { func parseKeymap(keymap map[int][]action, str string) {
if executeRegexp == nil { if executeRegexp == nil {
// Backreferences are not supported. // Backreferences are not supported.
// "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|')
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
"(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") "(?si):execute(-multi)?:.+|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)")
} }
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
if strings.HasPrefix(src, ":execute-multi") { if src[len(":execute")] == '-' {
return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")" return ":execute-multi(" + strings.Repeat(" ", len(src)-len(":execute-multi()")) + ")"
} }
return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" return ":execute(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")"
}) })
masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1)
masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1)
masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1)
idx := 0 idx := 0
for _, pairStr := range strings.Split(masked, ",") { for _, pairStr := range strings.Split(masked, ",") {
@ -610,151 +610,173 @@ func parseKeymap(keymap map[int]actionType, execmap map[int]string, str string)
key = ':' + tui.AltZ key = ':' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedComma { } else if len(pair[0]) == 1 && pair[0][0] == escapedComma {
key = ',' + tui.AltZ key = ',' + tui.AltZ
} else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = '+' + tui.AltZ
} else { } else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys) key = firstKey(keys)
} }
act := origPairStr[len(pair[0])+1 : len(origPairStr)] idx2 := len(pair[0]) + 1
actLower := strings.ToLower(act) specs := strings.Split(pair[1], "+")
switch actLower { actions := make([]action, 0, len(specs))
case "ignore": appendAction := func(types ...actionType) {
keymap[key] = actIgnore actions = append(actions, toActions(types...)...)
case "beginning-of-line":
keymap[key] = actBeginningOfLine
case "abort":
keymap[key] = actAbort
case "accept":
keymap[key] = actAccept
case "print-query":
keymap[key] = actPrintQuery
case "backward-char":
keymap[key] = actBackwardChar
case "backward-delete-char":
keymap[key] = actBackwardDeleteChar
case "backward-word":
keymap[key] = actBackwardWord
case "clear-screen":
keymap[key] = actClearScreen
case "delete-char":
keymap[key] = actDeleteChar
case "delete-char/eof":
keymap[key] = actDeleteCharEOF
case "end-of-line":
keymap[key] = actEndOfLine
case "cancel":
keymap[key] = actCancel
case "forward-char":
keymap[key] = actForwardChar
case "forward-word":
keymap[key] = actForwardWord
case "jump":
keymap[key] = actJump
case "jump-accept":
keymap[key] = actJumpAccept
case "kill-line":
keymap[key] = actKillLine
case "kill-word":
keymap[key] = actKillWord
case "unix-line-discard", "line-discard":
keymap[key] = actUnixLineDiscard
case "unix-word-rubout", "word-rubout":
keymap[key] = actUnixWordRubout
case "yank":
keymap[key] = actYank
case "backward-kill-word":
keymap[key] = actBackwardKillWord
case "toggle-down":
keymap[key] = actToggleDown
case "toggle-up":
keymap[key] = actToggleUp
case "toggle-in":
keymap[key] = actToggleIn
case "toggle-out":
keymap[key] = actToggleOut
case "toggle-all":
keymap[key] = actToggleAll
case "select-all":
keymap[key] = actSelectAll
case "deselect-all":
keymap[key] = actDeselectAll
case "toggle":
keymap[key] = actToggle
case "down":
keymap[key] = actDown
case "up":
keymap[key] = actUp
case "page-up":
keymap[key] = actPageUp
case "page-down":
keymap[key] = actPageDown
case "half-page-up":
keymap[key] = actHalfPageUp
case "half-page-down":
keymap[key] = actHalfPageDown
case "previous-history":
keymap[key] = actPreviousHistory
case "next-history":
keymap[key] = actNextHistory
case "toggle-preview":
keymap[key] = actTogglePreview
case "toggle-sort":
keymap[key] = actToggleSort
case "preview-up":
keymap[key] = actPreviewUp
case "preview-down":
keymap[key] = actPreviewDown
case "preview-page-up":
keymap[key] = actPreviewPageUp
case "preview-page-down":
keymap[key] = actPreviewPageDown
default:
if isExecuteAction(actLower) {
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[offset+1 : len(act)-1]
}
} else {
errorExit("unknown action: " + act)
}
} }
prevSpec := ""
for specIndex, maskedSpec := range specs {
spec := origPairStr[idx2 : idx2+len(maskedSpec)]
idx2 += len(maskedSpec) + 1
spec = prevSpec + spec
specLower := strings.ToLower(spec)
switch specLower {
case "ignore":
appendAction(actIgnore)
case "beginning-of-line":
appendAction(actBeginningOfLine)
case "abort":
appendAction(actAbort)
case "accept":
appendAction(actAccept)
case "print-query":
appendAction(actPrintQuery)
case "backward-char":
appendAction(actBackwardChar)
case "backward-delete-char":
appendAction(actBackwardDeleteChar)
case "backward-word":
appendAction(actBackwardWord)
case "clear-screen":
appendAction(actClearScreen)
case "delete-char":
appendAction(actDeleteChar)
case "delete-char/eof":
appendAction(actDeleteCharEOF)
case "end-of-line":
appendAction(actEndOfLine)
case "cancel":
appendAction(actCancel)
case "forward-char":
appendAction(actForwardChar)
case "forward-word":
appendAction(actForwardWord)
case "jump":
appendAction(actJump)
case "jump-accept":
appendAction(actJumpAccept)
case "kill-line":
appendAction(actKillLine)
case "kill-word":
appendAction(actKillWord)
case "unix-line-discard", "line-discard":
appendAction(actUnixLineDiscard)
case "unix-word-rubout", "word-rubout":
appendAction(actUnixWordRubout)
case "yank":
appendAction(actYank)
case "backward-kill-word":
appendAction(actBackwardKillWord)
case "toggle-down":
appendAction(actToggle, actDown)
case "toggle-up":
appendAction(actToggle, actUp)
case "toggle-in":
appendAction(actToggleIn)
case "toggle-out":
appendAction(actToggleOut)
case "toggle-all":
appendAction(actToggleAll)
case "select-all":
appendAction(actSelectAll)
case "deselect-all":
appendAction(actDeselectAll)
case "toggle":
appendAction(actToggle)
case "down":
appendAction(actDown)
case "up":
appendAction(actUp)
case "page-up":
appendAction(actPageUp)
case "page-down":
appendAction(actPageDown)
case "half-page-up":
appendAction(actHalfPageUp)
case "half-page-down":
appendAction(actHalfPageDown)
case "previous-history":
appendAction(actPreviousHistory)
case "next-history":
appendAction(actNextHistory)
case "toggle-preview":
appendAction(actTogglePreview)
case "toggle-sort":
appendAction(actToggleSort)
case "preview-up":
appendAction(actPreviewUp)
case "preview-down":
appendAction(actPreviewDown)
case "preview-page-up":
appendAction(actPreviewPageUp)
case "preview-page-down":
appendAction(actPreviewPageDown)
default:
t := isExecuteAction(specLower)
if t == actIgnore {
errorExit("unknown action: " + spec)
} else {
var offset int
if t == actExecuteMulti {
offset = len("execute-multi")
} else {
offset = len("execute")
}
if spec[offset] == ':' {
if specIndex == len(specs)-1 {
actions = append(actions, action{t: t, a: spec[offset+1:]})
} else {
prevSpec = spec + "+"
continue
}
} else {
actions = append(actions, action{t: t, a: spec[offset+1 : len(spec)-1]})
}
}
}
prevSpec = ""
}
keymap[key] = actions
} }
} }
func isExecuteAction(str string) bool { func isExecuteAction(str string) actionType {
if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { t := actExecute
return false if !strings.HasPrefix(str, "execute") || len(str) < len("execute(") {
return actIgnore
} }
b := str[len("execute")] b := str[len("execute")]
if strings.HasPrefix(str, "execute-multi") { if strings.HasPrefix(str, "execute-multi") {
if len(str) < len("execute-multi()") { if len(str) < len("execute-multi(") {
return false return actIgnore
} }
t = actExecuteMulti
b = str[len("execute-multi")] b = str[len("execute-multi")]
} }
e := str[len(str)-1] e := str[len(str)-1]
if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' ||
b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") { b == e && strings.ContainsAny(string(b), "~!@#$%^&*;/|") {
return true return t
} }
return false return actIgnore
} }
func parseToggleSort(keymap map[int]actionType, str string) { func parseToggleSort(keymap map[int][]action, str string) {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
} }
keymap[firstKey(keys)] = actToggleSort keymap[firstKey(keys)] = toActions(actToggleSort)
} }
func strLines(str string) []string { func strLines(str string) []string {
@ -919,7 +941,7 @@ func parseOptions(opts *Options, allArgs []string) {
case "--tiebreak": case "--tiebreak":
opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required"))
case "--bind": case "--bind":
parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required")) parseKeymap(opts.Keymap, 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 {
@ -1089,7 +1111,7 @@ 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 {
parseKeymap(opts.Keymap, opts.Execmap, value) parseKeymap(opts.Keymap, 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-size="); match { } else if match, value := optString(arg, "--history-size="); match {
@ -1145,20 +1167,22 @@ func postProcessOptions(opts *Options) {
// Default actions for CTRL-N / CTRL-P when --history is set // Default actions for CTRL-N / CTRL-P when --history is set
if opts.History != nil { if opts.History != nil {
if _, prs := opts.Keymap[tui.CtrlP]; !prs { if _, prs := opts.Keymap[tui.CtrlP]; !prs {
opts.Keymap[tui.CtrlP] = actPreviousHistory opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory)
} }
if _, prs := opts.Keymap[tui.CtrlN]; !prs { if _, prs := opts.Keymap[tui.CtrlN]; !prs {
opts.Keymap[tui.CtrlN] = actNextHistory opts.Keymap[tui.CtrlN] = toActions(actNextHistory)
} }
} }
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, act := range opts.Keymap { for key, actions := range opts.Keymap {
if act == actToggleSort { for _, act := range actions {
opts.ToggleSort = true if act.t == actToggleSort {
opts.ToggleSort = true
}
} }
keymap[key] = act keymap[key] = actions
} }
opts.Keymap = keymap opts.Keymap = keymap

View File

@ -225,49 +225,51 @@ func TestParseKeysWithComma(t *testing.T) {
} }
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
check := func(action actionType, expected actionType) {
if 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 := func(keyName int, arg1 string, types ...actionType) {
check(actBeginningOfLine, keymap[tui.CtrlA]) if len(keymap[keyName]) != len(types) {
parseKeymap(keymap, execmap, t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
"ctrl-a:kill-line,ctrl-b:toggle-sort,c:page-up,alt-z:page-down,"+ return
"f1:execute(ls {}),f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ }
"alt-a:execute@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};"+ for idx, action := range keymap[keyName] {
",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") if types[idx] != action.t {
check(actKillLine, keymap[tui.CtrlA]) t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
check(actToggleSort, keymap[tui.CtrlB]) }
check(actPageUp, keymap[tui.AltZ+'c']) }
check(actAbort, keymap[tui.AltZ+',']) if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
check(actAccept, keymap[tui.AltZ+':']) t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
check(actPageDown, keymap[tui.AltZ]) }
check(actExecute, keymap[tui.F1]) }
check(actExecute, keymap[tui.F2]) check(tui.CtrlA, "", actBeginningOfLine)
check(actExecute, keymap[tui.F3]) parseKeymap(keymap,
check(actExecute, keymap[tui.F4]) "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
checkString("ls {}", execmap[tui.F1]) "f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
checkString("echo {}, {}, {}", execmap[tui.F2]) "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
checkString("echo '({})'", execmap[tui.F3]) "x:Execute(foo+bar),X:execute/bar+baz/"+
checkString("less {}", execmap[tui.F4]) ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA]) check(tui.CtrlA, "", actKillLine)
checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB]) check(tui.CtrlB, "", actToggleSort, actUp, actDown)
checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X']) check(tui.AltZ+'c', "", actPageUp)
check(tui.AltZ+',', "", actAbort)
check(tui.AltZ+':', "", actAccept)
check(tui.AltZ, "", actPageDown)
check(tui.F1, "ls {}", actExecute, actAbort)
check(tui.F2, "echo {}, {}, {}", actExecute)
check(tui.F3, "echo '({})'", actExecute)
check(tui.F4, "less {}", actExecute)
check(tui.AltZ+'x', "foo+bar", actExecute)
check(tui.AltZ+'X', "bar+baz", actExecute)
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
} }
parseKeymap(keymap, execmap, "f1:abort") parseKeymap(keymap, "f1:abort")
check(actAbort, keymap[tui.F1]) check(tui.F1, "", actAbort)
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
@ -327,7 +329,7 @@ func TestDefaultCtrlNP(t *testing.T) {
opts := defaultOptions() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[key] != expected { if opts.Keymap[key][0].t != expected {
t.Error() t.Error()
} }
} }

View File

@ -72,8 +72,7 @@ type Terminal struct {
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[int]string expect map[int]string
keymap map[int]actionType keymap map[int][]action
execmap map[int]string
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
@ -148,6 +147,11 @@ const (
reqQuit reqQuit
) )
type action struct {
t actionType
a string
}
type actionType int type actionType int
const ( const (
@ -203,54 +207,62 @@ const (
actExecuteMulti actExecuteMulti
) )
func defaultKeymap() map[int]actionType { func toActions(types ...actionType) []action {
keymap := make(map[int]actionType) actions := make([]action, len(types))
keymap[tui.Invalid] = actInvalid for idx, t := range types {
keymap[tui.Resize] = actClearScreen actions[idx] = action{t: t, a: ""}
keymap[tui.CtrlA] = actBeginningOfLine }
keymap[tui.CtrlB] = actBackwardChar return actions
keymap[tui.CtrlC] = actAbort }
keymap[tui.CtrlG] = actAbort
keymap[tui.CtrlQ] = actAbort
keymap[tui.ESC] = actAbort
keymap[tui.CtrlD] = actDeleteCharEOF
keymap[tui.CtrlE] = actEndOfLine
keymap[tui.CtrlF] = actForwardChar
keymap[tui.CtrlH] = actBackwardDeleteChar
keymap[tui.BSpace] = actBackwardDeleteChar
keymap[tui.Tab] = actToggleDown
keymap[tui.BTab] = actToggleUp
keymap[tui.CtrlJ] = actDown
keymap[tui.CtrlK] = actUp
keymap[tui.CtrlL] = actClearScreen
keymap[tui.CtrlM] = actAccept
keymap[tui.CtrlN] = actDown
keymap[tui.CtrlP] = actUp
keymap[tui.CtrlU] = actUnixLineDiscard
keymap[tui.CtrlW] = actUnixWordRubout
keymap[tui.CtrlY] = actYank
keymap[tui.AltB] = actBackwardWord func defaultKeymap() map[int][]action {
keymap[tui.SLeft] = actBackwardWord keymap := make(map[int][]action)
keymap[tui.AltF] = actForwardWord keymap[tui.Invalid] = toActions(actInvalid)
keymap[tui.SRight] = actForwardWord keymap[tui.Resize] = toActions(actClearScreen)
keymap[tui.AltD] = actKillWord keymap[tui.CtrlA] = toActions(actBeginningOfLine)
keymap[tui.AltBS] = actBackwardKillWord keymap[tui.CtrlB] = toActions(actBackwardChar)
keymap[tui.CtrlC] = toActions(actAbort)
keymap[tui.CtrlG] = toActions(actAbort)
keymap[tui.CtrlQ] = toActions(actAbort)
keymap[tui.ESC] = toActions(actAbort)
keymap[tui.CtrlD] = toActions(actDeleteCharEOF)
keymap[tui.CtrlE] = toActions(actEndOfLine)
keymap[tui.CtrlF] = toActions(actForwardChar)
keymap[tui.CtrlH] = toActions(actBackwardDeleteChar)
keymap[tui.BSpace] = toActions(actBackwardDeleteChar)
keymap[tui.Tab] = toActions(actToggleDown)
keymap[tui.BTab] = toActions(actToggleUp)
keymap[tui.CtrlJ] = toActions(actDown)
keymap[tui.CtrlK] = toActions(actUp)
keymap[tui.CtrlL] = toActions(actClearScreen)
keymap[tui.CtrlM] = toActions(actAccept)
keymap[tui.CtrlN] = toActions(actDown)
keymap[tui.CtrlP] = toActions(actUp)
keymap[tui.CtrlU] = toActions(actUnixLineDiscard)
keymap[tui.CtrlW] = toActions(actUnixWordRubout)
keymap[tui.CtrlY] = toActions(actYank)
keymap[tui.Up] = actUp keymap[tui.AltB] = toActions(actBackwardWord)
keymap[tui.Down] = actDown keymap[tui.SLeft] = toActions(actBackwardWord)
keymap[tui.Left] = actBackwardChar keymap[tui.AltF] = toActions(actForwardWord)
keymap[tui.Right] = actForwardChar keymap[tui.SRight] = toActions(actForwardWord)
keymap[tui.AltD] = toActions(actKillWord)
keymap[tui.AltBS] = toActions(actBackwardKillWord)
keymap[tui.Home] = actBeginningOfLine keymap[tui.Up] = toActions(actUp)
keymap[tui.End] = actEndOfLine keymap[tui.Down] = toActions(actDown)
keymap[tui.Del] = actDeleteChar keymap[tui.Left] = toActions(actBackwardChar)
keymap[tui.PgUp] = actPageUp keymap[tui.Right] = toActions(actForwardChar)
keymap[tui.PgDn] = actPageDown
keymap[tui.Rune] = actRune keymap[tui.Home] = toActions(actBeginningOfLine)
keymap[tui.Mouse] = actMouse keymap[tui.End] = toActions(actEndOfLine)
keymap[tui.DoubleClick] = actAccept keymap[tui.Del] = toActions(actDeleteChar)
keymap[tui.PgUp] = toActions(actPageUp)
keymap[tui.PgDn] = toActions(actPageDown)
keymap[tui.Rune] = toActions(actRune)
keymap[tui.Mouse] = toActions(actMouse)
keymap[tui.DoubleClick] = toActions(actAccept)
return keymap return keymap
} }
@ -323,7 +335,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
delimiter: opts.Delimiter, delimiter: opts.Delimiter,
expect: opts.Expect, expect: opts.Expect,
keymap: opts.Keymap, keymap: opts.Keymap,
execmap: opts.Execmap,
pressed: "", pressed: "",
printQuery: opts.PrintQuery, printQuery: opts.PrintQuery,
history: opts.History, history: opts.History,
@ -1314,13 +1325,21 @@ func (t *Terminal) Loop() {
} }
} }
var doAction func(actionType, int) bool var doAction func(action, int) bool
doAction = func(action actionType, mapkey int) bool { doActions := func(actions []action, mapkey int) bool {
switch action { for _, action := range actions {
if !doAction(action, mapkey) {
return false
}
}
return true
}
doAction = func(a action, mapkey int) bool {
switch a.t {
case actIgnore: case actIgnore:
case actExecute: case actExecute:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
t.executeCommand(t.execmap[mapkey], []*Item{t.currentItem()}) t.executeCommand(a.a, []*Item{t.currentItem()})
} }
case actExecuteMulti: case actExecuteMulti:
if len(t.selected) > 0 { if len(t.selected) > 0 {
@ -1328,9 +1347,9 @@ func (t *Terminal) Loop() {
for i, sel := range t.sortSelected() { for i, sel := range t.sortSelected() {
sels[i] = sel.item sels[i] = sel.item
} }
t.executeCommand(t.execmap[mapkey], sels) t.executeCommand(a.a, sels)
} else { } else {
return doAction(actExecute, mapkey) return doAction(action{t: actExecute, a: a.a}, mapkey)
} }
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
@ -1431,14 +1450,14 @@ func (t *Terminal) Loop() {
} }
case actToggleIn: case actToggleIn:
if t.reverse { if t.reverse {
return doAction(actToggleUp, mapkey) return doAction(action{t: actToggleUp}, mapkey)
} }
return doAction(actToggleDown, mapkey) return doAction(action{t: actToggleDown}, mapkey)
case actToggleOut: case actToggleOut:
if t.reverse { if t.reverse {
return doAction(actToggleDown, mapkey) return doAction(action{t: actToggleDown}, mapkey)
} }
return doAction(actToggleUp, mapkey) return doAction(action{t: actToggleUp}, mapkey)
case actToggleDown: case actToggleDown:
if t.multi && t.merger.Length() > 0 { if t.multi && t.merger.Length() > 0 {
toggle() toggle()
@ -1558,7 +1577,7 @@ func (t *Terminal) Loop() {
// Double-click // Double-click
if my >= min { if my >= min {
if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() {
return doAction(t.keymap[tui.DoubleClick], tui.DoubleClick) return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick)
} }
} }
} else if me.Down { } else if me.Down {
@ -1580,14 +1599,14 @@ func (t *Terminal) Loop() {
changed := false changed := false
mapkey := event.Type mapkey := event.Type
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
action := t.keymap[mapkey] actions := t.keymap[mapkey]
if mapkey == tui.Rune { if mapkey == tui.Rune {
mapkey = int(event.Char) + int(tui.AltZ) mapkey = int(event.Char) + int(tui.AltZ)
if act, prs := t.keymap[mapkey]; prs { if act, prs := t.keymap[mapkey]; prs {
action = act actions = act
} }
} }
if !doAction(action, mapkey) { if !doActions(actions, mapkey) {
continue continue
} }
// Truncate the query if it's too long // Truncate the query if it's too long