main ece7682

normalize.go RawHistoryFolder

1package kittykey
2
3import (
4	"bytes"
5	"strconv"
6	"unicode/utf8"
7)
8
9// maxNormalizedSeq bounds one held key sequence. Associated text makes a key
10// event as long as the text an IME commits at once.
11const maxNormalizedSeq = 4096
12
13// Kitty functional key codes of the modifier and lock keys. RequestFlags
14// reports them; OuterFlags never does.
15const (
16	keyCapsLock     = 57358
17	keyNumLock      = 57360
18	keyLeftShift    = 57441
19	keyLeftControl  = 57442
20	keyRightControl = 57448
21	keyISOLevel5    = 57454
22)
23
24// Normalizer rewrites the outer terminal's RequestFlags key stream into the
25// OuterFlags encoding the rest of vev consumes, and tracks whether Ctrl is
26// physically held. Release events, modifier and lock key events are dropped,
27// repeats become presses, and text keys become their text again.
28//
29// Input already in the OuterFlags or legacy encoding passes through
30// unchanged, so enabling a Normalizer before the terminal switches flags is
31// safe. A trailing incomplete escape sequence is held until the next call. A
32// lone trailing ESC is held only once a CSI u key event proved the terminal
33// reports Escape as CSI 27 u; before that it may be a real Escape press.
34// A Normalizer is not safe for concurrent use.
35type Normalizer struct {
36	pending []byte
37	paste   bool
38	ctrl    bool
39	// kitty records that a CSI u key event arrived, so the terminal encodes
40	// Escape itself as one and a lone ESC can only start a split sequence.
41	kitty bool
42}
43
44// CtrlHeld reports whether Ctrl is held on its own: a Ctrl key is down and no
45// other key but a top-row digit was pressed since. Only a Ctrl key press sets
46// it, so a terminal that reports no modifier key events never shows it held.
47func (n *Normalizer) CtrlHeld() bool { return n.ctrl }
48
49// ReleaseCtrl forgets a held Ctrl, for a terminal that lost focus and may
50// never report the release.
51func (n *Normalizer) ReleaseCtrl() { n.ctrl = false }
52
53// Normalize returns data in the OuterFlags encoding, in a new slice.
54func (n *Normalizer) Normalize(data []byte) []byte {
55	if len(n.pending) > 0 {
56		data = append(n.pending, data...)
57		n.pending = nil
58	}
59	out := make([]byte, 0, len(data))
60	for i := 0; i < len(data); {
61		if n.paste {
62			end := bytes.Index(data[i:], pasteClose)
63			if end < 0 {
64				keep := len(data) - partialSuffix(data[i:], pasteClose)
65				out = append(out, data[i:keep]...)
66				n.hold(data[keep:])
67				return out
68			}
69			end += i + len(pasteClose)
70			out = append(out, data[i:end]...)
71			i = end
72			n.paste = false
73			continue
74		}
75		if data[i] != 0x1b {
76			out = append(out, data[i])
77			i++
78			continue
79		}
80		rest := data[i:]
81		if bytes.HasPrefix(rest, pasteOpen) {
82			out = append(out, pasteOpen...)
83			i += len(pasteOpen)
84			n.paste = true
85			continue
86		}
87		params, final, size, partial := scanCSI(rest)
88		if partial && len(rest) == 1 && !n.kitty {
89			out = append(out, rest...)
90			return out
91		}
92		if partial {
93			n.hold(rest)
94			return out
95		}
96		if size > 0 {
97			if repl, ok := n.rewrite(params, final); ok {
98				out = append(out, repl...)
99				i += size
100				continue
101			}
102		}
103		out = append(out, data[i])
104		i++
105	}
106	return out
107}
108
109func (n *Normalizer) hold(tail []byte) {
110	if len(tail) > 0 {
111		n.pending = append([]byte(nil), tail...)
112	}
113}
114
115// partialSuffix returns the length of the longest suffix of data that is a
116// strict prefix of marker.
117func partialSuffix(data, marker []byte) int {
118	for k := min(len(data), len(marker)-1); k > 0; k-- {
119		if bytes.HasPrefix(marker, data[len(data)-k:]) {
120			return k
121		}
122	}
123	return 0
124}
125
126// scanCSI reads ESC [ params final at the start of data, where params holds
127// only digits, ';' and ':'. size is 0 when data starts no such sequence;
128// partial reports a strict prefix that may still complete.
129func scanCSI(data []byte) (params []byte, final byte, size int, partial bool) {
130	if len(data) == 1 {
131		return nil, 0, 0, true
132	}
133	if data[1] != '[' {
134		return nil, 0, 0, false
135	}
136	for i := 2; i < len(data) && i < maxNormalizedSeq; i++ {
137		b := data[i]
138		if (b >= '0' && b <= '9') || b == ';' || b == ':' {
139			continue
140		}
141		return data[2:i], b, i + 1, false
142	}
143	return nil, 0, 0, len(data) < maxNormalizedSeq
144}
145
146// rewrite returns the OuterFlags form of one key sequence, or false when the
147// sequence is not a key event this package owns.
148func (n *Normalizer) rewrite(params []byte, final byte) ([]byte, bool) {
149	switch final {
150	case 'u':
151		if len(params) == 0 {
152			return nil, false
153		}
154		key, ok := parseParams(params)
155		if !ok {
156			return nil, false
157		}
158		n.kitty = true
159		n.track(key.ev, true)
160		return encodeOuter(key), true
161	case '~', 'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'S':
162		return n.rewriteFunctional(params, final)
163	}
164	return nil, false
165}
166
167// rewriteFunctional handles CSI number;mods[:event] ~ and CSI 1;mods[:event]
168// letter forms. Forms without a modifier field are already in OuterFlags
169// form, which also keeps bracketed paste markers and replies untouched.
170func (n *Normalizer) rewriteFunctional(params []byte, final byte) ([]byte, bool) {
171	fields := splitByte(params, ';')
172	if len(fields) != 2 {
173		return nil, false
174	}
175	mods, event, ok := parseMods(fields[1])
176	if !ok {
177		return nil, false
178	}
179	n.track(Event{Mods: mods &^ modLocks, Release: event == eventRelease}, false)
180	if event == eventRelease {
181		return nil, true
182	}
183	// Functional keys keep their lock bits under OuterFlags.
184	out := []byte("\x1b[")
185	if mods != 0 || (final == '~' || string(fields[0]) != "1") {
186		out = append(out, fields[0]...)
187	}
188	if mods != 0 {
189		out = append(out, ';')
190		out = strconv.AppendInt(out, int64(mods+1), 10)
191	}
192	return append(out, final), true
193}
194
195// track updates the Ctrl state from one key event. A Ctrl key event reports
196// the Ctrl state after it. Pressing any other key ends a Ctrl held on its own,
197// except a top-row digit, the Ctrl+1..9 recent-session switch.
198func (n *Normalizer) track(ev Event, unicodeKey bool) {
199	switch {
200	case unicodeKey && (ev.Code == keyLeftControl || ev.Code == keyRightControl):
201		n.ctrl = ev.Mods&ModCtrl != 0
202	case unicodeKey && ev.Code == 0:
203		// A text-only event carries no key.
204	case unicodeKey && isModifierOrLock(ev.Code):
205		if ev.Mods&ModCtrl == 0 {
206			n.ctrl = false
207		}
208	case ev.Mods&ModCtrl == 0:
209		n.ctrl = false
210	case ev.Release:
211	case unicodeKey:
212		if _, digit := ev.Digit(); !digit {
213			n.ctrl = false
214		}
215	default:
216		n.ctrl = false
217	}
218}
219
220// encodeOuter re-encodes one RequestFlags key event as the terminal would
221// have sent it under OuterFlags, or nil when it would have sent nothing.
222func encodeOuter(key fullKey) []byte {
223	ev := key.ev
224	if ev.Release || isModifierOrLock(ev.Code) {
225		return nil
226	}
227	switch {
228	case len(key.text) > 0 || ev.Code == 0:
229		// A terminal sends the text a key generates as is, whatever the
230		// modifiers that produced it.
231		return appendRunes(nil, key.text)
232	case ev.Mods == 0 && (ev.Code == KeyEnter || ev.Code == KeyTab || ev.Code == KeyBackspace):
233		return []byte{byte(ev.Code)}
234	case ev.Mods&^ModShift == 0 && isTextKey(ev.Code):
235		return legacyText(ev)
236	}
237	// A key sent as an escape code reports the lock bits too.
238	ev.Mods |= key.locks
239	return encodeCSIu(ev, OuterFlags)
240}
241
242func isModifierOrLock(code rune) bool {
243	return (code >= keyLeftShift && code <= keyISOLevel5) || (code >= keyCapsLock && code <= keyNumLock)
244}
245
246// isTextKey reports a key whose unmodified press produces its own text.
247func isTextKey(code rune) bool {
248	return code >= 0x20 && code != KeyBackspace && (code < 0xe000 || code > 0xf8ff) && utf8.ValidRune(code)
249}
250
251func appendRunes(out []byte, runes []rune) []byte {
252	for _, r := range runes {
253		out = utf8.AppendRune(out, r)
254	}
255	return out
256}

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