v0.20.0 29be61c

input.go RawHistoryFolder

1package tui
2
3import (
4	"strconv"
5	"strings"
6	"time"
7)
8
9// Key is a parsed keypress.
10type Key struct {
11	Kind  KeyKind
12	Rune  rune   // for KeyRune
13	Paste string // for KeyPaste
14	Ctrl  bool
15	Alt   bool
16	Shift bool
17	Super bool
18}
19
20type KeyKind int
21
22const (
23	KeyRune KeyKind = iota
24	KeyEnter
25	KeyBackspace
26	KeyTab
27	KeyShiftTab
28	KeyEsc
29	KeyUp
30	KeyDown
31	KeyLeft
32	KeyRight
33	KeyHome
34	KeyEnd
35	KeyPageUp
36	KeyPageDown
37	KeyDelete
38	KeyCtrlC
39	KeyCtrlD
40	KeyCtrlL
41	KeyCtrlU
42	KeyCtrlK
43	KeyCtrlA
44	KeyCtrlB
45	KeyCtrlE
46	KeyCtrlW
47	KeyCtrlO
48	KeyPaste
49	KeyPasteClipboard
50	KeyMouseWheelUp
51	KeyMouseWheelDown
52	KeyUnknown
53)
54
55// Reader parses a byte stream into Key events. It understands basic
56// xterm escape sequences and bracketed paste.
57type Reader struct {
58	src  func() (byte, error)
59	peek func(time.Duration) (byte, bool, error) // optional; may be nil
60
61	// pending contains bytes consumed while deciding whether an Esc was
62	// bare. In particular, a second Esc belongs to the next Read, not to
63	// the Alt+<char> case handled by readEscape.
64	pending []byte
65
66	// A Reader without a peek callback still needs to preserve the old
67	// NewReader API and parse Alt/CSI input. The pump lets its bounded
68	// lookahead wait without calling the blocking source on the Read caller's
69	// goroutine. There is never more than one outstanding source call: after a
70	// timeout that call is reused by the next Read rather than replaced.
71	//
72	// The source callback has no cancellation contract, so an implementation
73	// that blocks forever can keep this one goroutine alive forever. That
74	// limitation is intentional; starting another goroutine for each isolated
75	// Esc would leak an unbounded number of blocked goroutines and could also
76	// reorder bytes from an uncancellable source.
77	pump    *readerPump
78	pumpErr error
79}
80
81type readerResult struct {
82	b   byte
83	err error
84}
85
86type readerPump struct {
87	result chan readerResult
88}
89
90func newReaderPump(read func() (byte, error)) *readerPump {
91	p := &readerPump{result: make(chan readerResult, 1)}
92	go func() {
93		b, err := read()
94		p.result <- readerResult{b: b, err: err}
95	}()
96	return p
97}
98
99// NewReader returns a Reader that pulls bytes from read.
100func NewReader(read func() (byte, error)) *Reader { return &Reader{src: read} }
101
102// NewReaderWithPeek returns a Reader that pulls bytes from read and uses
103// peek to disambiguate bare Esc from the start of an escape sequence.
104func NewReaderWithPeek(read func() (byte, error), peek func(time.Duration) (byte, bool, error)) *Reader {
105	return &Reader{src: read, peek: peek}
106}
107
108// readByte returns the next source byte, including any byte held back after
109// an escape lookahead.
110func (r *Reader) readByte() (byte, error) {
111	if len(r.pending) != 0 {
112		b := r.pending[0]
113		r.pending = r.pending[1:]
114		return b, nil
115	}
116	if r.pumpErr != nil {
117		return 0, r.pumpErr
118	}
119	if r.pump == nil {
120		return r.src()
121	}
122	result := <-r.pump.result
123	r.pump = nil
124	if result.err != nil {
125		r.pumpErr = result.err
126	}
127	return result.b, result.err
128}
129
130// readByteTimeout returns one source byte without waiting longer than d. A
131// source call left outstanding after a timeout is consumed by the next Read.
132func (r *Reader) readByteTimeout(d time.Duration) (byte, bool, error) {
133	if r.pumpErr != nil {
134		return 0, false, r.pumpErr
135	}
136	if r.pump == nil {
137		r.pump = newReaderPump(r.src)
138	}
139
140	var result readerResult
141	if d <= 0 {
142		select {
143		case result = <-r.pump.result:
144		default:
145			return 0, false, nil
146		}
147	} else {
148		timer := time.NewTimer(d)
149		defer timer.Stop()
150		select {
151		case result = <-r.pump.result:
152		case <-timer.C:
153			return 0, false, nil
154		}
155	}
156	r.pump = nil
157	if result.err != nil {
158		r.pumpErr = result.err
159		return 0, false, result.err
160	}
161	return result.b, true, nil
162}
163
164// Read returns the next parsed Key.
165func (r *Reader) Read() (Key, error) {
166	b, err := r.readByte()
167	if err != nil {
168		return Key{}, err
169	}
170	switch {
171	case b == 0x03:
172		return Key{Kind: KeyCtrlC}, nil
173	case b == 0x04:
174		return Key{Kind: KeyCtrlD}, nil
175	case b == 0x0c:
176		return Key{Kind: KeyCtrlL}, nil
177	case b == 0x15:
178		return Key{Kind: KeyCtrlU}, nil
179	case b == 0x0b:
180		return Key{Kind: KeyCtrlK}, nil
181	case b == 0x01:
182		return Key{Kind: KeyCtrlA}, nil
183	case b == 0x02:
184		return Key{Kind: KeyCtrlB}, nil
185	case b == 0x05:
186		return Key{Kind: KeyCtrlE}, nil
187	case b == 0x17:
188		return Key{Kind: KeyCtrlW}, nil
189	case b == 0x0f:
190		return Key{Kind: KeyCtrlO}, nil
191	case b == 0x16:
192		return Key{Kind: KeyPasteClipboard, Ctrl: true}, nil
193	case b == '\r', b == '\n':
194		return Key{Kind: KeyEnter}, nil
195	case b == '\t':
196		return Key{Kind: KeyTab}, nil
197	case b == 0x7f, b == 0x08:
198		return Key{Kind: KeyBackspace}, nil
199	case b == 0x1b:
200		return r.readEscape()
201	case b < 0x20:
202		return Key{Kind: KeyUnknown}, nil
203	}
204	// UTF-8 multibyte?
205	if b < 0x80 {
206		return Key{Kind: KeyRune, Rune: rune(b)}, nil
207	}
208	// Decode UTF-8 (up to 4 bytes).
209	n := utf8Len(b)
210	buf := []byte{b}
211	for i := 1; i < n; i++ {
212		bb, err := r.readByte()
213		if err != nil {
214			return Key{}, err
215		}
216		buf = append(buf, bb)
217	}
218	rn, _ := decodeRune(buf)
219	return Key{Kind: KeyRune, Rune: rn}, nil
220}
221
222func utf8Len(b byte) int {
223	switch {
224	case b&0xe0 == 0xc0:
225		return 2
226	case b&0xf0 == 0xe0:
227		return 3
228	case b&0xf8 == 0xf0:
229		return 4
230	}
231	return 1
232}
233
234func decodeRune(b []byte) (rune, int) {
235	// Minimal decoder; invalid runes become U+FFFD.
236	if len(b) == 1 {
237		return rune(b[0]), 1
238	}
239	var r rune
240	switch len(b) {
241	case 2:
242		r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
243	case 3:
244		r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
245	case 4:
246		r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
247	default:
248		r = 0xFFFD
249	}
250	return r, len(b)
251}
252
253// readEscape handles sequences starting with 0x1b.
254func (r *Reader) readEscape() (Key, error) {
255	// Bare ESC: maybe followed by another byte within a short window.
256	b, have, err := r.readEscapeNext(50 * time.Millisecond)
257	if err != nil || !have {
258		return Key{Kind: KeyEsc}, nil
259	}
260	if b == 0x1b {
261		// A second raw ESC is a separate key, not Alt+ESC. Keep it for the
262		// next Read so Esc Esc produces two bare KeyEsc events. This is the
263		// one lookahead case that is not intentionally treated as Alt input.
264		r.pending = append(r.pending, b)
265		return Key{Kind: KeyEsc}, nil
266	}
267	switch b {
268	case '[':
269		return r.readCSI()
270	case 'O':
271		// SS3 sequences (function keys in some terminals).
272		c, err := r.readByte()
273		if err != nil {
274			return Key{}, err
275		}
276		switch c {
277		case 'H':
278			return Key{Kind: KeyHome}, nil
279		case 'F':
280			return Key{Kind: KeyEnd}, nil
281		}
282		return Key{Kind: KeyUnknown}, nil
283	case 0x7f, 0x08:
284		// Alt+Backspace (Option+Delete on macOS) — most terminals send
285		// ESC + DEL for this. Surface as a dedicated "alt backspace"
286		// so the editor can map it to delete-word.
287		return Key{Kind: KeyBackspace, Alt: true}, nil
288	case 'b':
289		// Emacs-style word-left, also emitted by some terminals for
290		// Option+LeftArrow.
291		return Key{Kind: KeyLeft, Alt: true}, nil
292	case 'f':
293		// Emacs-style word-right, also emitted for Option+RightArrow.
294		return Key{Kind: KeyRight, Alt: true}, nil
295	default:
296		// Alt+<char>
297		if b < 0x80 {
298			return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
299		}
300	}
301	return Key{Kind: KeyUnknown}, nil
302}
303
304// readEscapeNext tries to read one byte within d. If peek is available
305// we use it. NewReader's source pump supplies the same bounded behavior
306// when no peek callback is provided, so a bare Esc never waits forever for
307// a second byte. A follow-on printable byte, DEL, b, or f remains
308// intentionally ambiguous with Alt input and keeps the historical mapping
309// in readEscape.
310func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
311	if r.peek != nil {
312		return r.peek(d)
313	}
314	return r.readByteTimeout(d)
315}
316
317// readCSI parses a CSI sequence after ESC [.
318func (r *Reader) readCSI() (Key, error) {
319	var params []byte
320	for {
321		c, err := r.readByte()
322		if err != nil {
323			return Key{}, err
324		}
325		if c >= 0x30 && c <= 0x3f {
326			params = append(params, c)
327			continue
328		}
329		// Final byte.
330		return r.dispatchCSI(string(params), c), nil
331	}
332}
333
334func (r *Reader) dispatchCSI(params string, final byte) Key {
335	// SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
336	// button codes 64 (up) and 65 (down). We ignore coordinates for
337	// now; the chat view only needs scroll direction.
338	if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
339		parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
340		if len(parts) >= 1 {
341			switch parts[0] {
342			case "64":
343				return Key{Kind: KeyMouseWheelUp}
344			case "65":
345				return Key{Kind: KeyMouseWheelDown}
346			}
347		}
348		return Key{Kind: KeyUnknown}
349	}
350
351	shift, alt, super := parseCSIModifiers(params)
352	if final == 'u' {
353		if key, ok := parseCSIU(params); ok {
354			return key
355		}
356	}
357	if final == '~' {
358		if key, ok := parseModifyOtherKeys(params); ok {
359			return key
360		}
361	}
362	switch final {
363	case 'A':
364		return Key{Kind: KeyUp, Alt: alt, Shift: shift, Super: super}
365	case 'B':
366		return Key{Kind: KeyDown, Alt: alt, Shift: shift, Super: super}
367	case 'C':
368		return Key{Kind: KeyRight, Alt: alt, Shift: shift, Super: super}
369	case 'D':
370		return Key{Kind: KeyLeft, Alt: alt, Shift: shift, Super: super}
371	case 'H':
372		return Key{Kind: KeyHome}
373	case 'F':
374		return Key{Kind: KeyEnd}
375	case 'Z':
376		return Key{Kind: KeyShiftTab}
377	case '~':
378		switch params {
379		case "3":
380			return Key{Kind: KeyDelete}
381		case "5":
382			return Key{Kind: KeyPageUp}
383		case "6":
384			return Key{Kind: KeyPageDown}
385		case "200":
386			// Start of bracketed paste.
387			return r.readPaste()
388		}
389	}
390	return Key{Kind: KeyUnknown}
391}
392
393func parseCSIModifiers(params string) (shift, alt, super bool) {
394	if params == "" {
395		return false, false, false
396	}
397	i := strings.LastIndexByte(params, ';')
398	if i < 0 || i+1 >= len(params) {
399		return false, false, false
400	}
401	mod, ok := parseModifierParam(params[i+1:])
402	if !ok {
403		return false, false, false
404	}
405	shift, alt, _, super = modifierBits(mod)
406	return shift, alt, super
407}
408
409func parseCSIU(params string) (Key, bool) {
410	parts := strings.Split(params, ";")
411	if len(parts) == 0 {
412		return Key{}, false
413	}
414	code, err := strconv.Atoi(parts[0])
415	if err != nil {
416		return Key{}, false
417	}
418	mod := 1
419	if len(parts) >= 2 {
420		var ok bool
421		mod, ok = parseModifierParam(parts[1])
422		if !ok {
423			return Key{}, false
424		}
425	}
426	return keyFromModifiedCode(code, mod)
427}
428
429func parseModifyOtherKeys(params string) (Key, bool) {
430	parts := strings.Split(params, ";")
431	if len(parts) != 3 || parts[0] != "27" {
432		return Key{}, false
433	}
434	mod, ok := parseModifierParam(parts[1])
435	if !ok {
436		return Key{}, false
437	}
438	code, err := strconv.Atoi(parts[2])
439	if err != nil {
440		return Key{}, false
441	}
442	return keyFromModifiedCode(code, mod)
443}
444
445func parseModifierParam(s string) (int, bool) {
446	if i := strings.IndexByte(s, ':'); i >= 0 {
447		s = s[:i]
448	}
449	mod, err := strconv.Atoi(s)
450	if err != nil {
451		return 0, false
452	}
453	return mod, true
454}
455
456func modifierBits(mod int) (shift, alt, ctrl, super bool) {
457	bits := mod - 1
458	return bits&1 != 0, bits&2 != 0, bits&4 != 0, bits&8 != 0 || bits&32 != 0
459}
460
461func keyFromModifiedCode(code, mod int) (Key, bool) {
462	shift, alt, ctrl, super := modifierBits(mod)
463	// Kitty keyboard protocol (CSI ... u) reports control keys as their
464	// codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
465	// enhanced-mode handling these arrive as raw bytes; with it enabled
466	// they come through here, so map them back to their dedicated keys.
467	switch code {
468	case 13:
469		return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
470	case 27:
471		return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
472	case 9:
473		if shift {
474			return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl, Super: super}, true
475		}
476		return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
477	case 127, 8:
478		return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
479	}
480	if ctrl {
481		switch code {
482		case 'c', 'C':
483			return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
484		case 'd', 'D':
485			return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
486		case 'l', 'L':
487			return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
488		case 'u', 'U':
489			return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
490		case 'k', 'K':
491			return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
492		case 'a', 'A':
493			return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
494		case 'b', 'B', 2:
495			return Key{Kind: KeyCtrlB, Shift: shift, Alt: alt, Ctrl: true}, true
496		case 'e', 'E':
497			return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
498		case 'w', 'W':
499			return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
500		case 'o', 'O':
501			return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
502		case 'v', 'V':
503			return Key{Kind: KeyPasteClipboard, Shift: shift, Alt: alt, Ctrl: true}, true
504		}
505	}
506	if code >= '0' && code <= '9' {
507		return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
508	}
509	return Key{}, false
510}
511
512// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
513func (r *Reader) readPaste() Key {
514	var sb strings.Builder
515	const end = "\x1b[201~"
516	tail := make([]byte, 0, len(end))
517	for {
518		b, err := r.readByte()
519		if err != nil {
520			break
521		}
522		tail = append(tail, b)
523		if len(tail) > len(end) {
524			sb.WriteByte(tail[0])
525			tail = tail[1:]
526		}
527		if string(tail) == end {
528			break
529		}
530	}
531	return Key{Kind: KeyPaste, Paste: sb.String()}
532}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close