From 7f8e0dbc408eff786865d0e2d9e3c62ec3ed4776 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Wed, 30 Dec 2020 01:59:18 +0900 Subject: [PATCH] Extend support for alt key chords "alt-" with any case-sensitive character is allowed --- CHANGELOG.md | 4 ++ man/man1/fzf.1 | 6 +- src/options.go | 162 +++++++++++++++++++++--------------------- src/options_test.go | 121 +++++++++++++++++--------------- src/terminal.go | 167 +++++++++++++++++++++++--------------------- src/tui/light.go | 43 +++++------- src/tui/tcell.go | 71 ++++++++----------- src/tui/tui.go | 50 ++++++++----- 8 files changed, 318 insertions(+), 306 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b02dcbe..77296b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ CHANGELOG - Added `last` action to move the cursor to the last match - The opposite action `top` is renamed to `first`, but `top` is still recognized as a synonym for backward compatibility +- Extended support for alt key chords: alt with any case-sensitive single character + ```sh + fzf --bind alt-,:first,alt-.:last + ``` 0.24.4 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index b6ae8a6..f0161cd 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -666,9 +666,7 @@ e.g. .br \fIctrl-alt-[a-z]\fR .br -\fIalt-[a-z]\fR -.br -\fIalt-[0-9]\fR +\fIalt-[*]\fR (Any case-sensitive single character is allowed) .br \fIf[1-12]\fR .br @@ -692,8 +690,6 @@ e.g. .br \fIalt-bspace\fR (\fIalt-bs\fR) .br -\fIalt-/\fR -.br \fItab\fR .br \fIbtab\fR (\fIshift-tab\fR) diff --git a/src/options.go b/src/options.go index 5eff033..c33f14c 100644 --- a/src/options.go +++ b/src/options.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" "unicode" - "unicode/utf8" "github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/tui" @@ -211,8 +210,8 @@ type Options struct { Exit0 bool Filter *string ToggleSort bool - Expect map[int]string - Keymap map[int][]action + Expect map[tui.Event]string + Keymap map[tui.Event][]action Preview previewOpts PrintQuery bool ReadZero bool @@ -272,8 +271,8 @@ func defaultOptions() *Options { Exit0: false, Filter: nil, ToggleSort: false, - Expect: make(map[int]string), - Keymap: make(map[int][]action), + Expect: make(map[tui.Event]string), + Keymap: make(map[tui.Event][]action), Preview: defaultPreviewOpts(""), PrintQuery: false, ReadZero: false, @@ -445,7 +444,7 @@ func parseBorder(str string, optional bool) tui.BorderShape { return tui.BorderNone } -func parseKeyChords(str string, message string) map[int]string { +func parseKeyChords(str string, message string) map[tui.Event]string { if len(str) == 0 { errorExit(message) } @@ -455,124 +454,129 @@ func parseKeyChords(str string, message string) map[int]string { tokens = append(tokens, ",") } - chords := make(map[int]string) + chords := make(map[tui.Event]string) for _, key := range tokens { if len(key) == 0 { continue // ignore } lkey := strings.ToLower(key) - chord := 0 + add := func(e tui.EventType) { + chords[e.AsEvent()] = key + } switch lkey { case "up": - chord = tui.Up + add(tui.Up) case "down": - chord = tui.Down + add(tui.Down) case "left": - chord = tui.Left + add(tui.Left) case "right": - chord = tui.Right + add(tui.Right) case "enter", "return": - chord = tui.CtrlM + add(tui.CtrlM) case "space": - chord = tui.AltZ + int(' ') + chords[tui.Key(' ')] = key case "bspace", "bs": - chord = tui.BSpace + add(tui.BSpace) case "ctrl-space": - chord = tui.CtrlSpace + add(tui.CtrlSpace) case "ctrl-^", "ctrl-6": - chord = tui.CtrlCaret + add(tui.CtrlCaret) case "ctrl-/", "ctrl-_": - chord = tui.CtrlSlash + add(tui.CtrlSlash) case "ctrl-\\": - chord = tui.CtrlBackSlash + add(tui.CtrlBackSlash) case "ctrl-]": - chord = tui.CtrlRightBracket + add(tui.CtrlRightBracket) case "change": - chord = tui.Change + add(tui.Change) case "backward-eof": - chord = tui.BackwardEOF + add(tui.BackwardEOF) case "alt-enter", "alt-return": - chord = tui.CtrlAltM + chords[tui.CtrlAltKey('m')] = key case "alt-space": - chord = tui.AltSpace - case "alt-/": - chord = tui.AltSlash + chords[tui.AltKey(' ')] = key case "alt-bs", "alt-bspace": - chord = tui.AltBS + add(tui.AltBS) case "alt-up": - chord = tui.AltUp + add(tui.AltUp) case "alt-down": - chord = tui.AltDown + add(tui.AltDown) case "alt-left": - chord = tui.AltLeft + add(tui.AltLeft) case "alt-right": - chord = tui.AltRight + add(tui.AltRight) case "tab": - chord = tui.Tab + add(tui.Tab) case "btab", "shift-tab": - chord = tui.BTab + add(tui.BTab) case "esc": - chord = tui.ESC + add(tui.ESC) case "del": - chord = tui.Del + add(tui.Del) case "home": - chord = tui.Home + add(tui.Home) case "end": - chord = tui.End + add(tui.End) case "insert": - chord = tui.Insert + add(tui.Insert) case "pgup", "page-up": - chord = tui.PgUp + add(tui.PgUp) case "pgdn", "page-down": - chord = tui.PgDn + add(tui.PgDn) case "alt-shift-up", "shift-alt-up": - chord = tui.AltSUp + add(tui.AltSUp) case "alt-shift-down", "shift-alt-down": - chord = tui.AltSDown + add(tui.AltSDown) case "alt-shift-left", "shift-alt-left": - chord = tui.AltSLeft + add(tui.AltSLeft) case "alt-shift-right", "shift-alt-right": - chord = tui.AltSRight + add(tui.AltSRight) case "shift-up": - chord = tui.SUp + add(tui.SUp) case "shift-down": - chord = tui.SDown + add(tui.SDown) case "shift-left": - chord = tui.SLeft + add(tui.SLeft) case "shift-right": - chord = tui.SRight + add(tui.SRight) case "left-click": - chord = tui.LeftClick + add(tui.LeftClick) case "right-click": - chord = tui.RightClick + add(tui.RightClick) case "double-click": - chord = tui.DoubleClick + add(tui.DoubleClick) case "f10": - chord = tui.F10 + add(tui.F10) case "f11": - chord = tui.F11 + add(tui.F11) case "f12": - chord = tui.F12 + add(tui.F12) default: + runes := []rune(key) if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) { - chord = tui.CtrlAltA + int(lkey[9]) - 'a' + chords[tui.CtrlAltKey(rune(key[9]))] = key } else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) { - chord = tui.CtrlA + int(lkey[5]) - 'a' - } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { - chord = tui.AltA + int(lkey[4]) - 'a' - } else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) { - chord = tui.Alt0 + int(lkey[4]) - '0' + add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a')) + } else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") { + r := runes[4] + switch r { + case escapedColon: + r = ':' + case escapedComma: + r = ',' + case escapedPlus: + r = '+' + } + chords[tui.AltKey(r)] = key } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' { - chord = tui.F1 + int(key[1]) - '1' - } else if utf8.RuneCountInString(key) == 1 { - chord = tui.AltZ + int([]rune(key)[0]) + add(tui.EventType(tui.F1.Int() + int(key[1]) - '1')) + } else if len(runes) == 1 { + chords[tui.Key(runes[0])] = key } else { errorExit("unsupported key: " + key) } } - if chord > 0 { - chords[chord] = key - } } return chords } @@ -720,11 +724,11 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme { var executeRegexp *regexp.Regexp -func firstKey(keymap map[int]string) int { +func firstKey(keymap map[tui.Event]string) tui.Event { for k := range keymap { return k } - return 0 + return tui.EventType(0).AsEvent() } const ( @@ -740,7 +744,7 @@ func init() { `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) } -func parseKeymap(keymap map[int][]action, str string) { +func parseKeymap(keymap map[tui.Event][]action, str string) { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { symbol := ":" if strings.HasPrefix(src, "+") { @@ -776,13 +780,13 @@ func parseKeymap(keymap map[int][]action, str string) { if len(pair) < 2 { errorExit("bind action not specified: " + origPairStr) } - var key int + var key tui.Event if len(pair[0]) == 1 && pair[0][0] == escapedColon { - key = ':' + tui.AltZ + key = tui.Key(':') } else if len(pair[0]) == 1 && pair[0][0] == escapedComma { - key = ',' + tui.AltZ + key = tui.Key(',') } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus { - key = '+' + tui.AltZ + key = tui.Key('+') } else { keys := parseKeyChords(pair[0], "key name required") key = firstKey(keys) @@ -981,7 +985,7 @@ func isExecuteAction(str string) actionType { return actIgnore } -func parseToggleSort(keymap map[int][]action, str string) { +func parseToggleSort(keymap map[tui.Event][]action, str string) { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { errorExit("multiple keys specified") @@ -1188,7 +1192,7 @@ func parseOptions(opts *Options, allArgs []string) { opts.Expect[k] = v } case "--no-expect": - opts.Expect = make(map[int]string) + opts.Expect = make(map[tui.Event]string) case "--no-phony": opts.Phony = false case "--phony": @@ -1512,11 +1516,11 @@ 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] = toActions(actPreviousHistory) + if _, prs := opts.Keymap[tui.CtrlP.AsEvent()]; !prs { + opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPreviousHistory) } - if _, prs := opts.Keymap[tui.CtrlN]; !prs { - opts.Keymap[tui.CtrlN] = toActions(actNextHistory) + if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs { + opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory) } } diff --git a/src/options_test.go b/src/options_test.go index 5cc11c8..4311aa4 100644 --- a/src/options_test.go +++ b/src/options_test.go @@ -125,26 +125,29 @@ func TestIrrelevantNth(t *testing.T) { func TestParseKeys(t *testing.T) { pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "") - check := func(i int, s string) { - if pairs[i] != s { - t.Errorf("%s != %s", pairs[i], s) + checkEvent := func(e tui.Event, s string) { + if pairs[e] != s { + t.Errorf("%s != %s", pairs[e], s) } } + check := func(et tui.EventType, s string) { + checkEvent(et.AsEvent(), s) + } if len(pairs) != 12 { t.Error(12) } check(tui.CtrlZ, "ctrl-z") - check(tui.AltZ, "alt-z") check(tui.F2, "f2") - check(tui.AltZ+'@', "@") - check(tui.AltA, "Alt-a") - check(tui.AltZ+'!', "!") - check(tui.CtrlA+'g'-'a', "ctrl-G") - check(tui.AltZ+'J', "J") - check(tui.AltZ+'g', "g") - check(tui.CtrlAltA, "ctrl-alt-a") - check(tui.CtrlAltM, "ALT-enter") - check(tui.AltSpace, "alt-SPACE") + check(tui.CtrlG, "ctrl-G") + checkEvent(tui.AltKey('z'), "alt-z") + checkEvent(tui.Key('@'), "@") + checkEvent(tui.AltKey('a'), "Alt-a") + checkEvent(tui.Key('!'), "!") + checkEvent(tui.Key('J'), "J") + checkEvent(tui.Key('g'), "g") + checkEvent(tui.CtrlAltKey('a'), "ctrl-alt-a") + checkEvent(tui.CtrlAltKey('m'), "ALT-enter") + checkEvent(tui.AltKey(' '), "alt-SPACE") // Synonyms pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") @@ -152,7 +155,7 @@ func TestParseKeys(t *testing.T) { t.Error(9) } check(tui.CtrlM, "Return") - check(tui.AltZ+' ', "space") + checkEvent(tui.Key(' '), "space") check(tui.Tab, "tab") check(tui.BTab, "btab") check(tui.ESC, "esc") @@ -184,63 +187,64 @@ func TestParseKeysWithComma(t *testing.T) { t.Errorf("%d != %d", a, b) } } - check := func(pairs map[int]string, i int, s string) { - if pairs[i] != s { - t.Errorf("%s != %s", pairs[i], s) + check := func(pairs map[tui.Event]string, e tui.Event, s string) { + if pairs[e] != s { + t.Errorf("%s != %s", pairs[e], s) } } pairs := parseKeyChords(",", "") checkN(len(pairs), 1) - check(pairs, tui.AltZ+',', ",") + check(pairs, tui.Key(','), ",") pairs = parseKeyChords(",,a,b", "") checkN(len(pairs), 3) - check(pairs, tui.AltZ+'a', "a") - check(pairs, tui.AltZ+'b', "b") - check(pairs, tui.AltZ+',', ",") + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key(','), ",") pairs = parseKeyChords("a,b,,", "") checkN(len(pairs), 3) - check(pairs, tui.AltZ+'a', "a") - check(pairs, tui.AltZ+'b', "b") - check(pairs, tui.AltZ+',', ",") + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key(','), ",") pairs = parseKeyChords("a,,,b", "") checkN(len(pairs), 3) - check(pairs, tui.AltZ+'a', "a") - check(pairs, tui.AltZ+'b', "b") - check(pairs, tui.AltZ+',', ",") + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key(','), ",") pairs = parseKeyChords("a,,,b,c", "") checkN(len(pairs), 4) - check(pairs, tui.AltZ+'a', "a") - check(pairs, tui.AltZ+'b', "b") - check(pairs, tui.AltZ+'c', "c") - check(pairs, tui.AltZ+',', ",") + check(pairs, tui.Key('a'), "a") + check(pairs, tui.Key('b'), "b") + check(pairs, tui.Key('c'), "c") + check(pairs, tui.Key(','), ",") pairs = parseKeyChords(",,,", "") checkN(len(pairs), 1) - check(pairs, tui.AltZ+',', ",") + check(pairs, tui.Key(','), ",") } func TestBind(t *testing.T) { keymap := defaultKeymap() - 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])) + check := func(event tui.Event, arg1 string, types ...actionType) { + if len(keymap[event]) != len(types) { + t.Errorf("invalid number of actions for %v (%d != %d)", + event, len(types), len(keymap[event])) return } - for idx, action := range keymap[keyName] { + for idx, action := range keymap[event] { 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) + if len(arg1) > 0 && keymap[event][0].a != arg1 { + t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[event][0].a) } } - check(tui.CtrlA, "", actBeginningOfLine) + check(tui.CtrlA.AsEvent(), "", actBeginningOfLine) parseKeymap(keymap, "ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+ @@ -248,29 +252,29 @@ func TestBind(t *testing.T) { "x:Execute(foo+bar),X:execute/bar+baz/"+ ",f1:+first,f1:+top"+ ",,: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, actExecute, actSelectAll, actFirst, actFirst) - 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) + check(tui.CtrlA.AsEvent(), "", actKillLine) + check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown) + check(tui.Key('c'), "", actPageUp) + check(tui.Key(','), "", actAbort) + check(tui.Key(':'), "", actAccept) + check(tui.AltKey('z'), "", actPageDown) + check(tui.F1.AsEvent(), "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actFirst, actFirst) + check(tui.F2.AsEvent(), "echo {}, {}, {}", actExecute) + check(tui.F3.AsEvent(), "echo '({})'", actExecute) + check(tui.F4.AsEvent(), "less {}", actExecute) + check(tui.Key('x'), "foo+bar", actExecute) + check(tui.Key('X'), "bar+baz", actExecute) + check(tui.AltKey('a'), "echo (,),[,],/,:,;,%,{}", actExecuteMulti) + check(tui.AltKey('b'), "echo (,),[,],/,:,@,%,{}", actExecute) + check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute) for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { 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) + check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute) } parseKeymap(keymap, "f1:abort") - check(tui.F1, "", actAbort) + check(tui.F1.AsEvent(), "", actAbort) } func TestColorSpec(t *testing.T) { @@ -314,11 +318,12 @@ func TestColorSpec(t *testing.T) { } func TestDefaultCtrlNP(t *testing.T) { - check := func(words []string, key int, expected actionType) { + check := func(words []string, et tui.EventType, expected actionType) { + e := et.AsEvent() opts := defaultOptions() parseOptions(opts, words) postProcessOptions(opts) - if opts.Keymap[key][0].t != expected { + if opts.Keymap[e][0].t != expected { t.Error() } } diff --git a/src/terminal.go b/src/terminal.go index 2bba9c2..00370b3 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -109,8 +109,8 @@ type Terminal struct { sort bool toggleSort bool delimiter Delimiter - expect map[int]string - keymap map[int][]action + expect map[tui.Event]string + keymap map[tui.Event][]action pressed string printQuery bool history *History @@ -304,62 +304,68 @@ func toActions(types ...actionType) []action { return actions } -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) - if !util.IsWindows() { - keymap[tui.CtrlZ] = toActions(actSigStop) +func defaultKeymap() map[tui.Event][]action { + keymap := make(map[tui.Event][]action) + add := func(e tui.EventType, a actionType) { + keymap[e.AsEvent()] = toActions(a) + } + addEvent := func(e tui.Event, a actionType) { + keymap[e] = toActions(a) } - 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) + add(tui.Invalid, actInvalid) + add(tui.Resize, actClearScreen) + add(tui.CtrlA, actBeginningOfLine) + add(tui.CtrlB, actBackwardChar) + add(tui.CtrlC, actAbort) + add(tui.CtrlG, actAbort) + add(tui.CtrlQ, actAbort) + add(tui.ESC, actAbort) + add(tui.CtrlD, actDeleteCharEOF) + add(tui.CtrlE, actEndOfLine) + add(tui.CtrlF, actForwardChar) + add(tui.CtrlH, actBackwardDeleteChar) + add(tui.BSpace, actBackwardDeleteChar) + add(tui.Tab, actToggleDown) + add(tui.BTab, actToggleUp) + add(tui.CtrlJ, actDown) + add(tui.CtrlK, actUp) + add(tui.CtrlL, actClearScreen) + add(tui.CtrlM, actAccept) + add(tui.CtrlN, actDown) + add(tui.CtrlP, actUp) + add(tui.CtrlU, actUnixLineDiscard) + add(tui.CtrlW, actUnixWordRubout) + add(tui.CtrlY, actYank) + if !util.IsWindows() { + add(tui.CtrlZ, actSigStop) + } - keymap[tui.Up] = toActions(actUp) - keymap[tui.Down] = toActions(actDown) - keymap[tui.Left] = toActions(actBackwardChar) - keymap[tui.Right] = toActions(actForwardChar) + addEvent(tui.AltKey('b'), actBackwardWord) + add(tui.SLeft, actBackwardWord) + addEvent(tui.AltKey('f'), actForwardWord) + add(tui.SRight, actForwardWord) + addEvent(tui.AltKey('d'), actKillWord) + add(tui.AltBS, actBackwardKillWord) - 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) + add(tui.Up, actUp) + add(tui.Down, actDown) + add(tui.Left, actBackwardChar) + add(tui.Right, actForwardChar) - keymap[tui.SUp] = toActions(actPreviewUp) - keymap[tui.SDown] = toActions(actPreviewDown) + add(tui.Home, actBeginningOfLine) + add(tui.End, actEndOfLine) + add(tui.Del, actDeleteChar) + add(tui.PgUp, actPageUp) + add(tui.PgDn, actPageDown) - keymap[tui.Rune] = toActions(actRune) - keymap[tui.Mouse] = toActions(actMouse) - keymap[tui.DoubleClick] = toActions(actAccept) - keymap[tui.LeftClick] = toActions(actIgnore) - keymap[tui.RightClick] = toActions(actToggle) + add(tui.SUp, actPreviewUp) + add(tui.SDown, actPreviewDown) + + add(tui.Mouse, actMouse) + add(tui.DoubleClick, actAccept) + add(tui.LeftClick, actIgnore) + add(tui.RightClick, actToggle) return keymap } @@ -1452,10 +1458,9 @@ func (t *Terminal) rubout(pattern string) { t.input = append(t.input[:t.cx], after...) } -func keyMatch(key int, event tui.Event) bool { - return event.Type == key || - event.Type == tui.Rune && int(event.Char) == key-tui.AltZ || - event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double +func keyMatch(key tui.Event, event tui.Event) bool { + return event.Type == key.Type && event.Char == key.Char || + key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double } func quoteEntryCmd(entry string) string { @@ -2163,16 +2168,20 @@ func (t *Terminal) Loop() { } } - var doAction func(action, int) bool - doActions := func(actions []action, mapkey int) bool { + actionsFor := func(eventType tui.EventType) []action { + return t.keymap[eventType.AsEvent()] + } + + var doAction func(action) bool + doActions := func(actions []action) bool { for _, action := range actions { - if !doAction(action, mapkey) { + if !doAction(action) { return false } } return true } - doAction = func(a action, mapkey int) bool { + doAction = func(a action) bool { switch a.t { case actIgnore: case actExecute, actExecuteSilent: @@ -2326,14 +2335,14 @@ func (t *Terminal) Loop() { } case actToggleIn: if t.layout != layoutDefault { - return doAction(action{t: actToggleUp}, mapkey) + return doAction(action{t: actToggleUp}) } - return doAction(action{t: actToggleDown}, mapkey) + return doAction(action{t: actToggleDown}) case actToggleOut: if t.layout != layoutDefault { - return doAction(action{t: actToggleDown}, mapkey) + return doAction(action{t: actToggleDown}) } - return doAction(action{t: actToggleUp}, mapkey) + return doAction(action{t: actToggleUp}) case actToggleDown: if t.multi > 0 && t.merger.Length() > 0 && toggle() { t.vmove(-1, true) @@ -2490,7 +2499,7 @@ func (t *Terminal) Loop() { // Double-click if my >= min { if t.vset(t.offset+my-min) && t.cy < t.merger.Length() { - return doActions(t.keymap[tui.DoubleClick], tui.DoubleClick) + return doActions(actionsFor(tui.DoubleClick)) } } } else if me.Down { @@ -2504,9 +2513,9 @@ func (t *Terminal) Loop() { } req(reqList) if me.Left { - return doActions(t.keymap[tui.LeftClick], tui.LeftClick) + return doActions(actionsFor(tui.LeftClick)) } - return doActions(t.keymap[tui.RightClick], tui.RightClick) + return doActions(actionsFor(tui.RightClick)) } } } @@ -2528,33 +2537,29 @@ func (t *Terminal) Loop() { } return true } - mapkey := event.Type + if t.jumping == jumpDisabled { - actions := t.keymap[mapkey] - if mapkey == tui.Rune { - mapkey = int(event.Char) + int(tui.AltZ) - if act, prs := t.keymap[mapkey]; prs { - actions = act - } - } - if !doActions(actions, mapkey) { + actions := t.keymap[event.Comparable()] + if len(actions) == 0 && event.Type == tui.Rune { + doAction(action{t: actRune}) + } else if !doActions(actions) { continue } t.truncateQuery() queryChanged = string(previousInput) != string(t.input) changed = changed || queryChanged - if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs { - if !doActions(onChanges, tui.Change) { + if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs { + if !doActions(onChanges) { continue } } - if onEOFs, prs := t.keymap[tui.BackwardEOF]; beof && prs { - if !doActions(onEOFs, tui.BackwardEOF) { + if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs { + if !doActions(onEOFs) { continue } } } else { - if mapkey == tui.Rune { + if event.Type == tui.Rune { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() { t.cy = idx + t.offset if t.jumping == jumpAcceptEnabled { diff --git a/src/tui/light.go b/src/tui/light.go index 4f50d68..9c48b46 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -1,6 +1,7 @@ package tui import ( + "bytes" "fmt" "os" "regexp" @@ -230,7 +231,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { } retries := 0 - if c == ESC || nonblock { + if c == ESC.Int() || nonblock { retries = r.escDelay / escPollInterval } buffer = append(buffer, byte(c)) @@ -245,7 +246,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte { continue } break - } else if c == ESC && pc != c { + } else if c == ESC.Int() && pc != c { retries = r.escDelay / escPollInterval } else { retries = 0 @@ -278,11 +279,11 @@ func (r *LightRenderer) GetChar() Event { }() switch r.buffer[0] { - case CtrlC: + case CtrlC.Byte(): return Event{CtrlC, 0, nil} - case CtrlG: + case CtrlG.Byte(): return Event{CtrlG, 0, nil} - case CtrlQ: + case CtrlQ.Byte(): return Event{CtrlQ, 0, nil} case 127: return Event{BSpace, 0, nil} @@ -296,7 +297,7 @@ func (r *LightRenderer) GetChar() Event { return Event{CtrlCaret, 0, nil} case 31: return Event{CtrlSlash, 0, nil} - case ESC: + case ESC.Byte(): ev := r.escSequence(&sz) // Second chance if ev.Type == Invalid { @@ -307,8 +308,8 @@ func (r *LightRenderer) GetChar() Event { } // CTRL-A ~ CTRL-Z - if r.buffer[0] <= CtrlZ { - return Event{int(r.buffer[0]), 0, nil} + if r.buffer[0] <= CtrlZ.Byte() { + return Event{EventType(r.buffer[0]), 0, nil} } char, rsz := utf8.DecodeRune(r.buffer) if char == utf8.RuneError { @@ -331,26 +332,16 @@ func (r *LightRenderer) escSequence(sz *int) Event { *sz = 2 if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { - return Event{int(CtrlAltA + r.buffer[1] - 1), 0, nil} + return CtrlAltKey(rune(r.buffer[1] + 'a' - 1)) } alt := false - if len(r.buffer) > 2 && r.buffer[1] == ESC { + if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() { r.buffer = r.buffer[1:] alt = true } switch r.buffer[1] { - case ESC: + case ESC.Byte(): return Event{ESC, 0, nil} - case ' ': - return Event{AltSpace, 0, nil} - case '/': - return Event{AltSlash, 0, nil} - case 'b': - return Event{AltB, 0, nil} - case 'd': - return Event{AltD, 0, nil} - case 'f': - return Event{AltF, 0, nil} case 127: return Event{AltBS, 0, nil} case '[', 'O': @@ -518,11 +509,11 @@ func (r *LightRenderer) escSequence(sz *int) Event { } // r.buffer[2] } // r.buffer[2] } // r.buffer[1] - if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' { - return Event{AltA + int(r.buffer[1]) - 'a', 0, nil} - } - if r.buffer[1] >= '0' && r.buffer[1] <= '9' { - return Event{Alt0 + int(r.buffer[1]) - '0', 0, nil} + rest := bytes.NewBuffer(r.buffer[1:]) + c, size, err := rest.ReadRune() + if err == nil { + *sz = 1 + size + return AltKey(c) } return Event{Invalid, 0, nil} } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 16d3151..938c1ba 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -226,65 +226,65 @@ func (r *FullscreenRenderer) GetChar() Event { alt := (mods & tcell.ModAlt) > 0 shift := (mods & tcell.ModShift) > 0 altShift := alt && shift - keyfn := func(r rune) int { + keyfn := func(r rune) Event { if alt { - return CtrlAltA - 'a' + int(r) + return CtrlAltKey(r) } - return CtrlA - 'a' + int(r) + return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent() } switch ev.Key() { case tcell.KeyCtrlA: - return Event{keyfn('a'), 0, nil} + return keyfn('a') case tcell.KeyCtrlB: - return Event{keyfn('b'), 0, nil} + return keyfn('b') case tcell.KeyCtrlC: - return Event{keyfn('c'), 0, nil} + return keyfn('c') case tcell.KeyCtrlD: - return Event{keyfn('d'), 0, nil} + return keyfn('d') case tcell.KeyCtrlE: - return Event{keyfn('e'), 0, nil} + return keyfn('e') case tcell.KeyCtrlF: - return Event{keyfn('f'), 0, nil} + return keyfn('f') case tcell.KeyCtrlG: - return Event{keyfn('g'), 0, nil} + return keyfn('g') case tcell.KeyCtrlH: - return Event{keyfn('h'), 0, nil} + return keyfn('h') case tcell.KeyCtrlI: - return Event{keyfn('i'), 0, nil} + return keyfn('i') case tcell.KeyCtrlJ: - return Event{keyfn('j'), 0, nil} + return keyfn('j') case tcell.KeyCtrlK: - return Event{keyfn('k'), 0, nil} + return keyfn('k') case tcell.KeyCtrlL: - return Event{keyfn('l'), 0, nil} + return keyfn('l') case tcell.KeyCtrlM: - return Event{keyfn('m'), 0, nil} + return keyfn('m') case tcell.KeyCtrlN: - return Event{keyfn('n'), 0, nil} + return keyfn('n') case tcell.KeyCtrlO: - return Event{keyfn('o'), 0, nil} + return keyfn('o') case tcell.KeyCtrlP: - return Event{keyfn('p'), 0, nil} + return keyfn('p') case tcell.KeyCtrlQ: - return Event{keyfn('q'), 0, nil} + return keyfn('q') case tcell.KeyCtrlR: - return Event{keyfn('r'), 0, nil} + return keyfn('r') case tcell.KeyCtrlS: - return Event{keyfn('s'), 0, nil} + return keyfn('s') case tcell.KeyCtrlT: - return Event{keyfn('t'), 0, nil} + return keyfn('t') case tcell.KeyCtrlU: - return Event{keyfn('u'), 0, nil} + return keyfn('u') case tcell.KeyCtrlV: - return Event{keyfn('v'), 0, nil} + return keyfn('v') case tcell.KeyCtrlW: - return Event{keyfn('w'), 0, nil} + return keyfn('w') case tcell.KeyCtrlX: - return Event{keyfn('x'), 0, nil} + return keyfn('x') case tcell.KeyCtrlY: - return Event{keyfn('y'), 0, nil} + return keyfn('y') case tcell.KeyCtrlZ: - return Event{keyfn('z'), 0, nil} + return keyfn('z') case tcell.KeyCtrlSpace: return Event{CtrlSpace, 0, nil} case tcell.KeyCtrlBackslash: @@ -389,18 +389,7 @@ func (r *FullscreenRenderer) GetChar() Event { case tcell.KeyRune: r := ev.Rune() if alt { - switch r { - case ' ': - return Event{AltSpace, 0, nil} - case '/': - return Event{AltSlash, 0, nil} - } - if r >= 'a' && r <= 'z' { - return Event{AltA + int(r) - 'a', 0, nil} - } - if r >= '0' && r <= '9' { - return Event{Alt0 + int(r) - '0', 0, nil} - } + return AltKey(r) } return Event{Rune, r, nil} diff --git a/src/tui/tui.go b/src/tui/tui.go index edbc9a1..cc9c7f6 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -8,8 +8,10 @@ import ( ) // Types of user action +type EventType int + const ( - Rune = iota + Rune EventType = iota CtrlA CtrlB @@ -89,8 +91,6 @@ const ( Change BackwardEOF - AltSpace - AltSlash AltBS AltUp @@ -103,20 +103,38 @@ const ( AltSLeft AltSRight - Alt0 + Alt + CtrlAlt ) -const ( // Reset iota - AltA = Alt0 + 'a' - '0' + iota - AltB - AltC - AltD - AltE - AltF - AltZ = AltA + 'z' - 'a' - CtrlAltA = AltZ + 1 - CtrlAltM = CtrlAltA + 'm' - 'a' -) +func (t EventType) AsEvent() Event { + return Event{t, 0, nil} +} + +func (t EventType) Int() int { + return int(t) +} + +func (t EventType) Byte() byte { + return byte(t) +} + +func (e Event) Comparable() Event { + // Ignore MouseEvent pointer + return Event{e.Type, e.Char, nil} +} + +func Key(r rune) Event { + return Event{Rune, r, nil} +} + +func AltKey(r rune) Event { + return Event{Alt, r, nil} +} + +func CtrlAltKey(r rune) Event { + return Event{CtrlAlt, r, nil} +} const ( doubleClickDuration = 500 * time.Millisecond @@ -251,7 +269,7 @@ type ColorTheme struct { } type Event struct { - Type int + Type EventType Char rune MouseEvent *MouseEvent }