From 131aa5dd15788875e796bbd2f2bfdf644584284a Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sun, 22 Jan 2017 02:32:49 +0900 Subject: [PATCH] Composable actions in --bind Close #816 --- CHANGELOG.md | 6 + man/man1/fzf.1 | 12 +- src/options.go | 294 ++++++++++++++++++++++++-------------------- src/options_test.go | 80 ++++++------ src/terminal.go | 141 ++++++++++++--------- 5 files changed, 294 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 473f899..fac6f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ 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 ------ - Fixed `--height` option to properly fill the window with the background diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 5160d6b..b139dcc 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -481,17 +481,21 @@ e.g. \fBfzf --bind=ctrl-j:accept,ctrl-k:kill-line\fR \fBselect-all\fR \fBtoggle\fR \fBtoggle-all\fR - \fBtoggle-down\fR \fIctrl-i (tab)\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+down\fR \fIctrl-i (tab)\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-preview\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-word-rubout\fR \fIctrl-w\fR \fBup\fR \fIctrl-k ctrl-p up\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 leaving fzf. For example, you can turn fzf into a simple file browser by binding \fBenter\fR key to \fBless\fR command like follows. diff --git a/src/options.go b/src/options.go index 16020dd..eb21ee7 100644 --- a/src/options.go +++ b/src/options.go @@ -171,8 +171,7 @@ type Options struct { Filter *string ToggleSort bool Expect map[int]string - Keymap map[int]actionType - Execmap map[int]string + Keymap map[int][]action Preview previewOpts PrintQuery bool ReadZero bool @@ -220,8 +219,7 @@ func defaultOptions() *Options { Filter: nil, ToggleSort: false, Expect: make(map[int]string), - Keymap: make(map[int]actionType), - Execmap: make(map[int]string), + Keymap: make(map[int][]action), Preview: previewOpts{"", posRight, sizeSpec{50, true}, false, false}, PrintQuery: false, ReadZero: false, @@ -578,23 +576,25 @@ func firstKey(keymap map[int]string) int { const ( escapedColon = 0 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 { // Backreferences are not supported. // "~!@#$%^&*;/|".each_char.map { |c| Regexp.escape(c) }.map { |c| "#{c}[^#{c}]*#{c}" }.join('|') executeRegexp = regexp.MustCompile( - "(?s):execute(-multi)?:.*|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") + "(?si):execute(-multi)?:.+|:execute(-multi)?(\\([^)]*\\)|\\[[^\\]]*\\]|~[^~]*~|![^!]*!|@[^@]*@|\\#[^\\#]*\\#|\\$[^\\$]*\\$|%[^%]*%|\\^[^\\^]*\\^|&[^&]*&|\\*[^\\*]*\\*|;[^;]*;|/[^/]*/|\\|[^\\|]*\\|)") } 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(" + strings.Repeat(" ", len(src)-len(":execute()")) + ")" }) masked = strings.Replace(masked, "::", string([]rune{escapedColon, ':'}), -1) masked = strings.Replace(masked, ",:", string([]rune{escapedComma, ':'}), -1) + masked = strings.Replace(masked, "+:", string([]rune{escapedPlus, ':'}), -1) idx := 0 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 } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { key = ',' + tui.AltZ + } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { + key = '+' + tui.AltZ } else { keys := parseKeyChords(pair[0], "key name required") key = firstKey(keys) } - act := origPairStr[len(pair[0])+1 : len(origPairStr)] - actLower := strings.ToLower(act) - switch actLower { - case "ignore": - keymap[key] = actIgnore - 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) - } + idx2 := len(pair[0]) + 1 + specs := strings.Split(pair[1], "+") + actions := make([]action, 0, len(specs)) + appendAction := func(types ...actionType) { + actions = append(actions, toActions(types...)...) } + 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 { - if !strings.HasPrefix(str, "execute") || len(str) < len("execute()") { - return false +func isExecuteAction(str string) actionType { + t := actExecute + if !strings.HasPrefix(str, "execute") || len(str) < len("execute(") { + return actIgnore } + b := str[len("execute")] if strings.HasPrefix(str, "execute-multi") { - if len(str) < len("execute-multi()") { - return false + if len(str) < len("execute-multi(") { + return actIgnore } + t = actExecuteMulti b = str[len("execute-multi")] } e := str[len(str)-1] if b == ':' || b == '(' && e == ')' || b == '[' && e == ']' || 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") if len(keys) != 1 { errorExit("multiple keys specified") } - keymap[firstKey(keys)] = actToggleSort + keymap[firstKey(keys)] = toActions(actToggleSort) } func strLines(str string) []string { @@ -919,7 +941,7 @@ func parseOptions(opts *Options, allArgs []string) { case "--tiebreak": opts.Criteria = parseTiebreak(nextString(allArgs, &i, "sort criterion required")) case "--bind": - parseKeymap(opts.Keymap, opts.Execmap, nextString(allArgs, &i, "bind expression required")) + parseKeymap(opts.Keymap, nextString(allArgs, &i, "bind expression required")) case "--color": spec := optionalNextString(allArgs, &i) if len(spec) == 0 { @@ -1089,7 +1111,7 @@ 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 { - parseKeymap(opts.Keymap, opts.Execmap, value) + parseKeymap(opts.Keymap, value) } else if match, value := optString(arg, "--history="); match { setHistory(value) } 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 if opts.History != nil { 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 { - opts.Keymap[tui.CtrlN] = actNextHistory + opts.Keymap[tui.CtrlN] = toActions(actNextHistory) } } // Extend the default key map keymap := defaultKeymap() - for key, act := range opts.Keymap { - if act == actToggleSort { - opts.ToggleSort = true + for key, actions := range opts.Keymap { + for _, act := range actions { + if act.t == actToggleSort { + opts.ToggleSort = true + } } - keymap[key] = act + keymap[key] = actions } opts.Keymap = keymap diff --git a/src/options_test.go b/src/options_test.go index 07616fc..29d9842 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -225,49 +225,51 @@ func TestParseKeysWithComma(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() - execmap := make(map[int]string) - check(actBeginningOfLine, keymap[tui.CtrlA]) - parseKeymap(keymap, execmap, - "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 (,),[,],/,:,@,%,{};"+ - ",,:abort,::accept,X:execute:\nfoobar,Y:execute(baz)") - check(actKillLine, keymap[tui.CtrlA]) - check(actToggleSort, keymap[tui.CtrlB]) - check(actPageUp, keymap[tui.AltZ+'c']) - check(actAbort, keymap[tui.AltZ+',']) - check(actAccept, keymap[tui.AltZ+':']) - check(actPageDown, keymap[tui.AltZ]) - check(actExecute, keymap[tui.F1]) - check(actExecute, keymap[tui.F2]) - check(actExecute, keymap[tui.F3]) - check(actExecute, keymap[tui.F4]) - checkString("ls {}", execmap[tui.F1]) - checkString("echo {}, {}, {}", execmap[tui.F2]) - checkString("echo '({})'", execmap[tui.F3]) - checkString("less {}", execmap[tui.F4]) - checkString("echo (,),[,],/,:,;,%,{}", execmap[tui.AltA]) - checkString("echo (,),[,],/,:,@,%,{}", execmap[tui.AltB]) - checkString("\nfoobar,Y:execute(baz)", execmap[tui.AltZ+'X']) + check := func(keyName int, arg1 string, types ...actionType) { + if len(keymap[keyName]) != len(types) { + t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName])) + return + } + for idx, action := range keymap[keyName] { + if types[idx] != action.t { + t.Errorf("invalid action type (%d != %d)", types[idx], action.t) + } + } + if len(arg1) > 0 && keymap[keyName][0].a != arg1 { + t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a) + } + } + check(tui.CtrlA, "", actBeginningOfLine) + parseKeymap(keymap, + "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ + "f1:execute(ls {})+abort,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ + "alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+ + "x:Execute(foo+bar),X:execute/bar+baz/"+ + ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") + check(tui.CtrlA, "", actKillLine) + check(tui.CtrlB, "", actToggleSort, actUp, actDown) + 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{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { - parseKeymap(keymap, execmap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) - checkString("foobar", execmap[tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0])]) + parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) + check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) } - parseKeymap(keymap, execmap, "f1:abort") - check(actAbort, keymap[tui.F1]) + parseKeymap(keymap, "f1:abort") + check(tui.F1, "", actAbort) } func TestColorSpec(t *testing.T) { @@ -327,7 +329,7 @@ func TestDefaultCtrlNP(t *testing.T) { opts := defaultOptions() parseOptions(opts, words) postProcessOptions(opts) - if opts.Keymap[key] != expected { + if opts.Keymap[key][0].t != expected { t.Error() } } diff --git a/src/terminal.go b/src/terminal.go index 2378984..9e30f30 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -72,8 +72,7 @@ type Terminal struct { toggleSort bool delimiter Delimiter expect map[int]string - keymap map[int]actionType - execmap map[int]string + keymap map[int][]action pressed string printQuery bool history *History @@ -148,6 +147,11 @@ const ( reqQuit ) +type action struct { + t actionType + a string +} + type actionType int const ( @@ -203,54 +207,62 @@ const ( actExecuteMulti ) -func defaultKeymap() map[int]actionType { - keymap := make(map[int]actionType) - keymap[tui.Invalid] = actInvalid - keymap[tui.Resize] = actClearScreen - keymap[tui.CtrlA] = actBeginningOfLine - keymap[tui.CtrlB] = actBackwardChar - 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 +func toActions(types ...actionType) []action { + actions := make([]action, len(types)) + for idx, t := range types { + actions[idx] = action{t: t, a: ""} + } + return actions +} - keymap[tui.AltB] = actBackwardWord - keymap[tui.SLeft] = actBackwardWord - keymap[tui.AltF] = actForwardWord - keymap[tui.SRight] = actForwardWord - keymap[tui.AltD] = actKillWord - keymap[tui.AltBS] = actBackwardKillWord +func defaultKeymap() map[int][]action { + keymap := make(map[int][]action) + keymap[tui.Invalid] = toActions(actInvalid) + keymap[tui.Resize] = toActions(actClearScreen) + keymap[tui.CtrlA] = toActions(actBeginningOfLine) + 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.Down] = actDown - keymap[tui.Left] = actBackwardChar - keymap[tui.Right] = actForwardChar + keymap[tui.AltB] = toActions(actBackwardWord) + keymap[tui.SLeft] = toActions(actBackwardWord) + keymap[tui.AltF] = toActions(actForwardWord) + keymap[tui.SRight] = toActions(actForwardWord) + keymap[tui.AltD] = toActions(actKillWord) + keymap[tui.AltBS] = toActions(actBackwardKillWord) - keymap[tui.Home] = actBeginningOfLine - keymap[tui.End] = actEndOfLine - keymap[tui.Del] = actDeleteChar - keymap[tui.PgUp] = actPageUp - keymap[tui.PgDn] = actPageDown + keymap[tui.Up] = toActions(actUp) + keymap[tui.Down] = toActions(actDown) + keymap[tui.Left] = toActions(actBackwardChar) + keymap[tui.Right] = toActions(actForwardChar) - keymap[tui.Rune] = actRune - keymap[tui.Mouse] = actMouse - keymap[tui.DoubleClick] = actAccept + keymap[tui.Home] = toActions(actBeginningOfLine) + keymap[tui.End] = toActions(actEndOfLine) + 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 } @@ -323,7 +335,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { delimiter: opts.Delimiter, expect: opts.Expect, keymap: opts.Keymap, - execmap: opts.Execmap, pressed: "", printQuery: opts.PrintQuery, history: opts.History, @@ -1314,13 +1325,21 @@ func (t *Terminal) Loop() { } } - var doAction func(actionType, int) bool - doAction = func(action actionType, mapkey int) bool { - switch action { + var doAction func(action, int) bool + doActions := func(actions []action, mapkey int) bool { + 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 actExecute: 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: if len(t.selected) > 0 { @@ -1328,9 +1347,9 @@ func (t *Terminal) Loop() { for i, sel := range t.sortSelected() { sels[i] = sel.item } - t.executeCommand(t.execmap[mapkey], sels) + t.executeCommand(a.a, sels) } else { - return doAction(actExecute, mapkey) + return doAction(action{t: actExecute, a: a.a}, mapkey) } case actInvalid: t.mutex.Unlock() @@ -1431,14 +1450,14 @@ func (t *Terminal) Loop() { } case actToggleIn: 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: 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: if t.multi && t.merger.Length() > 0 { toggle() @@ -1558,7 +1577,7 @@ func (t *Terminal) Loop() { // Double-click if my >= min { 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 { @@ -1580,14 +1599,14 @@ func (t *Terminal) Loop() { changed := false mapkey := event.Type if t.jumping == jumpDisabled { - action := t.keymap[mapkey] + actions := t.keymap[mapkey] if mapkey == tui.Rune { mapkey = int(event.Char) + int(tui.AltZ) if act, prs := t.keymap[mapkey]; prs { - action = act + actions = act } } - if !doAction(action, mapkey) { + if !doActions(actions, mapkey) { continue } // Truncate the query if it's too long