v0.4.0 5f1ad00

keyboard.go RawHistoryFolder

1package neferclient
2
3import (
4	"errors"
5	"fmt"
6	"io"
7	"os"
8	"time"
9
10	xkb "github.com/bnema/purego-xkbcommon"
11	"github.com/bnema/purego-xkbcommon/raw"
12	"golang.org/x/sys/unix"
13
14	"github.com/bnema/neferclient/internal/secret"
15)
16
17// Modifiers is the set of active keyboard modifiers.
18type Modifiers uint8
19
20const (
21	ModShift Modifiers = 1 << iota
22	ModCtrl
23	ModAlt
24	ModSuper
25	ModCapsLock
26	ModNumLock
27)
28
29// modNames are the xkb modifier names behind each Modifiers bit, in order.
30var modNames = [...]string{"Shift", "Control", "Mod1", "Mod4", "Lock", "Mod2"}
31
32const (
33	maxHeldKeys = 32
34	scratchSize = 64 // longest key or compose text kept; longer yields no text
35	keymapMax   = 16 << 20
36)
37
38// heldKey is a pressed key. A key that went into the SecretBuffer keeps no
39// keysym and is flagged hidden, so its release is reported without either.
40type heldKey struct {
41	evdev, sym uint32
42	hidden     bool
43}
44
45type textKind uint8
46
47const (
48	kindNone    textKind = iota
49	kindText             // printable text went into the SecretBuffer
50	kindPending          // a dead or compose key: part of a text sequence, no text yet
51	kindBackspace
52)
53
54// secretWork is the argument and result area of doSecret, which secret.Do runs
55// without arguments. translate clears it after every use.
56type secretWork struct {
57	evdev, sym uint32
58	mods       Modifiers
59	repeat     bool
60	kind       textKind
61	changed    bool
62	err        error
63}
64
65// keyboard interprets wl_keyboard events with xkbcommon. It runs only on the
66// owner goroutine. It never builds a string from key text: translations go
67// through the fixed scratch array, which is wiped after every use.
68type keyboard struct {
69	ctx     *xkb.Context
70	keymap  *xkb.Keymap
71	state   *xkb.State
72	compose *xkb.ComposeState
73
74	modBits  [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent
75	mods     Modifiers             // effective modifiers and layout, refreshed when the mask changes
76	group    uint32
77	lastMask [6]uint32
78	haveMask bool
79
80	held           []heldKey
81	focus          bool
82	rate           int32 // repeats per second, 0 disables
83	delay          int32 // milliseconds before the first repeat
84	repeating      uint32
85	composePending bool
86	composeUsed    bool // the last text() call consumed the key into a compose sequence
87
88	tfd  int // repeat timerfd
89	tbuf [8]byte
90
91	secret  *SecretBuffer
92	scratch [scratchSize]byte
93
94	secretFn func()
95	w        secretWork
96	changed  bool  // the last translation edited the SecretBuffer
97	timerErr error // first repeat timer failure, reported by the seat
98}
99
100func newKeyboard(locale string) (*keyboard, error) {
101	c, err := xkb.NewContext()
102	if err != nil {
103		return nil, fmt.Errorf("xkb context: %w", err)
104	}
105	k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}
106	k.secretFn = k.doSecret
107	if t, e := c.NewComposeTable(locale); e == nil {
108		k.compose, _ = t.NewState() // compose is optional: without it, dead keys still come from xkb
109		_ = t.Close()
110	}
111	if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {
112		_ = c.Close()
113		return nil, fmt.Errorf("repeat timer: %w", err)
114	}
115	return k, nil
116}
117
118// composeLocale picks the locale for the compose table the way libc does.
119func composeLocale() string {
120	for _, name := range []string{"LC_ALL", "LC_CTYPE", "LANG"} {
121		if v := os.Getenv(name); v != "" {
122			return v
123		}
124	}
125	return "C"
126}
127
128func (k *keyboard) close() {
129	k.disarm()
130	if k.tfd >= 0 {
131		_ = unix.Close(k.tfd)
132		k.tfd = -1
133	}
134	if k.compose != nil {
135		_ = k.compose.Close()
136		k.compose = nil
137	}
138	if k.state != nil {
139		_ = k.state.Close()
140		k.state = nil
141	}
142	if k.keymap != nil {
143		_ = k.keymap.Close()
144		k.keymap = nil
145	}
146	if k.ctx != nil {
147		_ = k.ctx.Close()
148		k.ctx = nil
149	}
150}
151
152// replaceFD installs the keymap in fd. It consumes fd even on failure; the
153// previous keymap stays valid on failure.
154func (k *keyboard) replaceFD(fd, size int) error {
155	f := os.NewFile(uintptr(fd), "keymap")
156	if f == nil {
157		return errors.New("neferclient: invalid keymap descriptor")
158	}
159	defer f.Close()
160	km, err := k.ctx.NewKeymapFD(fd, size)
161	if err != nil {
162		return fmt.Errorf("neferclient: compile keymap: %w", err)
163	}
164	return k.replace(km)
165}
166
167func (k *keyboard) replace(km *xkb.Keymap) error {
168	s, err := km.NewState()
169	if err != nil {
170		_ = km.Close()
171		return fmt.Errorf("neferclient: xkb state: %w", err)
172	}
173	k.reset()
174	if k.state != nil {
175		_ = k.state.Close()
176	}
177	if k.keymap != nil {
178		_ = k.keymap.Close()
179	}
180	k.keymap, k.state = km, s
181	for i, name := range modNames {
182		k.modBits[i] = 0
183		if idx, e := km.ModIndex(name); e == nil && idx < 32 {
184			k.modBits[i] = 1 << idx
185		}
186	}
187	if k.haveMask { // the compositor's modifiers survive a keymap change
188		_, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])
189	}
190	k.refresh()
191	return nil
192}
193
194func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
195	mask := [6]uint32{depressed, latched, locked, 0, 0, group}
196	if k.haveMask && mask == k.lastMask {
197		return nil // compositors repeat identical masks; the state is current
198	}
199	k.lastMask = mask
200	k.haveMask = true
201	if k.state == nil {
202		return nil
203	}
204	changed, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
205	if err != nil {
206		return fmt.Errorf("neferclient: update xkb mask: %w", err)
207	}
208	if changed != 0 {
209		k.refresh()
210	}
211	return nil
212}
213
214// refresh recomputes the cached modifiers and layout from the xkb state. Both
215// only change when the mask does, so keys do not query them.
216func (k *keyboard) refresh() {
217	k.mods = k.queryModifiers()
218	k.group, _ = k.state.Layout()
219}
220
221// reset drops held keys, repeat and compose state. It keeps the keymap, the
222// modifier mask and the focus flag.
223func (k *keyboard) reset() {
224	clear(k.held[:cap(k.held)]) // no hidden-key record outlives the reset
225	k.held = k.held[:0]
226	k.repeating = 0
227	k.disarm()
228	k.composePending = false
229	if k.compose != nil {
230		_ = k.compose.Reset()
231	}
232}
233
234func (k *keyboard) setFocus(on bool) {
235	k.focus = on
236	k.reset()
237}
238
239func (k *keyboard) setSecret(b *SecretBuffer) {
240	k.secret = b
241	k.reset() // switching paths cancels held keys, repeat and compose
242}
243
244func (k *keyboard) setRepeat(rate, delay int32) {
245	k.rate, k.delay = max(rate, 0), max(delay, 0)
246	if k.rate == 0 {
247		k.repeating = 0
248		k.disarm()
249	}
250}
251
252// arm starts the repeat timer: first expiry after the delay, then at the rate.
253func (k *keyboard) arm() {
254	spec := unix.ItimerSpec{
255		Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),
256		Value:    unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm
257	}
258	k.setTimer(&spec)
259}
260
261func (k *keyboard) disarm() {
262	if k.tfd >= 0 {
263		var spec unix.ItimerSpec
264		k.setTimer(&spec)
265	}
266}
267
268func (k *keyboard) setTimer(spec *unix.ItimerSpec) {
269	if err := unix.TimerfdSettime(k.tfd, 0, spec, nil); err != nil && k.timerErr == nil {
270		k.timerErr = fmt.Errorf("neferclient: repeat timer: %w", err)
271	}
272}
273
274// takeTimerErr returns and clears the first timer failure.
275func (k *keyboard) takeTimerErr() error {
276	err := k.timerErr
277	k.timerErr = nil
278	return err
279}
280
281// expired consumes a timer expiration. Missed ticks are not accumulated: one
282// call yields at most one repeat.
283func (k *keyboard) expired() bool {
284	n, err := unix.Read(k.tfd, k.tbuf[:])
285	return err == nil && n == 8
286}
287
288func (k *keyboard) queryModifiers() Modifiers {
289	mods, err := k.state.Mods()
290	if err != nil {
291		return 0
292	}
293	var m Modifiers
294	for i, bit := range k.modBits {
295		if bit != 0 && mods&bit != 0 {
296			m |= 1 << i
297		}
298	}
299	return m
300}
301
302func (k *keyboard) find(evdev uint32) int {
303	for i := range k.held {
304		if k.held[i].evdev == evdev {
305			return i
306		}
307	}
308	return -1
309}
310
311func (k *keyboard) drop(i int) {
312	last := len(k.held) - 1
313	copy(k.held[i:], k.held[i+1:])
314	k.held[last] = heldKey{} // clear the vacated slot
315	k.held = k.held[:last]
316}
317
318// press translates a key press into ev. It reports false when the press must
319// not be delivered (no focus or keymap).
320func (k *keyboard) press(evdev uint32, ev *KeyEvent) (bool, error) {
321	if !k.focus || k.state == nil {
322		return false, nil
323	}
324	if i := k.find(evdev); i >= 0 {
325		k.drop(i)
326	}
327	if len(k.held) == cap(k.held) {
328		return false, nil
329	}
330	sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
331	if err != nil {
332		return false, fmt.Errorf("neferclient: key symbol: %w", err)
333	}
334	kind, err := k.translate(evdev, sym, false, ev)
335	if err != nil {
336		return false, fmt.Errorf("neferclient: key text: %w", err)
337	}
338	if ev.Secret {
339		k.held = append(k.held, heldKey{evdev: evdev, hidden: true})
340	} else {
341		k.held = append(k.held, heldKey{evdev: evdev, sym: sym})
342	}
343	if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {
344		if k.rate > 0 {
345			k.repeating = evdev
346			k.arm()
347		}
348	} else if k.secret != nil && k.repeating != 0 && repeatable(sym) {
349		// A control or rejected key stops a previous printable repeat;
350		// a modifier does not.
351		k.repeating = 0
352		k.disarm()
353	}
354	return true, nil
355}
356
357// release fills ev for a key released after a delivered press.
358func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
359	i := k.find(evdev)
360	if i < 0 {
361		return false
362	}
363	h := k.held[i]
364	k.drop(i)
365	if k.repeating == evdev {
366		k.repeating = 0
367		k.disarm()
368	}
369	ev.Pressed, ev.Secret = false, h.hidden
370	if !h.hidden {
371		ev.Keycode, ev.Keysym = evdev, h.sym
372	}
373	k.changed = false
374	ev.Modifiers, ev.Group = k.mods, k.group
375	return true
376}
377
378// repeat produces one repeat of the held key whose timer fired.
379func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
380	if !k.focus || k.state == nil || k.rate <= 0 || k.repeating == 0 || k.find(k.repeating) < 0 {
381		return false, nil
382	}
383	evdev := k.repeating
384	stop := func() (bool, error) {
385		k.repeating = 0
386		k.disarm()
387		return false, nil
388	}
389	sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
390	if err != nil {
391		_, _ = stop()
392		return false, fmt.Errorf("neferclient: key symbol: %w", err)
393	}
394	if k.secret == nil && !repeatable(sym) {
395		return stop()
396	}
397	kind, err := k.translate(evdev, sym, true, ev)
398	if err != nil {
399		_, _ = stop()
400		return false, fmt.Errorf("neferclient: key text: %w", err)
401	}
402	if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {
403		return stop()
404	}
405	return true, nil
406}
407
408// translate fills ev for a press or repeat and returns what the key meant to
409// the SecretBuffer, if one is set. In secret mode ev.Text stays nil.
410func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {
411	ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat
412	ev.Modifiers, ev.Group = k.mods, k.group
413	k.changed = false
414	if k.secret != nil {
415		k.w = secretWork{evdev: evdev, sym: sym, mods: ev.Modifiers, repeat: repeat}
416		secret.Do(k.secretFn)
417		kind, changed, err := k.w.kind, k.w.changed, k.w.err
418		k.w = secretWork{} // the key identity must not stay in the work area
419		k.changed = changed
420		if kind == kindText || kind == kindPending {
421			// Text keys are not identified: the keycode and keysym would
422			// reveal the secret character.
423			ev.Keycode, ev.Keysym, ev.Secret = 0, 0, true
424		}
425		return kind, err
426	}
427	n, err := k.text(evdev, sym, repeat)
428	if err != nil {
429		return kindNone, err
430	}
431	if n > 0 {
432		ev.Text = k.scratch[:n:n]
433	}
434	return kindNone, nil
435}
436
437// doSecret translates one key into the SecretBuffer. It is the only code that
438// handles secret text, so it runs under runtime/secret when available.
439func (k *keyboard) doSecret() {
440	w := &k.w
441	defer clear(k.scratch[:])
442	switch {
443	case w.sym == raw.XKB_KEY_BackSpace:
444		w.kind = kindBackspace
445		k.resetCompose()
446		w.changed = k.secret.backspace()
447	case w.sym == raw.XKB_KEY_Return || w.sym == raw.XKB_KEY_KP_Enter || w.sym == raw.XKB_KEY_Escape:
448		k.resetCompose() // controls are delivered as keys only
449	case w.mods&ModCtrl != 0:
450		// Other Control combinations are neither secret text nor ours to handle.
451	default:
452		n, err := k.text(w.evdev, w.sym, w.repeat)
453		if err != nil {
454			w.err = err
455			return
456		}
457		switch {
458		case classifySecretText(k.scratch[:n]):
459			w.kind = kindText
460			w.changed = k.secret.appendText(k.scratch[:n])
461		case k.composeUsed || k.composePending || isDeadKey(w.sym):
462			w.kind = kindPending // dead and compose keys name an accent, so hide them too
463		}
464	}
465}
466
467func (k *keyboard) resetCompose() {
468	if k.compose != nil {
469		_ = k.compose.Reset()
470	}
471	k.composePending = false
472}
473
474// text writes the text of a key into scratch and returns its length. Compose
475// is fed only by physical presses: a repeat neither advances a sequence nor
476// replays composed text.
477func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
478	k.composeUsed = false
479	if k.compose != nil && !repeat {
480		if _, err := k.compose.Feed(sym); err != nil {
481			return 0, err
482		}
483		status, err := k.compose.Status()
484		if err != nil {
485			return 0, err
486		}
487		k.composeUsed = status == raw.XKB_COMPOSE_COMPOSING || status == raw.XKB_COMPOSE_CANCELLED || status == raw.XKB_COMPOSE_COMPOSED
488		switch status {
489		case raw.XKB_COMPOSE_COMPOSING:
490			k.composePending = true
491			return 0, nil
492		case raw.XKB_COMPOSE_CANCELLED:
493			k.resetCompose()
494			return 0, nil
495		case raw.XKB_COMPOSE_COMPOSED:
496			n, err := k.compose.UTF8Into(k.scratch[:])
497			k.resetCompose()
498			return shortAsEmpty(n, err)
499		default:
500			k.composePending = false
501		}
502	}
503	if k.composePending {
504		return 0, nil
505	}
506	n, err := k.state.UTF8Into(xkb.WaylandKeycode(evdev), k.scratch[:])
507	return shortAsEmpty(n, err)
508}
509
510// shortAsEmpty maps a too-long text to no text; xkb already wiped the buffer.
511func shortAsEmpty(n int, err error) (int, error) {
512	if errors.Is(err, io.ErrShortBuffer) {
513		return 0, nil
514	}
515	return n, err
516}
517
518// Modifier, dead and compose keys never repeat. The compositor provides rate
519// and delay; its keymap is authoritative for text and layout.
520func repeatable(sym uint32) bool {
521	return !isModifierKey(sym) && !isDeadKey(sym)
522}
523
524func isModifierKey(sym uint32) bool {
525	switch sym {
526	case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
527		raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
528		raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock:
529		return true
530	}
531	return false
532}
533
534// isDeadKey reports dead keys and the compose key, which start a sequence.
535func isDeadKey(sym uint32) bool {
536	return sym == raw.XKB_KEY_Multi_key || sym >= raw.XKB_KEY_dead_grave && sym <= raw.XKB_KEY_dead_currency
537}

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 4

↑↓ moveenter openesc close