detached 869d03d

fix: hide text keys in secret mode, wipe key residue, skip identical masks, wrap errors

commit
869d03d7a8993b1767a6b8f8f55610a7d89455ee
author
brice · · 2026-10-01 18:57 UTC
parent
be6ebbf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +155 -66
M handler.go +4 -1
@@ -59,7 +59,10 @@ type Handler interface {
5959 // keyboard focus. Text is the text the key produced (nil for releases and
6060 // keys without text) and is valid only during the call. While a
6161 // [SecretBuffer] is installed with [Seat.SetSecret], Text is always nil
62- // and the text goes into the buffer; see [Handler.SecretChanged]. ev is
62+ // and the text goes into the buffer; see [Handler.SecretChanged]. For
63+ // those keys (press, repeat and release) ev.Secret is set and Keycode and
64+ // Keysym are 0, so the event does not reveal the typed character; Enter,
65+ // Escape, BackSpace and Control combinations keep their keysym. ev is
6366 // valid only during the call.
6467 Key(ev *KeyEvent)
6568 // KeyboardFocus reports that a surface gained or lost keyboard focus.
@@ -59,7 +59,10 @@ type Handler interface {
59 // keyboard focus. Text is the text the key produced (nil for releases and59 // keyboard focus. Text is the text the key produced (nil for releases and
60 // keys without text) and is valid only during the call. While a60 // keys without text) and is valid only during the call. While a
61 // [SecretBuffer] is installed with [Seat.SetSecret], Text is always nil61 // [SecretBuffer] is installed with [Seat.SetSecret], Text is always nil
62- // and the text goes into the buffer; see [Handler.SecretChanged]. ev is62+ // and the text goes into the buffer; see [Handler.SecretChanged]. For
63+ // those keys (press, repeat and release) ev.Secret is set and Keycode and
64+ // Keysym are 0, so the event does not reveal the typed character; Enter,
65+ // Escape, BackSpace and Control combinations keep their keysym. ev is
63 // valid only during the call.66 // valid only during the call.
64 Key(ev *KeyEvent)67 Key(ev *KeyEvent)
65 // KeyboardFocus reports that a surface gained or lost keyboard focus.68 // KeyboardFocus reports that a surface gained or lost keyboard focus.
M keyboard.go +106 -40
@@ -2,6 +2,7 @@ package neferclient
22
33 import (
44 "errors"
5+ "fmt"
56 "io"
67 "os"
78 "time"
@@ -34,16 +35,33 @@ const (
3435 keymapMax = 16 << 20
3536 )
3637
37-type heldKey struct{ evdev, sym uint32 }
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.
40+type heldKey struct {
41+ evdev, sym uint32
42+ hidden bool
43+}
3844
3945 type textKind uint8
4046
4147 const (
42- kindNone textKind = iota
43- kindText
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
4451 kindBackspace
4552 )
4653
54+// secretWork is the argument and result area of doSecret, which secret.Do runs
55+// without arguments. translate clears it after every use.
56+type secretWork struct {
57+ evdev, sym uint32
58+ mods Modifiers
59+ repeat bool
60+ kind textKind
61+ changed bool
62+ err error
63+}
64+
4765 // keyboard interprets wl_keyboard events with xkbcommon. It runs only on the
4866 // owner goroutine. It never builds a string from key text: translations go
4967 // through the fixed scratch array, which is wiped after every use.
@@ -65,6 +83,7 @@ type keyboard struct {
6583 delay int32 // milliseconds before the first repeat
6684 repeating uint32
6785 composePending bool
86+ composeUsed bool // the last text() call consumed the key into a compose sequence
6887
6988 tfd int // repeat timerfd
7089 tbuf [8]byte
@@ -72,23 +91,16 @@ type keyboard struct {
7291 secret *SecretBuffer
7392 scratch [scratchSize]byte
7493
75- // Work area of doSecret, which secret.Do runs without arguments.
7694 secretFn func()
77- w struct {
78- evdev, sym uint32
79- mods Modifiers
80- repeat bool
81- kind textKind
82- changed bool
83- err error
84- }
85- changed bool // the last translation edited the SecretBuffer
95+ w secretWork
96+ changed bool // the last translation edited the SecretBuffer
97+ timerErr error // first repeat timer failure, reported by the seat
8698 }
8799
88100 func newKeyboard(locale string) (*keyboard, error) {
89101 c, err := xkb.NewContext()
90102 if err != nil {
91- return nil, err
103+ return nil, fmt.Errorf("xkb context: %w", err)
92104 }
93105 k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}
94106 k.secretFn = k.doSecret
@@ -98,7 +110,7 @@ func newKeyboard(locale string) (*keyboard, error) {
98110 }
99111 if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {
100112 _ = c.Close()
101- return nil, err
113+ return nil, fmt.Errorf("repeat timer: %w", err)
102114 }
103115 return k, nil
104116 }
@@ -147,7 +159,7 @@ func (k *keyboard) replaceFD(fd, size int) error {
147159 defer f.Close()
148160 km, err := k.ctx.NewKeymapFD(fd, size)
149161 if err != nil {
150- return err
162+ return fmt.Errorf("neferclient: compile keymap: %w", err)
151163 }
152164 return k.replace(km)
153165 }
@@ -156,7 +168,7 @@ func (k *keyboard) replace(km *xkb.Keymap) error {
156168 s, err := km.NewState()
157169 if err != nil {
158170 _ = km.Close()
159- return err
171+ return fmt.Errorf("neferclient: xkb state: %w", err)
160172 }
161173 k.reset()
162174 if k.state != nil {
@@ -180,26 +192,36 @@ func (k *keyboard) replace(km *xkb.Keymap) error {
180192 }
181193
182194 func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
183- k.lastMask = [6]uint32{depressed, latched, locked, 0, 0, group}
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
184200 k.haveMask = true
185201 if k.state == nil {
186202 return nil
187203 }
188- _, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
189- k.refresh()
190- return err
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
191212 }
192213
193214 // refresh recomputes the cached modifiers and layout from the xkb state. Both
194215 // only change when the mask does, so keys do not query them.
195216 func (k *keyboard) refresh() {
196- k.mods, k.group = k.queryModifiers(), 0
217+ k.mods = k.queryModifiers()
197218 k.group, _ = k.state.Layout()
198219 }
199220
200221 // reset drops held keys, repeat and compose state. It keeps the keymap, the
201222 // modifier mask and the focus flag.
202223 func (k *keyboard) reset() {
224+ clear(k.held[:cap(k.held)]) // no hidden-key record outlives the reset
203225 k.held = k.held[:0]
204226 k.repeating = 0
205227 k.disarm()
@@ -233,16 +255,29 @@ func (k *keyboard) arm() {
233255 Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),
234256 Value: unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm
235257 }
236- _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)
258+ k.setTimer(&spec)
237259 }
238260
239261 func (k *keyboard) disarm() {
240262 if k.tfd >= 0 {
241263 var spec unix.ItimerSpec
242- _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)
264+ k.setTimer(&spec)
265+ }
266+}
267+
268+func (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)
243271 }
244272 }
245273
274+// takeTimerErr returns and clears the first timer failure.
275+func (k *keyboard) takeTimerErr() error {
276+ err := k.timerErr
277+ k.timerErr = nil
278+ return err
279+}
280+
246281 // expired consumes a timer expiration. Missed ticks are not accumulated: one
247282 // call yields at most one repeat.
248283 func (k *keyboard) expired() bool {
@@ -273,7 +308,12 @@ func (k *keyboard) find(evdev uint32) int {
273308 return -1
274309 }
275310
276-func (k *keyboard) drop(i int) { k.held = append(k.held[:i], k.held[i+1:]...) }
311+func (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+}
277317
278318 // press translates a key press into ev. It reports false when the press must
279319 // not be delivered (no focus or keymap).
@@ -289,13 +329,17 @@ func (k *keyboard) press(evdev uint32, ev *KeyEvent) (bool, error) {
289329 }
290330 sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
291331 if err != nil {
292- return false, err
332+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
293333 }
294334 kind, err := k.translate(evdev, sym, false, ev)
295335 if err != nil {
296- return false, err
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})
297342 }
298- k.held = append(k.held, heldKey{evdev, sym})
299343 if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {
300344 if k.rate > 0 {
301345 k.repeating = evdev
@@ -322,7 +366,10 @@ func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
322366 k.repeating = 0
323367 k.disarm()
324368 }
325- ev.Keycode, ev.Keysym, ev.Pressed = evdev, h.sym, false
369+ ev.Pressed, ev.Secret = false, h.hidden
370+ if !h.hidden {
371+ ev.Keycode, ev.Keysym = evdev, h.sym
372+ }
326373 k.changed = false
327374 ev.Modifiers, ev.Group = k.mods, k.group
328375 return true
@@ -342,7 +389,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
342389 sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
343390 if err != nil {
344391 _, _ = stop()
345- return false, err
392+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
346393 }
347394 if k.secret == nil && !repeatable(sym) {
348395 return stop()
@@ -350,7 +397,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
350397 kind, err := k.translate(evdev, sym, true, ev)
351398 if err != nil {
352399 _, _ = stop()
353- return false, err
400+ return false, fmt.Errorf("neferclient: key text: %w", err)
354401 }
355402 if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {
356403 return stop()
@@ -365,11 +412,17 @@ func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (text
365412 ev.Modifiers, ev.Group = k.mods, k.group
366413 k.changed = false
367414 if k.secret != nil {
368- w := &k.w
369- w.evdev, w.sym, w.mods, w.repeat = evdev, sym, ev.Modifiers, repeat
415+ k.w = secretWork{evdev: evdev, sym: sym, mods: ev.Modifiers, repeat: repeat}
370416 secret.Do(k.secretFn)
371- k.changed = w.changed
372- return w.kind, w.err
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
373426 }
374427 n, err := k.text(evdev, sym, repeat)
375428 if err != nil {
@@ -385,7 +438,6 @@ func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (text
385438 // handles secret text, so it runs under runtime/secret when available.
386439 func (k *keyboard) doSecret() {
387440 w := &k.w
388- w.kind, w.changed, w.err = kindNone, false, nil
389441 defer clear(k.scratch[:])
390442 switch {
391443 case w.sym == raw.XKB_KEY_BackSpace:
@@ -402,9 +454,12 @@ func (k *keyboard) doSecret() {
402454 w.err = err
403455 return
404456 }
405- if classifySecretText(k.scratch[:n]) {
457+ switch {
458+ case classifySecretText(k.scratch[:n]):
406459 w.kind = kindText
407460 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
408463 }
409464 }
410465 }
@@ -420,6 +475,7 @@ func (k *keyboard) resetCompose() {
420475 // is fed only by physical presses: a repeat neither advances a sequence nor
421476 // replays composed text.
422477 func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
478+ k.composeUsed = false
423479 if k.compose != nil && !repeat {
424480 if _, err := k.compose.Feed(sym); err != nil {
425481 return 0, err
@@ -428,6 +484,7 @@ func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
428484 if err != nil {
429485 return 0, err
430486 }
487+ k.composeUsed = status == raw.XKB_COMPOSE_COMPOSING || status == raw.XKB_COMPOSE_CANCELLED || status == raw.XKB_COMPOSE_COMPOSED
431488 switch status {
432489 case raw.XKB_COMPOSE_COMPOSING:
433490 k.composePending = true
@@ -461,11 +518,20 @@ func shortAsEmpty(n int, err error) (int, error) {
461518 // Modifier, dead and compose keys never repeat. The compositor provides rate
462519 // and delay; its keymap is authoritative for text and layout.
463520 func repeatable(sym uint32) bool {
521+ return !isModifierKey(sym) && !isDeadKey(sym)
522+}
523+
524+func isModifierKey(sym uint32) bool {
464525 switch sym {
465526 case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
466527 raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
467- raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock, raw.XKB_KEY_Multi_key:
468- return false
528+ raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock:
529+ return true
469530 }
470- return sym < raw.XKB_KEY_dead_grave || sym > raw.XKB_KEY_dead_currency
531+ return false
532+}
533+
534+// isDeadKey reports dead keys and the compose key, which start a sequence.
535+func isDeadKey(sym uint32) bool {
536+ return sym == raw.XKB_KEY_Multi_key || sym >= raw.XKB_KEY_dead_grave && sym <= raw.XKB_KEY_dead_currency
471537 }
@@ -2,6 +2,7 @@ package neferclient
2 2
3 import (3 import (
4 "errors"4 "errors"
5+ "fmt"
5 "io"6 "io"
6 "os"7 "os"
7 "time"8 "time"
@@ -34,16 +35,33 @@ const (
34 keymapMax = 16 << 2035 keymapMax = 16 << 20
35 )36 )
36 37
37-type heldKey struct{ evdev, sym uint32 }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.
40+type heldKey struct {
41+ evdev, sym uint32
42+ hidden bool
43+}
38 44
39 type textKind uint845 type textKind uint8
40 46
41 const (47 const (
42- kindNone textKind = iota48+ kindNone textKind = iota
43- kindText49+ kindText // printable text went into the SecretBuffer
50+ kindPending // a dead or compose key: part of a text sequence, no text yet
44 kindBackspace51 kindBackspace
45 )52 )
46 53
54+// secretWork is the argument and result area of doSecret, which secret.Do runs
55+// without arguments. translate clears it after every use.
56+type secretWork struct {
57+ evdev, sym uint32
58+ mods Modifiers
59+ repeat bool
60+ kind textKind
61+ changed bool
62+ err error
63+}
64+
47 // keyboard interprets wl_keyboard events with xkbcommon. It runs only on the65 // keyboard interprets wl_keyboard events with xkbcommon. It runs only on the
48 // owner goroutine. It never builds a string from key text: translations go66 // owner goroutine. It never builds a string from key text: translations go
49 // through the fixed scratch array, which is wiped after every use.67 // through the fixed scratch array, which is wiped after every use.
@@ -65,6 +83,7 @@ type keyboard struct {
65 delay int32 // milliseconds before the first repeat83 delay int32 // milliseconds before the first repeat
66 repeating uint3284 repeating uint32
67 composePending bool85 composePending bool
86+ composeUsed bool // the last text() call consumed the key into a compose sequence
68 87
69 tfd int // repeat timerfd88 tfd int // repeat timerfd
70 tbuf [8]byte89 tbuf [8]byte
@@ -72,23 +91,16 @@ type keyboard struct {
72 secret *SecretBuffer91 secret *SecretBuffer
73 scratch [scratchSize]byte92 scratch [scratchSize]byte
74 93
75- // Work area of doSecret, which secret.Do runs without arguments.
76 secretFn func()94 secretFn func()
77- w struct {95+ w secretWork
78- evdev, sym uint3296+ changed bool // the last translation edited the SecretBuffer
79- mods Modifiers97+ timerErr error // first repeat timer failure, reported by the seat
80- repeat bool
81- kind textKind
82- changed bool
83- err error
84- }
85- changed bool // the last translation edited the SecretBuffer
86 }98 }
87 99
88 func newKeyboard(locale string) (*keyboard, error) {100 func newKeyboard(locale string) (*keyboard, error) {
89 c, err := xkb.NewContext()101 c, err := xkb.NewContext()
90 if err != nil {102 if err != nil {
91- return nil, err103+ return nil, fmt.Errorf("xkb context: %w", err)
92 }104 }
93 k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}105 k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}
94 k.secretFn = k.doSecret106 k.secretFn = k.doSecret
@@ -98,7 +110,7 @@ func newKeyboard(locale string) (*keyboard, error) {
98 }110 }
99 if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {111 if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {
100 _ = c.Close()112 _ = c.Close()
101- return nil, err113+ return nil, fmt.Errorf("repeat timer: %w", err)
102 }114 }
103 return k, nil115 return k, nil
104 }116 }
@@ -147,7 +159,7 @@ func (k *keyboard) replaceFD(fd, size int) error {
147 defer f.Close()159 defer f.Close()
148 km, err := k.ctx.NewKeymapFD(fd, size)160 km, err := k.ctx.NewKeymapFD(fd, size)
149 if err != nil {161 if err != nil {
150- return err162+ return fmt.Errorf("neferclient: compile keymap: %w", err)
151 }163 }
152 return k.replace(km)164 return k.replace(km)
153 }165 }
@@ -156,7 +168,7 @@ func (k *keyboard) replace(km *xkb.Keymap) error {
156 s, err := km.NewState()168 s, err := km.NewState()
157 if err != nil {169 if err != nil {
158 _ = km.Close()170 _ = km.Close()
159- return err171+ return fmt.Errorf("neferclient: xkb state: %w", err)
160 }172 }
161 k.reset()173 k.reset()
162 if k.state != nil {174 if k.state != nil {
@@ -180,26 +192,36 @@ func (k *keyboard) replace(km *xkb.Keymap) error {
180 }192 }
181 193
182 func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {194 func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
183- k.lastMask = [6]uint32{depressed, latched, locked, 0, 0, group}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
184 k.haveMask = true200 k.haveMask = true
185 if k.state == nil {201 if k.state == nil {
186 return nil202 return nil
187 }203 }
188- _, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)204+ changed, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
189- k.refresh()205+ if err != nil {
190- return err206+ return fmt.Errorf("neferclient: update xkb mask: %w", err)
207+ }
208+ if changed != 0 {
209+ k.refresh()
210+ }
211+ return nil
191 }212 }
192 213
193 // refresh recomputes the cached modifiers and layout from the xkb state. Both214 // refresh recomputes the cached modifiers and layout from the xkb state. Both
194 // only change when the mask does, so keys do not query them.215 // only change when the mask does, so keys do not query them.
195 func (k *keyboard) refresh() {216 func (k *keyboard) refresh() {
196- k.mods, k.group = k.queryModifiers(), 0217+ k.mods = k.queryModifiers()
197 k.group, _ = k.state.Layout()218 k.group, _ = k.state.Layout()
198 }219 }
199 220
200 // reset drops held keys, repeat and compose state. It keeps the keymap, the221 // reset drops held keys, repeat and compose state. It keeps the keymap, the
201 // modifier mask and the focus flag.222 // modifier mask and the focus flag.
202 func (k *keyboard) reset() {223 func (k *keyboard) reset() {
224+ clear(k.held[:cap(k.held)]) // no hidden-key record outlives the reset
203 k.held = k.held[:0]225 k.held = k.held[:0]
204 k.repeating = 0226 k.repeating = 0
205 k.disarm()227 k.disarm()
@@ -233,16 +255,29 @@ func (k *keyboard) arm() {
233 Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),255 Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),
234 Value: unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm256 Value: unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm
235 }257 }
236- _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)258+ k.setTimer(&spec)
237 }259 }
238 260
239 func (k *keyboard) disarm() {261 func (k *keyboard) disarm() {
240 if k.tfd >= 0 {262 if k.tfd >= 0 {
241 var spec unix.ItimerSpec263 var spec unix.ItimerSpec
242- _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)264+ k.setTimer(&spec)
265+ }
266+}
267+
268+func (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)
243 }271 }
244 }272 }
245 273
274+// takeTimerErr returns and clears the first timer failure.
275+func (k *keyboard) takeTimerErr() error {
276+ err := k.timerErr
277+ k.timerErr = nil
278+ return err
279+}
280+
246 // expired consumes a timer expiration. Missed ticks are not accumulated: one281 // expired consumes a timer expiration. Missed ticks are not accumulated: one
247 // call yields at most one repeat.282 // call yields at most one repeat.
248 func (k *keyboard) expired() bool {283 func (k *keyboard) expired() bool {
@@ -273,7 +308,12 @@ func (k *keyboard) find(evdev uint32) int {
273 return -1308 return -1
274 }309 }
275 310
276-func (k *keyboard) drop(i int) { k.held = append(k.held[:i], k.held[i+1:]...) }311+func (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+}
277 317
278 // press translates a key press into ev. It reports false when the press must318 // press translates a key press into ev. It reports false when the press must
279 // not be delivered (no focus or keymap).319 // not be delivered (no focus or keymap).
@@ -289,13 +329,17 @@ func (k *keyboard) press(evdev uint32, ev *KeyEvent) (bool, error) {
289 }329 }
290 sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))330 sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
291 if err != nil {331 if err != nil {
292- return false, err332+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
293 }333 }
294 kind, err := k.translate(evdev, sym, false, ev)334 kind, err := k.translate(evdev, sym, false, ev)
295 if err != nil {335 if err != nil {
296- return false, err336+ 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})
297 }342 }
298- k.held = append(k.held, heldKey{evdev, sym})
299 if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {343 if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {
300 if k.rate > 0 {344 if k.rate > 0 {
301 k.repeating = evdev345 k.repeating = evdev
@@ -322,7 +366,10 @@ func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
322 k.repeating = 0366 k.repeating = 0
323 k.disarm()367 k.disarm()
324 }368 }
325- ev.Keycode, ev.Keysym, ev.Pressed = evdev, h.sym, false369+ ev.Pressed, ev.Secret = false, h.hidden
370+ if !h.hidden {
371+ ev.Keycode, ev.Keysym = evdev, h.sym
372+ }
326 k.changed = false373 k.changed = false
327 ev.Modifiers, ev.Group = k.mods, k.group374 ev.Modifiers, ev.Group = k.mods, k.group
328 return true375 return true
@@ -342,7 +389,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
342 sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))389 sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
343 if err != nil {390 if err != nil {
344 _, _ = stop()391 _, _ = stop()
345- return false, err392+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
346 }393 }
347 if k.secret == nil && !repeatable(sym) {394 if k.secret == nil && !repeatable(sym) {
348 return stop()395 return stop()
@@ -350,7 +397,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
350 kind, err := k.translate(evdev, sym, true, ev)397 kind, err := k.translate(evdev, sym, true, ev)
351 if err != nil {398 if err != nil {
352 _, _ = stop()399 _, _ = stop()
353- return false, err400+ return false, fmt.Errorf("neferclient: key text: %w", err)
354 }401 }
355 if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {402 if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {
356 return stop()403 return stop()
@@ -365,11 +412,17 @@ func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (text
365 ev.Modifiers, ev.Group = k.mods, k.group412 ev.Modifiers, ev.Group = k.mods, k.group
366 k.changed = false413 k.changed = false
367 if k.secret != nil {414 if k.secret != nil {
368- w := &k.w415+ k.w = secretWork{evdev: evdev, sym: sym, mods: ev.Modifiers, repeat: repeat}
369- w.evdev, w.sym, w.mods, w.repeat = evdev, sym, ev.Modifiers, repeat
370 secret.Do(k.secretFn)416 secret.Do(k.secretFn)
371- k.changed = w.changed417+ kind, changed, err := k.w.kind, k.w.changed, k.w.err
372- return w.kind, w.err418+ 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
373 }426 }
374 n, err := k.text(evdev, sym, repeat)427 n, err := k.text(evdev, sym, repeat)
375 if err != nil {428 if err != nil {
@@ -385,7 +438,6 @@ func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (text
385 // handles secret text, so it runs under runtime/secret when available.438 // handles secret text, so it runs under runtime/secret when available.
386 func (k *keyboard) doSecret() {439 func (k *keyboard) doSecret() {
387 w := &k.w440 w := &k.w
388- w.kind, w.changed, w.err = kindNone, false, nil
389 defer clear(k.scratch[:])441 defer clear(k.scratch[:])
390 switch {442 switch {
391 case w.sym == raw.XKB_KEY_BackSpace:443 case w.sym == raw.XKB_KEY_BackSpace:
@@ -402,9 +454,12 @@ func (k *keyboard) doSecret() {
402 w.err = err454 w.err = err
403 return455 return
404 }456 }
405- if classifySecretText(k.scratch[:n]) {457+ switch {
458+ case classifySecretText(k.scratch[:n]):
406 w.kind = kindText459 w.kind = kindText
407 w.changed = k.secret.appendText(k.scratch[:n])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
408 }463 }
409 }464 }
410 }465 }
@@ -420,6 +475,7 @@ func (k *keyboard) resetCompose() {
420 // is fed only by physical presses: a repeat neither advances a sequence nor475 // is fed only by physical presses: a repeat neither advances a sequence nor
421 // replays composed text.476 // replays composed text.
422 func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {477 func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
478+ k.composeUsed = false
423 if k.compose != nil && !repeat {479 if k.compose != nil && !repeat {
424 if _, err := k.compose.Feed(sym); err != nil {480 if _, err := k.compose.Feed(sym); err != nil {
425 return 0, err481 return 0, err
@@ -428,6 +484,7 @@ func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
428 if err != nil {484 if err != nil {
429 return 0, err485 return 0, err
430 }486 }
487+ k.composeUsed = status == raw.XKB_COMPOSE_COMPOSING || status == raw.XKB_COMPOSE_CANCELLED || status == raw.XKB_COMPOSE_COMPOSED
431 switch status {488 switch status {
432 case raw.XKB_COMPOSE_COMPOSING:489 case raw.XKB_COMPOSE_COMPOSING:
433 k.composePending = true490 k.composePending = true
@@ -461,11 +518,20 @@ func shortAsEmpty(n int, err error) (int, error) {
461 // Modifier, dead and compose keys never repeat. The compositor provides rate518 // Modifier, dead and compose keys never repeat. The compositor provides rate
462 // and delay; its keymap is authoritative for text and layout.519 // and delay; its keymap is authoritative for text and layout.
463 func repeatable(sym uint32) bool {520 func repeatable(sym uint32) bool {
521+ return !isModifierKey(sym) && !isDeadKey(sym)
522+}
523+
524+func isModifierKey(sym uint32) bool {
464 switch sym {525 switch sym {
465 case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,526 case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
466 raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,527 raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
467- raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock, raw.XKB_KEY_Multi_key:528+ raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock:
468- return false529+ return true
469 }530 }
470- return sym < raw.XKB_KEY_dead_grave || sym > raw.XKB_KEY_dead_currency531+ return false
532+}
533+
534+// isDeadKey reports dead keys and the compose key, which start a sequence.
535+func isDeadKey(sym uint32) bool {
536+ return sym == raw.XKB_KEY_Multi_key || sym >= raw.XKB_KEY_dead_grave && sym <= raw.XKB_KEY_dead_currency
471 }537 }
M queue.go +1 -1
@@ -170,7 +170,7 @@ func (q *queue) pop(ev *event) bool {
170170 return false
171171 }
172172 *ev = q.ring[q.head]
173- q.ring[q.head].kind = 0 // ownership of a carried descriptor moved to ev
173+ q.ring[q.head] = event{} // ownership of a carried descriptor moved to ev; leave no key data behind
174174 q.head = (q.head + 1) % ringSize
175175 full := q.n == ringSize
176176 q.n--
@@ -170,7 +170,7 @@ func (q *queue) pop(ev *event) bool {
170 return false170 return false
171 }171 }
172 *ev = q.ring[q.head]172 *ev = q.ring[q.head]
173- q.ring[q.head].kind = 0 // ownership of a carried descriptor moved to ev173+ q.ring[q.head] = event{} // ownership of a carried descriptor moved to ev; leave no key data behind
174 q.head = (q.head + 1) % ringSize174 q.head = (q.head + 1) % ringSize
175 full := q.n == ringSize175 full := q.n == ringSize
176 q.n--176 q.n--
M seat.go +44 -24
@@ -10,8 +10,8 @@ import (
1010 "golang.org/x/sys/unix"
1111 )
1212
13-// maxSeatVersion is the wl_seat version bound: 9 adds axis_value120 users and
14-// the release requests.
13+// maxSeatVersion is the highest wl_seat version bound. Version 5 adds the
14+// wl_seat.release request and wl_pointer.frame, 8 adds wl_pointer.axis_value120.
1515 const maxSeatVersion = 9
1616
1717 // CursorShape is a wp_cursor_shape_v1 shape. The values are the protocol's.
@@ -95,12 +95,18 @@ type PointerEvent struct {
9595 // KeyEvent is one key press, repeat or release.
9696 type KeyEvent struct {
9797 Surface SurfaceID
98- Keycode uint32 // evdev key code
99- Keysym uint32 // xkb keysym in the current layout and modifier state
98+ Keycode uint32 // evdev key code; 0 when Secret is set
99+ Keysym uint32 // xkb keysym in the current layout and modifier state; 0 when Secret is set
100100 Modifiers Modifiers
101101 Group uint32 // effective layout index
102102 Pressed bool
103103 Repeat bool
104+ // Secret is set while a [SecretBuffer] is installed, for the press,
105+ // repeats and release of a key that produced text (or is a dead or compose
106+ // key): the text went into the buffer, and Keycode and Keysym are 0 so the
107+ // event does not reveal which character was typed. Other keys (Enter,
108+ // Escape, BackSpace, modifiers, Control combinations) keep their keysym.
109+ Secret bool
104110 // Text is the UTF-8 text of the key, nil for releases and keys without
105111 // text. It aliases library storage that is wiped when Handler.Key
106112 // returns. It is always nil while a [SecretBuffer] is installed.
@@ -109,7 +115,8 @@ type KeyEvent struct {
109115
110116 // Seat is the connection's first wl_seat: pointer, keyboard and cursor. It
111117 // belongs to the owner goroutine. Without a seat global every method is a
112-// no-op.
118+// no-op. The seat is bound once, at first use: a seat announced later is not
119+// picked up, and a removed one is not replaced (no seat hotplug).
113120 type Seat struct {
114121 c *Conn
115122 proxy *wayland.Seat
@@ -184,8 +191,15 @@ func (c *Conn) ensureSeat() {
184191 // SetSecret installs b to receive typed text, or removes it with nil. While
185192 // installed, printable text goes only into b and never into KeyEvent.Text;
186193 // BackSpace removes one code point; every other key is still delivered as a
187-// KeyEvent without text. Installing or removing cancels held keys and repeat.
188-// The previous buffer is not wiped: that is the caller's call.
194+// KeyEvent without text. Keys that produced text are delivered with
195+// KeyEvent.Secret set and Keycode and Keysym 0, for the press, repeats and
196+// release; Enter, Escape, BackSpace and Control combinations keep their
197+// keysym. Installing or removing cancels held keys and repeat. The previous
198+// buffer is not wiped: that is the caller's call.
199+//
200+// Limits: runtime/secret (GOEXPERIMENT=runtimesecret) erases Go registers,
201+// stack and heap temporaries, but not the native memory of libxkbcommon, which
202+// processes the key text.
189203 func (s *Seat) SetSecret(b *SecretBuffer) {
190204 if s == nil {
191205 return
@@ -203,28 +217,24 @@ func (s *Seat) SetCursor(shape CursorShape) error {
203217 if s == nil || s.c.closed {
204218 return ErrClosed
205219 }
206- limit := CursorZoomOut
207- if s.shapesVer >= 2 {
208- limit = CursorAllResize
209- }
210220 if shape < CursorDefault || shape > CursorAllResize {
211221 return fmt.Errorf("neferclient: invalid cursor shape %d", shape)
212222 }
213- if s.shapes == nil || s.pointer == nil || s.enterSerial == 0 || shape == s.shape {
214- return nil
215- }
216- if shape > limit {
223+ if shape >= CursorDndAsk && s.shapes != nil && s.shapesVer < 2 {
217224 return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}
218225 }
226+ if s.shapes == nil || s.pointer == nil || s.ptrSurf == 0 || shape == s.shape {
227+ return nil
228+ }
219229 if s.shapeDev == nil {
220230 dev, err := s.shapes.GetPointer(s.pointer)
221231 if err != nil {
222- return err
232+ return fmt.Errorf("neferclient: cursor shape device: %w", err)
223233 }
224234 s.shapeDev = dev
225235 }
226236 if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {
227- return err
237+ return fmt.Errorf("neferclient: set cursor shape: %w", err)
228238 }
229239 s.shape = shape
230240 return nil
@@ -258,6 +268,9 @@ func (c *Conn) seatRepeat(h Handler) {
258268 s.deliverKey(ke, h)
259269 }
260270 }
271+ if err := s.kb.takeTimerErr(); err != nil {
272+ h.Error(err)
273+ }
261274 if !c.closed {
262275 if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {
263276 h.Error(err)
@@ -268,7 +281,7 @@ func (c *Conn) seatRepeat(h Handler) {
268281 func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {
269282 h.Key(ke)
270283 clear(s.kb.scratch[:]) // ordinary text must not outlive the call
271- ke.Text = nil
284+ *ke = KeyEvent{}
272285 if s.kb.changed {
273286 h.SecretChanged(s.secret.Len())
274287 }
@@ -363,7 +376,7 @@ func (s *Seat) newPointer() error {
363376 defer c.resume()
364377 p, err := s.proxy.GetPointer()
365378 if err != nil {
366- return err
379+ return fmt.Errorf("neferclient: get wl_pointer: %w", err)
367380 }
368381 s.pointer = p
369382 s.ptrGen++
@@ -391,11 +404,11 @@ func (s *Seat) newKeyboard() error {
391404 if s.kb == nil {
392405 kb, err := newKeyboard(composeLocale())
393406 if err != nil {
394- return err
407+ return fmt.Errorf("neferclient: keyboard: %w", err)
395408 }
396409 if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {
397410 kb.close()
398- return err
411+ return err // armFD wraps it
399412 }
400413 kb.secret = s.secret
401414 s.kb = kb
@@ -404,7 +417,7 @@ func (s *Seat) newKeyboard() error {
404417 defer c.resume()
405418 k, err := s.proxy.GetKeyboard()
406419 if err != nil {
407- return err
420+ return fmt.Errorf("neferclient: get wl_keyboard: %w", err)
408421 }
409422 s.keyboard = k
410423 s.kbGen++
@@ -455,6 +468,9 @@ func (c *Conn) applySeat(ev *event, h Handler) {
455468 default:
456469 if s.keyboard != nil && ev.version == s.kbGen {
457470 s.applyKeyboard(ev, h)
471+ if err := s.kb.takeTimerErr(); err != nil {
472+ h.Error(err)
473+ }
458474 }
459475 }
460476 }
@@ -538,10 +554,14 @@ func (s *Seat) applyKeyboard(ev *event, h Handler) {
538554 switch ev.kind {
539555 case evKbKeymap:
540556 fd := int(ev.fd)
541- ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again
557+ ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again
558+ if ev.a == 0 { // no_keymap: the compositor sends no usable map; nothing to report
559+ _ = unix.Close(fd)
560+ return
561+ }
542562 if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {
543563 _ = unix.Close(fd)
544- h.Error(errors.New("neferclient: unsupported keymap format or size"))
564+ h.Error(fmt.Errorf("neferclient: unsupported keymap format %d or size %d", ev.a, ev.flags))
545565 return
546566 }
547567 if err := kb.replaceFD(fd, int(ev.flags)); err != nil {
@@ -10,8 +10,8 @@ import (
10 "golang.org/x/sys/unix"10 "golang.org/x/sys/unix"
11 )11 )
12 12
13-// maxSeatVersion is the wl_seat version bound: 9 adds axis_value120 users and13+// maxSeatVersion is the highest wl_seat version bound. Version 5 adds the
14-// the release requests.14+// wl_seat.release request and wl_pointer.frame, 8 adds wl_pointer.axis_value120.
15 const maxSeatVersion = 915 const maxSeatVersion = 9
16 16
17 // CursorShape is a wp_cursor_shape_v1 shape. The values are the protocol's.17 // CursorShape is a wp_cursor_shape_v1 shape. The values are the protocol's.
@@ -95,12 +95,18 @@ type PointerEvent struct {
95 // KeyEvent is one key press, repeat or release.95 // KeyEvent is one key press, repeat or release.
96 type KeyEvent struct {96 type KeyEvent struct {
97 Surface SurfaceID97 Surface SurfaceID
98- Keycode uint32 // evdev key code98+ Keycode uint32 // evdev key code; 0 when Secret is set
99- Keysym uint32 // xkb keysym in the current layout and modifier state99+ Keysym uint32 // xkb keysym in the current layout and modifier state; 0 when Secret is set
100 Modifiers Modifiers100 Modifiers Modifiers
101 Group uint32 // effective layout index101 Group uint32 // effective layout index
102 Pressed bool102 Pressed bool
103 Repeat bool103 Repeat bool
104+ // Secret is set while a [SecretBuffer] is installed, for the press,
105+ // repeats and release of a key that produced text (or is a dead or compose
106+ // key): the text went into the buffer, and Keycode and Keysym are 0 so the
107+ // event does not reveal which character was typed. Other keys (Enter,
108+ // Escape, BackSpace, modifiers, Control combinations) keep their keysym.
109+ Secret bool
104 // Text is the UTF-8 text of the key, nil for releases and keys without110 // Text is the UTF-8 text of the key, nil for releases and keys without
105 // text. It aliases library storage that is wiped when Handler.Key111 // text. It aliases library storage that is wiped when Handler.Key
106 // returns. It is always nil while a [SecretBuffer] is installed.112 // returns. It is always nil while a [SecretBuffer] is installed.
@@ -109,7 +115,8 @@ type KeyEvent struct {
109 115
110 // Seat is the connection's first wl_seat: pointer, keyboard and cursor. It116 // Seat is the connection's first wl_seat: pointer, keyboard and cursor. It
111 // belongs to the owner goroutine. Without a seat global every method is a117 // belongs to the owner goroutine. Without a seat global every method is a
112-// no-op.118+// no-op. The seat is bound once, at first use: a seat announced later is not
119+// picked up, and a removed one is not replaced (no seat hotplug).
113 type Seat struct {120 type Seat struct {
114 c *Conn121 c *Conn
115 proxy *wayland.Seat122 proxy *wayland.Seat
@@ -184,8 +191,15 @@ func (c *Conn) ensureSeat() {
184 // SetSecret installs b to receive typed text, or removes it with nil. While191 // SetSecret installs b to receive typed text, or removes it with nil. While
185 // installed, printable text goes only into b and never into KeyEvent.Text;192 // installed, printable text goes only into b and never into KeyEvent.Text;
186 // BackSpace removes one code point; every other key is still delivered as a193 // BackSpace removes one code point; every other key is still delivered as a
187-// KeyEvent without text. Installing or removing cancels held keys and repeat.194+// KeyEvent without text. Keys that produced text are delivered with
188-// The previous buffer is not wiped: that is the caller's call.195+// KeyEvent.Secret set and Keycode and Keysym 0, for the press, repeats and
196+// release; Enter, Escape, BackSpace and Control combinations keep their
197+// keysym. Installing or removing cancels held keys and repeat. The previous
198+// buffer is not wiped: that is the caller's call.
199+//
200+// Limits: runtime/secret (GOEXPERIMENT=runtimesecret) erases Go registers,
201+// stack and heap temporaries, but not the native memory of libxkbcommon, which
202+// processes the key text.
189 func (s *Seat) SetSecret(b *SecretBuffer) {203 func (s *Seat) SetSecret(b *SecretBuffer) {
190 if s == nil {204 if s == nil {
191 return205 return
@@ -203,28 +217,24 @@ func (s *Seat) SetCursor(shape CursorShape) error {
203 if s == nil || s.c.closed {217 if s == nil || s.c.closed {
204 return ErrClosed218 return ErrClosed
205 }219 }
206- limit := CursorZoomOut
207- if s.shapesVer >= 2 {
208- limit = CursorAllResize
209- }
210 if shape < CursorDefault || shape > CursorAllResize {220 if shape < CursorDefault || shape > CursorAllResize {
211 return fmt.Errorf("neferclient: invalid cursor shape %d", shape)221 return fmt.Errorf("neferclient: invalid cursor shape %d", shape)
212 }222 }
213- if s.shapes == nil || s.pointer == nil || s.enterSerial == 0 || shape == s.shape {223+ if shape >= CursorDndAsk && s.shapes != nil && s.shapesVer < 2 {
214- return nil
215- }
216- if shape > limit {
217 return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}224 return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}
218 }225 }
226+ if s.shapes == nil || s.pointer == nil || s.ptrSurf == 0 || shape == s.shape {
227+ return nil
228+ }
219 if s.shapeDev == nil {229 if s.shapeDev == nil {
220 dev, err := s.shapes.GetPointer(s.pointer)230 dev, err := s.shapes.GetPointer(s.pointer)
221 if err != nil {231 if err != nil {
222- return err232+ return fmt.Errorf("neferclient: cursor shape device: %w", err)
223 }233 }
224 s.shapeDev = dev234 s.shapeDev = dev
225 }235 }
226 if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {236 if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {
227- return err237+ return fmt.Errorf("neferclient: set cursor shape: %w", err)
228 }238 }
229 s.shape = shape239 s.shape = shape
230 return nil240 return nil
@@ -258,6 +268,9 @@ func (c *Conn) seatRepeat(h Handler) {
258 s.deliverKey(ke, h)268 s.deliverKey(ke, h)
259 }269 }
260 }270 }
271+ if err := s.kb.takeTimerErr(); err != nil {
272+ h.Error(err)
273+ }
261 if !c.closed {274 if !c.closed {
262 if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {275 if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {
263 h.Error(err)276 h.Error(err)
@@ -268,7 +281,7 @@ func (c *Conn) seatRepeat(h Handler) {
268 func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {281 func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {
269 h.Key(ke)282 h.Key(ke)
270 clear(s.kb.scratch[:]) // ordinary text must not outlive the call283 clear(s.kb.scratch[:]) // ordinary text must not outlive the call
271- ke.Text = nil284+ *ke = KeyEvent{}
272 if s.kb.changed {285 if s.kb.changed {
273 h.SecretChanged(s.secret.Len())286 h.SecretChanged(s.secret.Len())
274 }287 }
@@ -363,7 +376,7 @@ func (s *Seat) newPointer() error {
363 defer c.resume()376 defer c.resume()
364 p, err := s.proxy.GetPointer()377 p, err := s.proxy.GetPointer()
365 if err != nil {378 if err != nil {
366- return err379+ return fmt.Errorf("neferclient: get wl_pointer: %w", err)
367 }380 }
368 s.pointer = p381 s.pointer = p
369 s.ptrGen++382 s.ptrGen++
@@ -391,11 +404,11 @@ func (s *Seat) newKeyboard() error {
391 if s.kb == nil {404 if s.kb == nil {
392 kb, err := newKeyboard(composeLocale())405 kb, err := newKeyboard(composeLocale())
393 if err != nil {406 if err != nil {
394- return err407+ return fmt.Errorf("neferclient: keyboard: %w", err)
395 }408 }
396 if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {409 if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {
397 kb.close()410 kb.close()
398- return err411+ return err // armFD wraps it
399 }412 }
400 kb.secret = s.secret413 kb.secret = s.secret
401 s.kb = kb414 s.kb = kb
@@ -404,7 +417,7 @@ func (s *Seat) newKeyboard() error {
404 defer c.resume()417 defer c.resume()
405 k, err := s.proxy.GetKeyboard()418 k, err := s.proxy.GetKeyboard()
406 if err != nil {419 if err != nil {
407- return err420+ return fmt.Errorf("neferclient: get wl_keyboard: %w", err)
408 }421 }
409 s.keyboard = k422 s.keyboard = k
410 s.kbGen++423 s.kbGen++
@@ -455,6 +468,9 @@ func (c *Conn) applySeat(ev *event, h Handler) {
455 default:468 default:
456 if s.keyboard != nil && ev.version == s.kbGen {469 if s.keyboard != nil && ev.version == s.kbGen {
457 s.applyKeyboard(ev, h)470 s.applyKeyboard(ev, h)
471+ if err := s.kb.takeTimerErr(); err != nil {
472+ h.Error(err)
473+ }
458 }474 }
459 }475 }
460 }476 }
@@ -538,10 +554,14 @@ func (s *Seat) applyKeyboard(ev *event, h Handler) {
538 switch ev.kind {554 switch ev.kind {
539 case evKbKeymap:555 case evKbKeymap:
540 fd := int(ev.fd)556 fd := int(ev.fd)
541- ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again557+ ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again
558+ if ev.a == 0 { // no_keymap: the compositor sends no usable map; nothing to report
559+ _ = unix.Close(fd)
560+ return
561+ }
542 if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {562 if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {
543 _ = unix.Close(fd)563 _ = unix.Close(fd)
544- h.Error(errors.New("neferclient: unsupported keymap format or size"))564+ h.Error(fmt.Errorf("neferclient: unsupported keymap format %d or size %d", ev.a, ev.flags))
545 return565 return
546 }566 }
547 if err := kb.replaceFD(fd, int(ev.flags)); err != nil {567 if err := kb.replaceFD(fd, int(ev.flags)); err != nil {

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