v0.3.0 6800c6a

secret.go RawHistoryFolder

1package keyboard
2
3import (
4	"errors"
5	"time"
6	"unicode/utf8"
7
8	xkb "github.com/bnema/purego-xkbcommon"
9	"github.com/bnema/purego-xkbcommon/raw"
10)
11
12// SecretKind is the limited editing vocabulary accepted by secret consumers.
13type SecretKind uint8
14
15const (
16	SecretNone SecretKind = iota
17	SecretBytes
18	SecretBackspace
19	SecretReturn
20	SecretEscape
21	SecretClear // Control+U
22)
23
24// SecretKey contains scalar metadata only, never names or text. Bytes are
25// supplied separately and exclusively within EventSecret/TickSecret callbacks.
26type SecretKey struct {
27	Physical, Logical uint32
28	Kind              SecretKind
29	Pressed, Repeat   bool
30}
31
32var errSecretCallback = errors.New("keyboard: secret callback is nil")
33
34// ClearSecret cancels secret-held keys, repeat and local compose. It retains
35// the compositor's current keymap and modifier mask. No text is cached here.
36func (k *Keyboard) ClearSecret() {
37	clear(k.secretHeld)
38	clear(k.held) // fail-closed even when secret entry fails from ordinary mode
39	k.repeating = 0
40	k.next = time.Time{}
41	k.composePending = false
42	if k.compose != nil {
43		_ = k.compose.Reset()
44	}
45}
46
47// EventSecret interprets a physical event without calling any string-producing
48// XKB API or populating Key/held. scratch length must be 1..512, including space
49// for native NUL (max payload 511). The callback is synchronous and must not
50// retain bytes or convert them to strings. scratch is valid only for that call;
51// this method clears it on entry and every exit, in addition to caller wiping.
52// Enter, Escape and Control+U are initial-press controls, never repeats.
53// Release callbacks carry no bytes and Pressed=false; consumers must act on
54// controls only when Pressed=true. Switching APIs cancels the other path.
55func (k *Keyboard) EventSecret(evdev uint32, pressed bool, now time.Time, scratch []byte, consume func(SecretKey, []byte)) error {
56	clear(scratch)
57	defer clear(scratch)
58	if len(scratch) < 1 || len(scratch) > xkb.MaxUTF8Buffer {
59		k.ClearSecret()
60		return xkb.ErrUTF8Buffer
61	}
62	if consume == nil {
63		k.ClearSecret()
64		return errSecretCallback
65	}
66	if !k.secretMode {
67		k.Focus(k.focus) // discard ordinary held text and compose on mode entry
68		k.secretMode = true
69		if k.secretHeld == nil {
70			k.secretHeld = make(map[uint32]SecretKey)
71		}
72	}
73	if !pressed {
74		key, ok := k.secretHeld[evdev]
75		delete(k.secretHeld, evdev)
76		if k.repeating == evdev {
77			k.repeating = 0
78		}
79		if ok && key.Kind != SecretNone {
80			key.Pressed = false
81			consume(key, nil)
82		}
83		return nil
84	}
85	if !k.focus || k.state == nil {
86		return nil
87	}
88	_, repeat := k.secretHeld[evdev]
89	key, n, err := k.secretKey(evdev, repeat, scratch)
90	if err != nil {
91		k.ClearSecret()
92		return err
93	}
94	k.secretHeld[evdev] = key
95	if key.Kind == SecretReturn || key.Kind == SecretEscape || key.Kind == SecretClear {
96		// Clear/submit/cancel must stop a previously held printable repeat
97		// before the consumer runs; releasing Control must not resurrect it.
98		k.repeating = 0
99		k.next = time.Time{}
100	}
101	if k.rate > 0 && secretRepeatable(key) {
102		k.repeating = evdev
103		k.next = now.Add(k.delay)
104	}
105	if key.Kind != SecretNone {
106		consume(key, scratch[:n:n])
107	}
108	return nil
109}
110
111func secretRepeatable(key SecretKey) bool {
112	return key.Kind == SecretBackspace || key.Kind == SecretBytes && repeatable(key.Logical)
113}
114
115func (k *Keyboard) secretKey(evdev uint32, repeat bool, scratch []byte) (SecretKey, int, error) {
116	code := xkb.WaylandKeycode(evdev)
117	sym, err := k.state.KeySym(code)
118	if err != nil {
119		return SecretKey{}, 0, err
120	}
121	key := SecretKey{Physical: evdev, Logical: sym, Pressed: true, Repeat: repeat}
122	_, ctrl, err := k.modifiers()
123	if err != nil {
124		return SecretKey{}, 0, err
125	}
126	switch {
127	case sym == raw.XKB_KEY_BackSpace:
128		key.Kind = SecretBackspace
129	case sym == raw.XKB_KEY_Return || sym == raw.XKB_KEY_KP_Enter:
130		key.Kind = SecretReturn
131	case sym == raw.XKB_KEY_Escape:
132		key.Kind = SecretEscape
133	case ctrl && (sym == raw.XKB_KEY_u || sym == raw.XKB_KEY_U):
134		key.Kind = SecretClear
135	}
136	if key.Kind != SecretNone {
137		if k.compose != nil {
138			_ = k.compose.Reset()
139		}
140		k.composePending = false
141		if repeat && key.Kind != SecretBackspace {
142			key.Kind = SecretNone
143		}
144		return key, 0, nil
145	}
146	// Other Control combinations are not secret bytes or application shortcuts.
147	if ctrl {
148		return key, 0, nil
149	}
150	if k.compose != nil && !repeat {
151		if _, err := k.compose.Feed(sym); err != nil {
152			return key, 0, err
153		}
154		status, err := k.compose.Status()
155		if err != nil {
156			return key, 0, err
157		}
158		switch status {
159		case raw.XKB_COMPOSE_COMPOSING:
160			k.composePending = true
161			return key, 0, nil
162		case raw.XKB_COMPOSE_CANCELLED:
163			_ = k.compose.Reset()
164			k.composePending = false
165			return key, 0, nil
166		case raw.XKB_COMPOSE_COMPOSED:
167			n, err := k.compose.UTF8Into(scratch)
168			_ = k.compose.Reset()
169			k.composePending = false
170			if err != nil {
171				return key, 0, err
172			}
173			return classifySecretBytes(key, scratch, n), n, nil
174		default:
175			k.composePending = false
176		}
177	}
178	if k.composePending {
179		return key, 0, nil
180	}
181	n, err := k.state.UTF8Into(code, scratch)
182	if err != nil {
183		return key, 0, err
184	}
185	return classifySecretBytes(key, scratch, n), n, nil
186}
187
188func classifySecretBytes(key SecretKey, scratch []byte, n int) SecretKey {
189	if n == 0 || !utf8.Valid(scratch[:n]) {
190		return key
191	}
192	for rest := scratch[:n]; len(rest) > 0; {
193		r, size := utf8.DecodeRune(rest)
194		if r < 0x20 || r == 0x7f {
195			return key
196		}
197		rest = rest[size:]
198	}
199	key.Kind = SecretBytes
200	return key
201}
202
203// TickSecret resolves repeat bytes using the current mask/group, not press-time
204// plaintext. Compose is never fed by repeats. It reports whether a callback ran.
205func (k *Keyboard) TickSecret(now time.Time, scratch []byte, consume func(SecretKey, []byte)) (bool, error) {
206	clear(scratch)
207	defer clear(scratch)
208	if len(scratch) < 1 || len(scratch) > xkb.MaxUTF8Buffer {
209		k.ClearSecret()
210		return false, xkb.ErrUTF8Buffer
211	}
212	if consume == nil {
213		k.ClearSecret()
214		return false, errSecretCallback
215	}
216	if !k.secretMode || !k.focus || k.rate <= 0 || k.repeating == 0 || now.Before(k.next) {
217		return false, nil
218	}
219	if _, ok := k.secretHeld[k.repeating]; !ok {
220		return false, nil
221	}
222	key, n, err := k.secretKey(k.repeating, true, scratch)
223	if err != nil {
224		k.ClearSecret()
225		return false, err
226	}
227	if !secretRepeatable(key) {
228		k.repeating = 0
229		return false, nil
230	}
231	k.next = now.Add(time.Second / time.Duration(k.rate))
232	consume(key, scratch[:n:n])
233	return true, nil
234}

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 12

↑↓ moveenter openesc close