gitstack

grindlemire/go-tui code browser

6.7 KB Go 282 lines 2026-03-26 · 62714b4 raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package tui

import "strings"

// Key represents a keyboard key.
type Key uint16

const (
	// KeyNone represents no key (zero value).
	KeyNone Key = iota

	// KeyRune represents a printable character. Check KeyEvent.Rune for the character.
	KeyRune

	// Special keys
	KeyEscape
	KeyEnter
	KeyTab
	KeyBackspace
	KeyDelete
	KeyInsert

	// Arrow keys
	KeyUp
	KeyDown
	KeyLeft
	KeyRight

	// Navigation keys
	KeyHome
	KeyEnd
	KeyPageUp
	KeyPageDown

	// Function keys
	KeyF1
	KeyF2
	KeyF3
	KeyF4
	KeyF5
	KeyF6
	KeyF7
	KeyF8
	KeyF9
	KeyF10
	KeyF11
	KeyF12
)

// Ctrl+letter helpers. Each is a RuneSpec that matches the corresponding
// Ctrl+letter combination via the modifier API.
var (
	KeyCtrlA     = Rune('a').Ctrl()
	KeyCtrlB     = Rune('b').Ctrl()
	KeyCtrlC     = Rune('c').Ctrl()
	KeyCtrlD     = Rune('d').Ctrl()
	KeyCtrlE     = Rune('e').Ctrl()
	KeyCtrlF     = Rune('f').Ctrl()
	KeyCtrlG     = Rune('g').Ctrl()
	KeyCtrlH     = Rune('h').Ctrl()
	KeyCtrlI     = Rune('i').Ctrl()
	KeyCtrlJ     = Rune('j').Ctrl()
	KeyCtrlK     = Rune('k').Ctrl()
	KeyCtrlL     = Rune('l').Ctrl()
	KeyCtrlM     = Rune('m').Ctrl()
	KeyCtrlN     = Rune('n').Ctrl()
	KeyCtrlO     = Rune('o').Ctrl()
	KeyCtrlP     = Rune('p').Ctrl()
	KeyCtrlQ     = Rune('q').Ctrl()
	KeyCtrlR     = Rune('r').Ctrl()
	KeyCtrlS     = Rune('s').Ctrl()
	KeyCtrlT     = Rune('t').Ctrl()
	KeyCtrlU     = Rune('u').Ctrl()
	KeyCtrlV     = Rune('v').Ctrl()
	KeyCtrlW     = Rune('w').Ctrl()
	KeyCtrlX     = Rune('x').Ctrl()
	KeyCtrlY     = Rune('y').Ctrl()
	KeyCtrlZ     = Rune('z').Ctrl()
	KeyCtrlSpace = Rune(' ').Ctrl()
)

// String returns a human-readable representation of the key.
func (k Key) String() string {
	switch k {
	case KeyNone:
		return "None"
	case KeyRune:
		return "Rune"
	case KeyEscape:
		return "Escape"
	case KeyEnter:
		return "Enter"
	case KeyTab:
		return "Tab"
	case KeyBackspace:
		return "Backspace"
	case KeyDelete:
		return "Delete"
	case KeyInsert:
		return "Insert"
	case KeyUp:
		return "Up"
	case KeyDown:
		return "Down"
	case KeyLeft:
		return "Left"
	case KeyRight:
		return "Right"
	case KeyHome:
		return "Home"
	case KeyEnd:
		return "End"
	case KeyPageUp:
		return "PageUp"
	case KeyPageDown:
		return "PageDown"
	case KeyF1:
		return "F1"
	case KeyF2:
		return "F2"
	case KeyF3:
		return "F3"
	case KeyF4:
		return "F4"
	case KeyF5:
		return "F5"
	case KeyF6:
		return "F6"
	case KeyF7:
		return "F7"
	case KeyF8:
		return "F8"
	case KeyF9:
		return "F9"
	case KeyF10:
		return "F10"
	case KeyF11:
		return "F11"
	case KeyF12:
		return "F12"
	default:
		return "Unknown"
	}
}

// KeyMatcher describes what key events to match.
// Sealed to this package via unexported method.
type KeyMatcher interface {
	keyPattern() KeyPattern
}

// KeySpec matches a special key with specific modifiers.
type KeySpec struct {
	key Key
	mod Modifier
}

func (s KeySpec) keyPattern() KeyPattern {
	if s.mod != 0 {
		return KeyPattern{Key: s.key, Mod: s.mod}
	}
	return KeyPattern{Key: s.key, ExcludeMods: ModCtrl | ModAlt | ModShift}
}

// Ctrl returns a KeySpec requiring the Ctrl modifier.
func (s KeySpec) Ctrl() KeySpec { s.mod |= ModCtrl; return s }

// Alt returns a KeySpec requiring the Alt modifier.
func (s KeySpec) Alt() KeySpec { s.mod |= ModAlt; return s }

// Shift returns a KeySpec requiring the Shift modifier.
func (s KeySpec) Shift() KeySpec { s.mod |= ModShift; return s }

// keyPattern makes Key satisfy KeyMatcher directly.
// Matches the bare key with no modifiers (excludes Ctrl/Alt/Shift).
func (k Key) keyPattern() KeyPattern {
	return KeyPattern{Key: k, ExcludeMods: ModCtrl | ModAlt | ModShift}
}

// Ctrl returns a KeySpec for this key with the Ctrl modifier.
func (k Key) Ctrl() KeySpec { return KeySpec{key: k, mod: ModCtrl} }

// Alt returns a KeySpec for this key with the Alt modifier.
func (k Key) Alt() KeySpec { return KeySpec{key: k, mod: ModAlt} }

// Shift returns a KeySpec for this key with the Shift modifier.
func (k Key) Shift() KeySpec { return KeySpec{key: k, mod: ModShift} }

// RuneSpec matches a specific printable character with optional modifiers.
type RuneSpec struct {
	r   rune
	mod Modifier
}

// Rune returns a RuneSpec that matches a specific printable character.
// r must be a non-zero printable rune; Rune(0) will never match any event.
// Without modifiers, allows Shift (character-forming) but excludes Ctrl and Alt.
//
// Rune('a').Ctrl() works in both legacy and Kitty keyboard modes. In legacy
// mode, Ctrl+letter bytes (0x01-0x1A) are normalized to {KeyRune, letter, ModCtrl}
// by the parser, producing the same event as Kitty mode's CSI u encoding.
// Modifier combinations beyond what the terminal can distinguish (e.g.
// Ctrl+Shift+letter in legacy mode, where both produce the same control byte)
// will match whichever event the terminal actually sends.
func Rune(r rune) RuneSpec {
	return RuneSpec{r: r}
}

func (s RuneSpec) keyPattern() KeyPattern {
	if s.mod != 0 {
		return KeyPattern{Rune: s.r, Mod: s.mod}
	}
	return KeyPattern{Rune: s.r, ExcludeMods: ModCtrl | ModAlt}
}

// Ctrl returns a RuneSpec requiring the Ctrl modifier.
func (s RuneSpec) Ctrl() RuneSpec { s.mod |= ModCtrl; return s }

// Alt returns a RuneSpec requiring the Alt modifier.
func (s RuneSpec) Alt() RuneSpec { s.mod |= ModAlt; return s }

// Shift returns a RuneSpec requiring the Shift modifier.
func (s RuneSpec) Shift() RuneSpec { s.mod |= ModShift; return s }

// anyRuneSpec matches any printable character.
type anyRuneSpec struct{}

func (anyRuneSpec) keyPattern() KeyPattern {
	return KeyPattern{AnyRune: true, ExcludeMods: ModCtrl | ModAlt}
}

// AnyRune matches any printable character.
// Allows Shift (character-forming) but excludes Ctrl and Alt.
var AnyRune KeyMatcher = anyRuneSpec{}

// anyKeySpec matches any key event (printable or special).
type anyKeySpec struct{}

func (anyKeySpec) keyPattern() KeyPattern {
	return KeyPattern{AnyKey: true}
}

// AnyKey matches any key event. Used by modal overlays to block all parent handlers.
var AnyKey KeyMatcher = anyKeySpec{}

// Modifier represents keyboard modifier flags.
type Modifier uint8

const (
	// ModNone represents no modifiers.
	ModNone Modifier = 0
	// ModCtrl represents the Ctrl modifier.
	ModCtrl Modifier = 1 << iota
	// ModAlt represents the Alt modifier.
	ModAlt
	// ModShift represents the Shift modifier.
	ModShift
)

// Has checks if the modifier set includes the given modifier.
func (m Modifier) Has(mod Modifier) bool {
	return m&mod != 0
}

// String returns a human-readable representation of the modifiers.
func (m Modifier) String() string {
	if m == ModNone {
		return "None"
	}

	var parts []string
	if m.Has(ModCtrl) {
		parts = append(parts, "Ctrl")
	}
	if m.Has(ModAlt) {
		parts = append(parts, "Alt")
	}
	if m.Has(ModShift) {
		parts = append(parts, "Shift")
	}
	return strings.Join(parts, "+")
}