tokenizer.go RawHistoryFolder

1package keys
2
3import (
4	"bytes"
5	"unicode/utf8"
6
7	"github.com/bnema/vev/internal/ports"
8	"github.com/bnema/vev/internal/usecase/keys/kittykey"
9)
10
11// escKind classifies the input introduced by one ESC byte.
12type escKind uint8
13
14const (
15	// escIncomplete means more bytes are needed before the ESC can be
16	// classified: a lone trailing ESC, a partial Alt-arrow CSI, or a partial
17	// UTF-8 rune after ESC.
18	escIncomplete escKind = iota
19	// escPaste is a complete bracketed paste, open marker through close marker.
20	escPaste
21	// escAltArrow is a complete ESC [ 1 ; 3|9 <A-D> sequence.
22	escAltArrow
23	// escControlPrefix is ESC [ or ESC O, the start of a terminal control
24	// sequence that passes through untouched.
25	escControlPrefix
26	// escAltRune is ESC followed by one complete, valid UTF-8 rune.
27	escAltRune
28	// escKittyKey is a complete kitty keyboard protocol CSI u key event.
29	escKittyKey
30	// escBare is an ESC that starts none of the above.
31	escBare
32)
33
34// escToken is one lexed ESC-introduced unit. raw always starts with ESC and
35// aliases the scanned input.
36type escToken struct {
37	kind  escKind
38	raw   []byte
39	rune  rune // escAltRune and escControlPrefix: the rune after ESC
40	arrow byte // escAltArrow: the final byte, one of 'A'..'D'
41	key   kittykey.Event
42}
43
44const altArrowLen = len("\x1b[1;3A")
45
46// scanEscape lexes the unit starting at data[0], which must be ESC. It only
47// classifies bytes; binding lookups and forwarding decisions belong to Router.
48func scanEscape(data []byte, kitty bool) escToken {
49	if len(data) == 1 {
50		return escToken{kind: escIncomplete, raw: data}
51	}
52	if bytes.HasPrefix(data, ports.BracketedPasteOpenMarker) {
53		if rel := bytes.Index(data, ports.BracketedPasteCloseMarker); rel >= 0 {
54			return escToken{kind: escPaste, raw: data[:rel+len(ports.BracketedPasteCloseMarker)]}
55		}
56	}
57	if final, ok := altArrowFinal(data); ok {
58		return escToken{kind: escAltArrow, raw: data[:altArrowLen], arrow: final}
59	}
60	if isAltArrowPrefix(data) {
61		return escToken{kind: escIncomplete, raw: data}
62	}
63	if kitty {
64		if ev, n, ok, partial := kittykey.Parse(data); ok {
65			return escToken{kind: escKittyKey, raw: data[:n], key: ev}
66		} else if partial {
67			return escToken{kind: escIncomplete, raw: data}
68		}
69	}
70	next := data[1]
71	if next == '[' || next == 'O' {
72		return escToken{kind: escControlPrefix, raw: data[:2], rune: rune(next)}
73	}
74	rest := data[1:]
75	if partialUTF8Rune(rest) {
76		return escToken{kind: escIncomplete, raw: data}
77	}
78	if key, size := utf8.DecodeRune(rest); key != utf8.RuneError || size > 1 {
79		return escToken{kind: escAltRune, raw: data[:1+size], rune: key}
80	}
81	return escToken{kind: escBare, raw: data[:1]}
82}
83
84// altArrowFinal reports the arrow final byte of a complete Alt (;3) or Meta
85// (;9) modified arrow CSI at the start of data.
86func altArrowFinal(data []byte) (byte, bool) {
87	if len(data) < altArrowLen {
88		return 0, false
89	}
90	seq := data[:altArrowLen]
91	if seq[1] != '[' || seq[2] != '1' || seq[3] != ';' || (seq[4] != '3' && seq[4] != '9') {
92		return 0, false
93	}
94	switch final := seq[5]; final {
95	case 'A', 'B', 'C', 'D':
96		return final, true
97	default:
98		return 0, false
99	}
100}
101
102// isAltArrowPrefix reports whether data is a strict prefix of an Alt/Meta
103// arrow CSI, meaning the next read may still complete it.
104func isAltArrowPrefix(data []byte) bool {
105	if len(data) < 2 || len(data) >= altArrowLen {
106		return false
107	}
108	return matchesPrefix(data, "\x1b[1;3") || matchesPrefix(data, "\x1b[1;9")
109}
110
111func matchesPrefix(data []byte, want string) bool {
112	return len(data) <= len(want) && string(data) == want[:len(data)]
113}
114
115func partialUTF8Rune(data []byte) bool {
116	if len(data) == 0 || utf8.FullRune(data) {
117		return false
118	}
119	key, size := utf8.DecodeRune(data)
120	return key == utf8.RuneError && size == 1
121}

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 12

Tags 11

↑↓ moveenter openesc close