main 4509ec7

input.go RawHistoryFolder

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

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