key.go RawHistoryFolder
1package vimkeys
2
3import (
4 "errors"
5 "strings"
6)
7
8// Mods holds keyboard modifier flags for a parsed key.
9type Mods uint8
10
11const (
12 ModCtrl Mods = 1 << iota
13 ModShift
14 ModAlt
15)
16
17const (
18 symCR = "CR"
19 symEsc = "Esc"
20)
21
22// Key is a single key event with symbol and modifiers.
23type Key struct {
24 Sym string
25 Mods Mods
26}
27
28// Sequence is an ordered list of keys forming a Vim-style binding.
29type Sequence []Key
30
31var (
32 ErrEmptyBinding = errors.New("vimkeys: empty binding")
33 ErrBadBinding = errors.New("vimkeys: malformed binding")
34)
35
36// String renders a key in canonical form.
37func (k Key) String() string {
38 if k.Sym == "" {
39 return ""
40 }
41
42 if k.Mods == ModShift && len(k.Sym) == 1 {
43 if c := k.Sym[0]; c >= 'a' && c <= 'z' {
44 return strings.ToUpper(k.Sym)
45 }
46 }
47
48 if k.Mods == 0 {
49 if k.Sym == "+" {
50 return "<Plus>"
51 }
52 if alias := angleAliasForSym(k.Sym); alias != "" {
53 return "<" + alias + ">"
54 }
55 return k.Sym
56 }
57
58 return formatAngleKey(k)
59}
60
61// String renders the full binding sequence in canonical form.
62// When the naive concatenation would case-insensitively equal an exact legacy
63// atom (enter/return/escape/esc), the first key is forced to an angle literal
64// so ParseBinding cannot reabsorb the sequence as that atom.
65func (s Sequence) String() string {
66 rendered := s.render(false)
67 if collidesWithLegacyAtom(rendered) {
68 return s.render(true)
69 }
70 return rendered
71}
72
73func (s Sequence) render(forceAngleFirst bool) string {
74 var b strings.Builder
75 for i, key := range s {
76 if forceAngleFirst && i == 0 {
77 b.WriteString(key.angleLiteralString())
78 continue
79 }
80 b.WriteString(key.String())
81 }
82 return b.String()
83}
84
85func collidesWithLegacyAtom(s string) bool {
86 switch strings.ToLower(s) {
87 case "enter", "return", "escape", "esc":
88 return true
89 default:
90 return false
91 }
92}
93
94// angleLiteralString forces angle-bracket form for keys that Key.String would
95// otherwise emit as bare or shifted letters (e.g. "e" / "E").
96func (k Key) angleLiteralString() string {
97 if len(k.Sym) == 1 {
98 c := k.Sym[0]
99 if c >= 'a' && c <= 'z' {
100 switch k.Mods {
101 case 0:
102 return "<" + k.Sym + ">"
103 case ModShift:
104 return formatAngleKey(k)
105 }
106 }
107 }
108 return k.String()
109}
110
111// Equal reports whether two sequences contain the same keys.
112func (s Sequence) Equal(other Sequence) bool {
113 if len(s) != len(other) {
114 return false
115 }
116 for i := range s {
117 if s[i] != other[i] {
118 return false
119 }
120 }
121 return true
122}
123
124func angleAliasForSym(sym string) string {
125 switch sym {
126 case symCR:
127 return "Return"
128 case symEsc:
129 return "Escape"
130 case "Space", "Tab", "BackSpace", "Delete",
131 "Left", "Right", "Up", "Down",
132 "Home", "End", "PageUp", "PageDown":
133 return sym
134 case "<":
135 return "lt"
136 case ">":
137 return "gt"
138 default:
139 return ""
140 }
141}
142
143func formatAngleKey(k Key) string {
144 var b strings.Builder
145 b.WriteByte('<')
146 if k.Mods&ModCtrl != 0 {
147 b.WriteByte('C')
148 }
149 if k.Mods&ModShift != 0 {
150 if b.Len() > 1 {
151 b.WriteByte('-')
152 }
153 b.WriteByte('S')
154 }
155 if k.Mods&ModAlt != 0 {
156 if b.Len() > 1 {
157 b.WriteByte('-')
158 }
159 b.WriteByte('A')
160 }
161 b.WriteByte('-')
162 if alias := angleAliasForSym(k.Sym); alias != "" {
163 b.WriteString(alias)
164 } else {
165 b.WriteString(k.Sym)
166 }
167 b.WriteByte('>')
168 return b.String()
169}