Extend support for alt key chords

"alt-" with any case-sensitive character is allowed
pull/2305/head
Junegunn Choi 3 years ago
parent 0de7ab18f6
commit 7f8e0dbc40
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

@ -26,6 +26,10 @@ CHANGELOG
- Added `last` action to move the cursor to the last match - Added `last` action to move the cursor to the last match
- The opposite action `top` is renamed to `first`, but `top` is still - The opposite action `top` is renamed to `first`, but `top` is still
recognized as a synonym for backward compatibility 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 0.24.4
------ ------

@ -666,9 +666,7 @@ e.g.
.br .br
\fIctrl-alt-[a-z]\fR \fIctrl-alt-[a-z]\fR
.br .br
\fIalt-[a-z]\fR \fIalt-[*]\fR (Any case-sensitive single character is allowed)
.br
\fIalt-[0-9]\fR
.br .br
\fIf[1-12]\fR \fIf[1-12]\fR
.br .br
@ -692,8 +690,6 @@ e.g.
.br .br
\fIalt-bspace\fR (\fIalt-bs\fR) \fIalt-bspace\fR (\fIalt-bs\fR)
.br .br
\fIalt-/\fR
.br
\fItab\fR \fItab\fR
.br .br
\fIbtab\fR (\fIshift-tab\fR) \fIbtab\fR (\fIshift-tab\fR)

@ -7,7 +7,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"unicode" "unicode"
"unicode/utf8"
"github.com/junegunn/fzf/src/algo" "github.com/junegunn/fzf/src/algo"
"github.com/junegunn/fzf/src/tui" "github.com/junegunn/fzf/src/tui"
@ -211,8 +210,8 @@ type Options struct {
Exit0 bool Exit0 bool
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[int]string Expect map[tui.Event]string
Keymap map[int][]action Keymap map[tui.Event][]action
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
@ -272,8 +271,8 @@ func defaultOptions() *Options {
Exit0: false, Exit0: false,
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[int]string), Expect: make(map[tui.Event]string),
Keymap: make(map[int][]action), Keymap: make(map[tui.Event][]action),
Preview: defaultPreviewOpts(""), Preview: defaultPreviewOpts(""),
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@ -445,7 +444,7 @@ func parseBorder(str string, optional bool) tui.BorderShape {
return tui.BorderNone 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 { if len(str) == 0 {
errorExit(message) errorExit(message)
} }
@ -455,124 +454,129 @@ func parseKeyChords(str string, message string) map[int]string {
tokens = append(tokens, ",") tokens = append(tokens, ",")
} }
chords := make(map[int]string) chords := make(map[tui.Event]string)
for _, key := range tokens { for _, key := range tokens {
if len(key) == 0 { if len(key) == 0 {
continue // ignore continue // ignore
} }
lkey := strings.ToLower(key) lkey := strings.ToLower(key)
chord := 0 add := func(e tui.EventType) {
chords[e.AsEvent()] = key
}
switch lkey { switch lkey {
case "up": case "up":
chord = tui.Up add(tui.Up)
case "down": case "down":
chord = tui.Down add(tui.Down)
case "left": case "left":
chord = tui.Left add(tui.Left)
case "right": case "right":
chord = tui.Right add(tui.Right)
case "enter", "return": case "enter", "return":
chord = tui.CtrlM add(tui.CtrlM)
case "space": case "space":
chord = tui.AltZ + int(' ') chords[tui.Key(' ')] = key
case "bspace", "bs": case "bspace", "bs":
chord = tui.BSpace add(tui.BSpace)
case "ctrl-space": case "ctrl-space":
chord = tui.CtrlSpace add(tui.CtrlSpace)
case "ctrl-^", "ctrl-6": case "ctrl-^", "ctrl-6":
chord = tui.CtrlCaret add(tui.CtrlCaret)
case "ctrl-/", "ctrl-_": case "ctrl-/", "ctrl-_":
chord = tui.CtrlSlash add(tui.CtrlSlash)
case "ctrl-\\": case "ctrl-\\":
chord = tui.CtrlBackSlash add(tui.CtrlBackSlash)
case "ctrl-]": case "ctrl-]":
chord = tui.CtrlRightBracket add(tui.CtrlRightBracket)
case "change": case "change":
chord = tui.Change add(tui.Change)
case "backward-eof": case "backward-eof":
chord = tui.BackwardEOF add(tui.BackwardEOF)
case "alt-enter", "alt-return": case "alt-enter", "alt-return":
chord = tui.CtrlAltM chords[tui.CtrlAltKey('m')] = key
case "alt-space": case "alt-space":
chord = tui.AltSpace chords[tui.AltKey(' ')] = key
case "alt-/":
chord = tui.AltSlash
case "alt-bs", "alt-bspace": case "alt-bs", "alt-bspace":
chord = tui.AltBS add(tui.AltBS)
case "alt-up": case "alt-up":
chord = tui.AltUp add(tui.AltUp)
case "alt-down": case "alt-down":
chord = tui.AltDown add(tui.AltDown)
case "alt-left": case "alt-left":
chord = tui.AltLeft add(tui.AltLeft)
case "alt-right": case "alt-right":
chord = tui.AltRight add(tui.AltRight)
case "tab": case "tab":
chord = tui.Tab add(tui.Tab)
case "btab", "shift-tab": case "btab", "shift-tab":
chord = tui.BTab add(tui.BTab)
case "esc": case "esc":
chord = tui.ESC add(tui.ESC)
case "del": case "del":
chord = tui.Del add(tui.Del)
case "home": case "home":
chord = tui.Home add(tui.Home)
case "end": case "end":
chord = tui.End add(tui.End)
case "insert": case "insert":
chord = tui.Insert add(tui.Insert)
case "pgup", "page-up": case "pgup", "page-up":
chord = tui.PgUp add(tui.PgUp)
case "pgdn", "page-down": case "pgdn", "page-down":
chord = tui.PgDn add(tui.PgDn)
case "alt-shift-up", "shift-alt-up": case "alt-shift-up", "shift-alt-up":
chord = tui.AltSUp add(tui.AltSUp)
case "alt-shift-down", "shift-alt-down": case "alt-shift-down", "shift-alt-down":
chord = tui.AltSDown add(tui.AltSDown)
case "alt-shift-left", "shift-alt-left": case "alt-shift-left", "shift-alt-left":
chord = tui.AltSLeft add(tui.AltSLeft)
case "alt-shift-right", "shift-alt-right": case "alt-shift-right", "shift-alt-right":
chord = tui.AltSRight add(tui.AltSRight)
case "shift-up": case "shift-up":
chord = tui.SUp add(tui.SUp)
case "shift-down": case "shift-down":
chord = tui.SDown add(tui.SDown)
case "shift-left": case "shift-left":
chord = tui.SLeft add(tui.SLeft)
case "shift-right": case "shift-right":
chord = tui.SRight add(tui.SRight)
case "left-click": case "left-click":
chord = tui.LeftClick add(tui.LeftClick)
case "right-click": case "right-click":
chord = tui.RightClick add(tui.RightClick)
case "double-click": case "double-click":
chord = tui.DoubleClick add(tui.DoubleClick)
case "f10": case "f10":
chord = tui.F10 add(tui.F10)
case "f11": case "f11":
chord = tui.F11 add(tui.F11)
case "f12": case "f12":
chord = tui.F12 add(tui.F12)
default: default:
runes := []rune(key)
if len(key) == 10 && strings.HasPrefix(lkey, "ctrl-alt-") && isAlphabet(lkey[9]) { 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]) { } else if len(key) == 6 && strings.HasPrefix(lkey, "ctrl-") && isAlphabet(lkey[5]) {
chord = tui.CtrlA + int(lkey[5]) - 'a' add(tui.EventType(tui.CtrlA.Int() + int(lkey[5]) - 'a'))
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isAlphabet(lkey[4]) { } else if len(runes) == 5 && strings.HasPrefix(lkey, "alt-") {
chord = tui.AltA + int(lkey[4]) - 'a' r := runes[4]
} else if len(key) == 5 && strings.HasPrefix(lkey, "alt-") && isNumeric(lkey[4]) { switch r {
chord = tui.Alt0 + int(lkey[4]) - '0' 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' { } else if len(key) == 2 && strings.HasPrefix(lkey, "f") && key[1] >= '1' && key[1] <= '9' {
chord = tui.F1 + int(key[1]) - '1' add(tui.EventType(tui.F1.Int() + int(key[1]) - '1'))
} else if utf8.RuneCountInString(key) == 1 { } else if len(runes) == 1 {
chord = tui.AltZ + int([]rune(key)[0]) chords[tui.Key(runes[0])] = key
} else { } else {
errorExit("unsupported key: " + key) errorExit("unsupported key: " + key)
} }
} }
if chord > 0 {
chords[chord] = key
}
} }
return chords return chords
} }
@ -720,11 +724,11 @@ func parseTheme(defaultTheme *tui.ColorTheme, str string) *tui.ColorTheme {
var executeRegexp *regexp.Regexp var executeRegexp *regexp.Regexp
func firstKey(keymap map[int]string) int { func firstKey(keymap map[tui.Event]string) tui.Event {
for k := range keymap { for k := range keymap {
return k return k
} }
return 0 return tui.EventType(0).AsEvent()
} }
const ( const (
@ -740,7 +744,7 @@ func init() {
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?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 { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
symbol := ":" symbol := ":"
if strings.HasPrefix(src, "+") { if strings.HasPrefix(src, "+") {
@ -776,13 +780,13 @@ func parseKeymap(keymap map[int][]action, str string) {
if len(pair) < 2 { if len(pair) < 2 {
errorExit("bind action not specified: " + origPairStr) errorExit("bind action not specified: " + origPairStr)
} }
var key int var key tui.Event
if len(pair[0]) == 1 && pair[0][0] == escapedColon { 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 { } 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 { } else if len(pair[0]) == 1 && pair[0][0] == escapedPlus {
key = '+' + tui.AltZ key = tui.Key('+')
} else { } else {
keys := parseKeyChords(pair[0], "key name required") keys := parseKeyChords(pair[0], "key name required")
key = firstKey(keys) key = firstKey(keys)
@ -981,7 +985,7 @@ func isExecuteAction(str string) actionType {
return actIgnore 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") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
@ -1188,7 +1192,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Expect[k] = v opts.Expect[k] = v
} }
case "--no-expect": case "--no-expect":
opts.Expect = make(map[int]string) opts.Expect = make(map[tui.Event]string)
case "--no-phony": case "--no-phony":
opts.Phony = false opts.Phony = false
case "--phony": case "--phony":
@ -1512,11 +1516,11 @@ 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.AsEvent()]; !prs {
opts.Keymap[tui.CtrlP] = toActions(actPreviousHistory) opts.Keymap[tui.CtrlP.AsEvent()] = toActions(actPreviousHistory)
} }
if _, prs := opts.Keymap[tui.CtrlN]; !prs { if _, prs := opts.Keymap[tui.CtrlN.AsEvent()]; !prs {
opts.Keymap[tui.CtrlN] = toActions(actNextHistory) opts.Keymap[tui.CtrlN.AsEvent()] = toActions(actNextHistory)
} }
} }

@ -125,26 +125,29 @@ func TestIrrelevantNth(t *testing.T) {
func TestParseKeys(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", "") 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) { checkEvent := func(e tui.Event, s string) {
if pairs[i] != s { if pairs[e] != s {
t.Errorf("%s != %s", pairs[i], s) t.Errorf("%s != %s", pairs[e], s)
} }
} }
check := func(et tui.EventType, s string) {
checkEvent(et.AsEvent(), s)
}
if len(pairs) != 12 { if len(pairs) != 12 {
t.Error(12) t.Error(12)
} }
check(tui.CtrlZ, "ctrl-z") check(tui.CtrlZ, "ctrl-z")
check(tui.AltZ, "alt-z")
check(tui.F2, "f2") check(tui.F2, "f2")
check(tui.AltZ+'@', "@") check(tui.CtrlG, "ctrl-G")
check(tui.AltA, "Alt-a") checkEvent(tui.AltKey('z'), "alt-z")
check(tui.AltZ+'!', "!") checkEvent(tui.Key('@'), "@")
check(tui.CtrlA+'g'-'a', "ctrl-G") checkEvent(tui.AltKey('a'), "Alt-a")
check(tui.AltZ+'J', "J") checkEvent(tui.Key('!'), "!")
check(tui.AltZ+'g', "g") checkEvent(tui.Key('J'), "J")
check(tui.CtrlAltA, "ctrl-alt-a") checkEvent(tui.Key('g'), "g")
check(tui.CtrlAltM, "ALT-enter") checkEvent(tui.CtrlAltKey('a'), "ctrl-alt-a")
check(tui.AltSpace, "alt-SPACE") checkEvent(tui.CtrlAltKey('m'), "ALT-enter")
checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms // Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "") pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
@ -152,7 +155,7 @@ func TestParseKeys(t *testing.T) {
t.Error(9) t.Error(9)
} }
check(tui.CtrlM, "Return") check(tui.CtrlM, "Return")
check(tui.AltZ+' ', "space") checkEvent(tui.Key(' '), "space")
check(tui.Tab, "tab") check(tui.Tab, "tab")
check(tui.BTab, "btab") check(tui.BTab, "btab")
check(tui.ESC, "esc") check(tui.ESC, "esc")
@ -184,63 +187,64 @@ func TestParseKeysWithComma(t *testing.T) {
t.Errorf("%d != %d", a, b) t.Errorf("%d != %d", a, b)
} }
} }
check := func(pairs map[int]string, i int, s string) { check := func(pairs map[tui.Event]string, e tui.Event, s string) {
if pairs[i] != s { if pairs[e] != s {
t.Errorf("%s != %s", pairs[i], s) t.Errorf("%s != %s", pairs[e], s)
} }
} }
pairs := parseKeyChords(",", "") pairs := parseKeyChords(",", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.AltZ+',', ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,a,b", "") pairs = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.AltZ+'a', "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.AltZ+'b', "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.AltZ+',', ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,b,,", "") pairs = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.AltZ+'a', "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.AltZ+'b', "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.AltZ+',', ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b", "") pairs = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3) checkN(len(pairs), 3)
check(pairs, tui.AltZ+'a', "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.AltZ+'b', "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.AltZ+',', ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b,c", "") pairs = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4) checkN(len(pairs), 4)
check(pairs, tui.AltZ+'a', "a") check(pairs, tui.Key('a'), "a")
check(pairs, tui.AltZ+'b', "b") check(pairs, tui.Key('b'), "b")
check(pairs, tui.AltZ+'c', "c") check(pairs, tui.Key('c'), "c")
check(pairs, tui.AltZ+',', ",") check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,,", "") pairs = parseKeyChords(",,,", "")
checkN(len(pairs), 1) checkN(len(pairs), 1)
check(pairs, tui.AltZ+',', ",") check(pairs, tui.Key(','), ",")
} }
func TestBind(t *testing.T) { func TestBind(t *testing.T) {
keymap := defaultKeymap() keymap := defaultKeymap()
check := func(keyName int, arg1 string, types ...actionType) { check := func(event tui.Event, arg1 string, types ...actionType) {
if len(keymap[keyName]) != len(types) { if len(keymap[event]) != len(types) {
t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName])) t.Errorf("invalid number of actions for %v (%d != %d)",
event, len(types), len(keymap[event]))
return return
} }
for idx, action := range keymap[keyName] { for idx, action := range keymap[event] {
if types[idx] != action.t { if types[idx] != action.t {
t.Errorf("invalid action type (%d != %d)", types[idx], action.t) t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
} }
} }
if len(arg1) > 0 && keymap[keyName][0].a != arg1 { if len(arg1) > 0 && keymap[event][0].a != arg1 {
t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a) t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[event][0].a)
} }
} }
check(tui.CtrlA, "", actBeginningOfLine) check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
parseKeymap(keymap, parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+ "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 {};,"+ "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/"+ "x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+ ",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up") ",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA, "", actKillLine) check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB, "", actToggleSort, actUp, actDown) check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.AltZ+'c', "", actPageUp) check(tui.Key('c'), "", actPageUp)
check(tui.AltZ+',', "", actAbort) check(tui.Key(','), "", actAbort)
check(tui.AltZ+':', "", actAccept) check(tui.Key(':'), "", actAccept)
check(tui.AltZ, "", actPageDown) check(tui.AltKey('z'), "", actPageDown)
check(tui.F1, "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actFirst, actFirst) check(tui.F1.AsEvent(), "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actFirst, actFirst)
check(tui.F2, "echo {}, {}, {}", actExecute) check(tui.F2.AsEvent(), "echo {}, {}, {}", actExecute)
check(tui.F3, "echo '({})'", actExecute) check(tui.F3.AsEvent(), "echo '({})'", actExecute)
check(tui.F4, "less {}", actExecute) check(tui.F4.AsEvent(), "less {}", actExecute)
check(tui.AltZ+'x', "foo+bar", actExecute) check(tui.Key('x'), "foo+bar", actExecute)
check(tui.AltZ+'X', "bar+baz", actExecute) check(tui.Key('X'), "bar+baz", actExecute)
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti) check(tui.AltKey('a'), "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute) check(tui.AltKey('b'), "echo (,),[,],/,:,@,%,{}", actExecute)
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute) check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} { for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char)) 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") parseKeymap(keymap, "f1:abort")
check(tui.F1, "", actAbort) check(tui.F1.AsEvent(), "", actAbort)
} }
func TestColorSpec(t *testing.T) { func TestColorSpec(t *testing.T) {
@ -314,11 +318,12 @@ func TestColorSpec(t *testing.T) {
} }
func TestDefaultCtrlNP(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() opts := defaultOptions()
parseOptions(opts, words) parseOptions(opts, words)
postProcessOptions(opts) postProcessOptions(opts)
if opts.Keymap[key][0].t != expected { if opts.Keymap[e][0].t != expected {
t.Error() t.Error()
} }
} }

@ -109,8 +109,8 @@ type Terminal struct {
sort bool sort bool
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[int]string expect map[tui.Event]string
keymap map[int][]action keymap map[tui.Event][]action
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
@ -304,62 +304,68 @@ func toActions(types ...actionType) []action {
return actions return actions
} }
func defaultKeymap() map[int][]action { func defaultKeymap() map[tui.Event][]action {
keymap := make(map[int][]action) keymap := make(map[tui.Event][]action)
keymap[tui.Invalid] = toActions(actInvalid) add := func(e tui.EventType, a actionType) {
keymap[tui.Resize] = toActions(actClearScreen) keymap[e.AsEvent()] = toActions(a)
keymap[tui.CtrlA] = toActions(actBeginningOfLine) }
keymap[tui.CtrlB] = toActions(actBackwardChar) addEvent := func(e tui.Event, a actionType) {
keymap[tui.CtrlC] = toActions(actAbort) keymap[e] = toActions(a)
keymap[tui.CtrlG] = toActions(actAbort) }
keymap[tui.CtrlQ] = toActions(actAbort)
keymap[tui.ESC] = toActions(actAbort) add(tui.Invalid, actInvalid)
keymap[tui.CtrlD] = toActions(actDeleteCharEOF) add(tui.Resize, actClearScreen)
keymap[tui.CtrlE] = toActions(actEndOfLine) add(tui.CtrlA, actBeginningOfLine)
keymap[tui.CtrlF] = toActions(actForwardChar) add(tui.CtrlB, actBackwardChar)
keymap[tui.CtrlH] = toActions(actBackwardDeleteChar) add(tui.CtrlC, actAbort)
keymap[tui.BSpace] = toActions(actBackwardDeleteChar) add(tui.CtrlG, actAbort)
keymap[tui.Tab] = toActions(actToggleDown) add(tui.CtrlQ, actAbort)
keymap[tui.BTab] = toActions(actToggleUp) add(tui.ESC, actAbort)
keymap[tui.CtrlJ] = toActions(actDown) add(tui.CtrlD, actDeleteCharEOF)
keymap[tui.CtrlK] = toActions(actUp) add(tui.CtrlE, actEndOfLine)
keymap[tui.CtrlL] = toActions(actClearScreen) add(tui.CtrlF, actForwardChar)
keymap[tui.CtrlM] = toActions(actAccept) add(tui.CtrlH, actBackwardDeleteChar)
keymap[tui.CtrlN] = toActions(actDown) add(tui.BSpace, actBackwardDeleteChar)
keymap[tui.CtrlP] = toActions(actUp) add(tui.Tab, actToggleDown)
keymap[tui.CtrlU] = toActions(actUnixLineDiscard) add(tui.BTab, actToggleUp)
keymap[tui.CtrlW] = toActions(actUnixWordRubout) add(tui.CtrlJ, actDown)
keymap[tui.CtrlY] = toActions(actYank) 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() { if !util.IsWindows() {
keymap[tui.CtrlZ] = toActions(actSigStop) add(tui.CtrlZ, actSigStop)
} }
keymap[tui.AltB] = toActions(actBackwardWord) addEvent(tui.AltKey('b'), actBackwardWord)
keymap[tui.SLeft] = toActions(actBackwardWord) add(tui.SLeft, actBackwardWord)
keymap[tui.AltF] = toActions(actForwardWord) addEvent(tui.AltKey('f'), actForwardWord)
keymap[tui.SRight] = toActions(actForwardWord) add(tui.SRight, actForwardWord)
keymap[tui.AltD] = toActions(actKillWord) addEvent(tui.AltKey('d'), actKillWord)
keymap[tui.AltBS] = toActions(actBackwardKillWord) add(tui.AltBS, actBackwardKillWord)
keymap[tui.Up] = toActions(actUp) add(tui.Up, actUp)
keymap[tui.Down] = toActions(actDown) add(tui.Down, actDown)
keymap[tui.Left] = toActions(actBackwardChar) add(tui.Left, actBackwardChar)
keymap[tui.Right] = toActions(actForwardChar) add(tui.Right, actForwardChar)
keymap[tui.Home] = toActions(actBeginningOfLine) add(tui.Home, actBeginningOfLine)
keymap[tui.End] = toActions(actEndOfLine) add(tui.End, actEndOfLine)
keymap[tui.Del] = toActions(actDeleteChar) add(tui.Del, actDeleteChar)
keymap[tui.PgUp] = toActions(actPageUp) add(tui.PgUp, actPageUp)
keymap[tui.PgDn] = toActions(actPageDown) add(tui.PgDn, actPageDown)
keymap[tui.SUp] = toActions(actPreviewUp) add(tui.SUp, actPreviewUp)
keymap[tui.SDown] = toActions(actPreviewDown) add(tui.SDown, actPreviewDown)
keymap[tui.Rune] = toActions(actRune) add(tui.Mouse, actMouse)
keymap[tui.Mouse] = toActions(actMouse) add(tui.DoubleClick, actAccept)
keymap[tui.DoubleClick] = toActions(actAccept) add(tui.LeftClick, actIgnore)
keymap[tui.LeftClick] = toActions(actIgnore) add(tui.RightClick, actToggle)
keymap[tui.RightClick] = toActions(actToggle)
return keymap return keymap
} }
@ -1452,10 +1458,9 @@ func (t *Terminal) rubout(pattern string) {
t.input = append(t.input[:t.cx], after...) t.input = append(t.input[:t.cx], after...)
} }
func keyMatch(key int, event tui.Event) bool { func keyMatch(key tui.Event, event tui.Event) bool {
return event.Type == key || return event.Type == key.Type && event.Char == key.Char ||
event.Type == tui.Rune && int(event.Char) == key-tui.AltZ || key.Type == tui.DoubleClick && event.Type == tui.Mouse && event.MouseEvent.Double
event.Type == tui.Mouse && key == tui.DoubleClick && event.MouseEvent.Double
} }
func quoteEntryCmd(entry string) string { func quoteEntryCmd(entry string) string {
@ -2163,16 +2168,20 @@ func (t *Terminal) Loop() {
} }
} }
var doAction func(action, int) bool actionsFor := func(eventType tui.EventType) []action {
doActions := func(actions []action, mapkey int) bool { return t.keymap[eventType.AsEvent()]
}
var doAction func(action) bool
doActions := func(actions []action) bool {
for _, action := range actions { for _, action := range actions {
if !doAction(action, mapkey) { if !doAction(action) {
return false return false
} }
} }
return true return true
} }
doAction = func(a action, mapkey int) bool { doAction = func(a action) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore:
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
@ -2326,14 +2335,14 @@ func (t *Terminal) Loop() {
} }
case actToggleIn: case actToggleIn:
if t.layout != layoutDefault { 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: case actToggleOut:
if t.layout != layoutDefault { 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: case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() { if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true) t.vmove(-1, true)
@ -2490,7 +2499,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 doActions(t.keymap[tui.DoubleClick], tui.DoubleClick) return doActions(actionsFor(tui.DoubleClick))
} }
} }
} else if me.Down { } else if me.Down {
@ -2504,9 +2513,9 @@ func (t *Terminal) Loop() {
} }
req(reqList) req(reqList)
if me.Left { 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 return true
} }
mapkey := event.Type
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
actions := t.keymap[mapkey] actions := t.keymap[event.Comparable()]
if mapkey == tui.Rune { if len(actions) == 0 && event.Type == tui.Rune {
mapkey = int(event.Char) + int(tui.AltZ) doAction(action{t: actRune})
if act, prs := t.keymap[mapkey]; prs { } else if !doActions(actions) {
actions = act
}
}
if !doActions(actions, mapkey) {
continue continue
} }
t.truncateQuery() t.truncateQuery()
queryChanged = string(previousInput) != string(t.input) queryChanged = string(previousInput) != string(t.input)
changed = changed || queryChanged changed = changed || queryChanged
if onChanges, prs := t.keymap[tui.Change]; queryChanged && prs { if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs {
if !doActions(onChanges, tui.Change) { if !doActions(onChanges) {
continue continue
} }
} }
if onEOFs, prs := t.keymap[tui.BackwardEOF]; beof && prs { if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs {
if !doActions(onEOFs, tui.BackwardEOF) { if !doActions(onEOFs) {
continue continue
} }
} }
} else { } 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() { if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
t.cy = idx + t.offset t.cy = idx + t.offset
if t.jumping == jumpAcceptEnabled { if t.jumping == jumpAcceptEnabled {

@ -1,6 +1,7 @@
package tui package tui
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"regexp" "regexp"
@ -230,7 +231,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
} }
retries := 0 retries := 0
if c == ESC || nonblock { if c == ESC.Int() || nonblock {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} }
buffer = append(buffer, byte(c)) buffer = append(buffer, byte(c))
@ -245,7 +246,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
continue continue
} }
break break
} else if c == ESC && pc != c { } else if c == ESC.Int() && pc != c {
retries = r.escDelay / escPollInterval retries = r.escDelay / escPollInterval
} else { } else {
retries = 0 retries = 0
@ -278,11 +279,11 @@ func (r *LightRenderer) GetChar() Event {
}() }()
switch r.buffer[0] { switch r.buffer[0] {
case CtrlC: case CtrlC.Byte():
return Event{CtrlC, 0, nil} return Event{CtrlC, 0, nil}
case CtrlG: case CtrlG.Byte():
return Event{CtrlG, 0, nil} return Event{CtrlG, 0, nil}
case CtrlQ: case CtrlQ.Byte():
return Event{CtrlQ, 0, nil} return Event{CtrlQ, 0, nil}
case 127: case 127:
return Event{BSpace, 0, nil} return Event{BSpace, 0, nil}
@ -296,7 +297,7 @@ func (r *LightRenderer) GetChar() Event {
return Event{CtrlCaret, 0, nil} return Event{CtrlCaret, 0, nil}
case 31: case 31:
return Event{CtrlSlash, 0, nil} return Event{CtrlSlash, 0, nil}
case ESC: case ESC.Byte():
ev := r.escSequence(&sz) ev := r.escSequence(&sz)
// Second chance // Second chance
if ev.Type == Invalid { if ev.Type == Invalid {
@ -307,8 +308,8 @@ func (r *LightRenderer) GetChar() Event {
} }
// CTRL-A ~ CTRL-Z // CTRL-A ~ CTRL-Z
if r.buffer[0] <= CtrlZ { if r.buffer[0] <= CtrlZ.Byte() {
return Event{int(r.buffer[0]), 0, nil} return Event{EventType(r.buffer[0]), 0, nil}
} }
char, rsz := utf8.DecodeRune(r.buffer) char, rsz := utf8.DecodeRune(r.buffer)
if char == utf8.RuneError { if char == utf8.RuneError {
@ -331,26 +332,16 @@ func (r *LightRenderer) escSequence(sz *int) Event {
*sz = 2 *sz = 2
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 { 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 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:] r.buffer = r.buffer[1:]
alt = true alt = true
} }
switch r.buffer[1] { switch r.buffer[1] {
case ESC: case ESC.Byte():
return Event{ESC, 0, nil} 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: case 127:
return Event{AltBS, 0, nil} return Event{AltBS, 0, nil}
case '[', 'O': case '[', 'O':
@ -518,11 +509,11 @@ func (r *LightRenderer) escSequence(sz *int) Event {
} // r.buffer[2] } // r.buffer[2]
} // r.buffer[2] } // r.buffer[2]
} // r.buffer[1] } // r.buffer[1]
if r.buffer[1] >= 'a' && r.buffer[1] <= 'z' { rest := bytes.NewBuffer(r.buffer[1:])
return Event{AltA + int(r.buffer[1]) - 'a', 0, nil} c, size, err := rest.ReadRune()
} if err == nil {
if r.buffer[1] >= '0' && r.buffer[1] <= '9' { *sz = 1 + size
return Event{Alt0 + int(r.buffer[1]) - '0', 0, nil} return AltKey(c)
} }
return Event{Invalid, 0, nil} return Event{Invalid, 0, nil}
} }

@ -226,65 +226,65 @@ func (r *FullscreenRenderer) GetChar() Event {
alt := (mods & tcell.ModAlt) > 0 alt := (mods & tcell.ModAlt) > 0
shift := (mods & tcell.ModShift) > 0 shift := (mods & tcell.ModShift) > 0
altShift := alt && shift altShift := alt && shift
keyfn := func(r rune) int { keyfn := func(r rune) Event {
if alt { 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() { switch ev.Key() {
case tcell.KeyCtrlA: case tcell.KeyCtrlA:
return Event{keyfn('a'), 0, nil} return keyfn('a')
case tcell.KeyCtrlB: case tcell.KeyCtrlB:
return Event{keyfn('b'), 0, nil} return keyfn('b')
case tcell.KeyCtrlC: case tcell.KeyCtrlC:
return Event{keyfn('c'), 0, nil} return keyfn('c')
case tcell.KeyCtrlD: case tcell.KeyCtrlD:
return Event{keyfn('d'), 0, nil} return keyfn('d')
case tcell.KeyCtrlE: case tcell.KeyCtrlE:
return Event{keyfn('e'), 0, nil} return keyfn('e')
case tcell.KeyCtrlF: case tcell.KeyCtrlF:
return Event{keyfn('f'), 0, nil} return keyfn('f')
case tcell.KeyCtrlG: case tcell.KeyCtrlG:
return Event{keyfn('g'), 0, nil} return keyfn('g')
case tcell.KeyCtrlH: case tcell.KeyCtrlH:
return Event{keyfn('h'), 0, nil} return keyfn('h')
case tcell.KeyCtrlI: case tcell.KeyCtrlI:
return Event{keyfn('i'), 0, nil} return keyfn('i')
case tcell.KeyCtrlJ: case tcell.KeyCtrlJ:
return Event{keyfn('j'), 0, nil} return keyfn('j')
case tcell.KeyCtrlK: case tcell.KeyCtrlK:
return Event{keyfn('k'), 0, nil} return keyfn('k')
case tcell.KeyCtrlL: case tcell.KeyCtrlL:
return Event{keyfn('l'), 0, nil} return keyfn('l')
case tcell.KeyCtrlM: case tcell.KeyCtrlM:
return Event{keyfn('m'), 0, nil} return keyfn('m')
case tcell.KeyCtrlN: case tcell.KeyCtrlN:
return Event{keyfn('n'), 0, nil} return keyfn('n')
case tcell.KeyCtrlO: case tcell.KeyCtrlO:
return Event{keyfn('o'), 0, nil} return keyfn('o')
case tcell.KeyCtrlP: case tcell.KeyCtrlP:
return Event{keyfn('p'), 0, nil} return keyfn('p')
case tcell.KeyCtrlQ: case tcell.KeyCtrlQ:
return Event{keyfn('q'), 0, nil} return keyfn('q')
case tcell.KeyCtrlR: case tcell.KeyCtrlR:
return Event{keyfn('r'), 0, nil} return keyfn('r')
case tcell.KeyCtrlS: case tcell.KeyCtrlS:
return Event{keyfn('s'), 0, nil} return keyfn('s')
case tcell.KeyCtrlT: case tcell.KeyCtrlT:
return Event{keyfn('t'), 0, nil} return keyfn('t')
case tcell.KeyCtrlU: case tcell.KeyCtrlU:
return Event{keyfn('u'), 0, nil} return keyfn('u')
case tcell.KeyCtrlV: case tcell.KeyCtrlV:
return Event{keyfn('v'), 0, nil} return keyfn('v')
case tcell.KeyCtrlW: case tcell.KeyCtrlW:
return Event{keyfn('w'), 0, nil} return keyfn('w')
case tcell.KeyCtrlX: case tcell.KeyCtrlX:
return Event{keyfn('x'), 0, nil} return keyfn('x')
case tcell.KeyCtrlY: case tcell.KeyCtrlY:
return Event{keyfn('y'), 0, nil} return keyfn('y')
case tcell.KeyCtrlZ: case tcell.KeyCtrlZ:
return Event{keyfn('z'), 0, nil} return keyfn('z')
case tcell.KeyCtrlSpace: case tcell.KeyCtrlSpace:
return Event{CtrlSpace, 0, nil} return Event{CtrlSpace, 0, nil}
case tcell.KeyCtrlBackslash: case tcell.KeyCtrlBackslash:
@ -389,18 +389,7 @@ func (r *FullscreenRenderer) GetChar() Event {
case tcell.KeyRune: case tcell.KeyRune:
r := ev.Rune() r := ev.Rune()
if alt { if alt {
switch r { return AltKey(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 Event{Rune, r, nil} return Event{Rune, r, nil}

@ -8,8 +8,10 @@ import (
) )
// Types of user action // Types of user action
type EventType int
const ( const (
Rune = iota Rune EventType = iota
CtrlA CtrlA
CtrlB CtrlB
@ -89,8 +91,6 @@ const (
Change Change
BackwardEOF BackwardEOF
AltSpace
AltSlash
AltBS AltBS
AltUp AltUp
@ -103,20 +103,38 @@ const (
AltSLeft AltSLeft
AltSRight AltSRight
Alt0 Alt
CtrlAlt
) )
const ( // Reset iota func (t EventType) AsEvent() Event {
AltA = Alt0 + 'a' - '0' + iota return Event{t, 0, nil}
AltB }
AltC
AltD func (t EventType) Int() int {
AltE return int(t)
AltF }
AltZ = AltA + 'z' - 'a'
CtrlAltA = AltZ + 1 func (t EventType) Byte() byte {
CtrlAltM = CtrlAltA + 'm' - 'a' 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 ( const (
doubleClickDuration = 500 * time.Millisecond doubleClickDuration = 500 * time.Millisecond
@ -251,7 +269,7 @@ type ColorTheme struct {
} }
type Event struct { type Event struct {
Type int Type EventType
Char rune Char rune
MouseEvent *MouseEvent MouseEvent *MouseEvent
} }

Loading…
Cancel
Save