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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +155 -66
M handler.go +4 -1
| @@ -59,7 +59,10 @@ type Handler interface { | |||
| 59 | 59 | // keyboard focus. Text is the text the key produced (nil for releases and | |
| 60 | 60 | // keys without text) and is valid only during the call. While a | |
| 61 | 61 | // [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 | |
| 63 | 66 | // valid only during the call. | |
| 64 | 67 | Key(ev *KeyEvent) | |
| 65 | 68 | // 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 and | 59 | // 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 a | 60 | // keys without text) and is valid only during the call. While a | ||
| 61 | // [SecretBuffer] is installed with [Seat.SetSecret], Text is always nil | 61 | // [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 | |||
| 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "fmt" | |
| 5 | 6 | "io" | |
| 6 | 7 | "os" | |
| 7 | 8 | "time" | |
| @@ -34,16 +35,33 @@ const ( | |||
| 34 | 35 | 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 | 45 | type textKind uint8 | |
| 40 | 46 | ||
| 41 | 47 | 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 | |
| 44 | 51 | 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 | 65 | // keyboard interprets wl_keyboard events with xkbcommon. It runs only on the | |
| 48 | 66 | // owner goroutine. It never builds a string from key text: translations go | |
| 49 | 67 | // through the fixed scratch array, which is wiped after every use. | |
| @@ -65,6 +83,7 @@ type keyboard struct { | |||
| 65 | 83 | delay int32 // milliseconds before the first repeat | |
| 66 | 84 | repeating uint32 | |
| 67 | 85 | composePending bool | |
| 86 | + | composeUsed bool // the last text() call consumed the key into a compose sequence | |
| 68 | 87 | ||
| 69 | 88 | tfd int // repeat timerfd | |
| 70 | 89 | tbuf [8]byte | |
| @@ -72,23 +91,16 @@ type keyboard struct { | |||
| 72 | 91 | secret *SecretBuffer | |
| 73 | 92 | scratch [scratchSize]byte | |
| 74 | 93 | ||
| 75 | - | // Work area of doSecret, which secret.Do runs without arguments. | |
| 76 | 94 | 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 | |
| 86 | 98 | } | |
| 87 | 99 | ||
| 88 | 100 | func newKeyboard(locale string) (*keyboard, error) { | |
| 89 | 101 | c, err := xkb.NewContext() | |
| 90 | 102 | if err != nil { | |
| 91 | - | return nil, err | |
| 103 | + | return nil, fmt.Errorf("xkb context: %w", err) | |
| 92 | 104 | } | |
| 93 | 105 | k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1} | |
| 94 | 106 | k.secretFn = k.doSecret | |
| @@ -98,7 +110,7 @@ func newKeyboard(locale string) (*keyboard, error) { | |||
| 98 | 110 | } | |
| 99 | 111 | if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil { | |
| 100 | 112 | _ = c.Close() | |
| 101 | - | return nil, err | |
| 113 | + | return nil, fmt.Errorf("repeat timer: %w", err) | |
| 102 | 114 | } | |
| 103 | 115 | return k, nil | |
| 104 | 116 | } | |
| @@ -147,7 +159,7 @@ func (k *keyboard) replaceFD(fd, size int) error { | |||
| 147 | 159 | defer f.Close() | |
| 148 | 160 | km, err := k.ctx.NewKeymapFD(fd, size) | |
| 149 | 161 | if err != nil { | |
| 150 | - | return err | |
| 162 | + | return fmt.Errorf("neferclient: compile keymap: %w", err) | |
| 151 | 163 | } | |
| 152 | 164 | return k.replace(km) | |
| 153 | 165 | } | |
| @@ -156,7 +168,7 @@ func (k *keyboard) replace(km *xkb.Keymap) error { | |||
| 156 | 168 | s, err := km.NewState() | |
| 157 | 169 | if err != nil { | |
| 158 | 170 | _ = km.Close() | |
| 159 | - | return err | |
| 171 | + | return fmt.Errorf("neferclient: xkb state: %w", err) | |
| 160 | 172 | } | |
| 161 | 173 | k.reset() | |
| 162 | 174 | if k.state != nil { | |
| @@ -180,26 +192,36 @@ func (k *keyboard) replace(km *xkb.Keymap) error { | |||
| 180 | 192 | } | |
| 181 | 193 | ||
| 182 | 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 | 200 | k.haveMask = true | |
| 185 | 201 | if k.state == nil { | |
| 186 | 202 | return nil | |
| 187 | 203 | } | |
| 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 | |
| 191 | 212 | } | |
| 192 | 213 | ||
| 193 | 214 | // refresh recomputes the cached modifiers and layout from the xkb state. Both | |
| 194 | 215 | // only change when the mask does, so keys do not query them. | |
| 195 | 216 | func (k *keyboard) refresh() { | |
| 196 | - | k.mods, k.group = k.queryModifiers(), 0 | |
| 217 | + | k.mods = k.queryModifiers() | |
| 197 | 218 | k.group, _ = k.state.Layout() | |
| 198 | 219 | } | |
| 199 | 220 | ||
| 200 | 221 | // reset drops held keys, repeat and compose state. It keeps the keymap, the | |
| 201 | 222 | // modifier mask and the focus flag. | |
| 202 | 223 | func (k *keyboard) reset() { | |
| 224 | + | clear(k.held[:cap(k.held)]) // no hidden-key record outlives the reset | |
| 203 | 225 | k.held = k.held[:0] | |
| 204 | 226 | k.repeating = 0 | |
| 205 | 227 | k.disarm() | |
| @@ -233,16 +255,29 @@ func (k *keyboard) arm() { | |||
| 233 | 255 | Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)), | |
| 234 | 256 | 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 | 261 | func (k *keyboard) disarm() { | |
| 240 | 262 | if k.tfd >= 0 { | |
| 241 | 263 | 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 | 281 | // expired consumes a timer expiration. Missed ticks are not accumulated: one | |
| 247 | 282 | // call yields at most one repeat. | |
| 248 | 283 | func (k *keyboard) expired() bool { | |
| @@ -273,7 +308,12 @@ func (k *keyboard) find(evdev uint32) int { | |||
| 273 | 308 | 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 | 318 | // press translates a key press into ev. It reports false when the press must | |
| 279 | 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 | 330 | sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev)) | |
| 291 | 331 | if err != nil { | |
| 292 | - | return false, err | |
| 332 | + | return false, fmt.Errorf("neferclient: key symbol: %w", err) | |
| 293 | 333 | } | |
| 294 | 334 | kind, err := k.translate(evdev, sym, false, ev) | |
| 295 | 335 | 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}) | |
| 297 | 342 | } | |
| 298 | - | k.held = append(k.held, heldKey{evdev, sym}) | |
| 299 | 343 | if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) { | |
| 300 | 344 | if k.rate > 0 { | |
| 301 | 345 | k.repeating = evdev | |
| @@ -322,7 +366,10 @@ func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool { | |||
| 322 | 366 | k.repeating = 0 | |
| 323 | 367 | k.disarm() | |
| 324 | 368 | } | |
| 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 | + | } | |
| 326 | 373 | k.changed = false | |
| 327 | 374 | ev.Modifiers, ev.Group = k.mods, k.group | |
| 328 | 375 | return true | |
| @@ -342,7 +389,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) { | |||
| 342 | 389 | sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev)) | |
| 343 | 390 | if err != nil { | |
| 344 | 391 | _, _ = stop() | |
| 345 | - | return false, err | |
| 392 | + | return false, fmt.Errorf("neferclient: key symbol: %w", err) | |
| 346 | 393 | } | |
| 347 | 394 | if k.secret == nil && !repeatable(sym) { | |
| 348 | 395 | return stop() | |
| @@ -350,7 +397,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) { | |||
| 350 | 397 | kind, err := k.translate(evdev, sym, true, ev) | |
| 351 | 398 | if err != nil { | |
| 352 | 399 | _, _ = stop() | |
| 353 | - | return false, err | |
| 400 | + | return false, fmt.Errorf("neferclient: key text: %w", err) | |
| 354 | 401 | } | |
| 355 | 402 | if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) { | |
| 356 | 403 | return stop() | |
| @@ -365,11 +412,17 @@ func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (text | |||
| 365 | 412 | ev.Modifiers, ev.Group = k.mods, k.group | |
| 366 | 413 | k.changed = false | |
| 367 | 414 | 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} | |
| 370 | 416 | 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 | |
| 373 | 426 | } | |
| 374 | 427 | n, err := k.text(evdev, sym, repeat) | |
| 375 | 428 | if err != nil { | |
| @@ -385,7 +438,6 @@ func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (text | |||
| 385 | 438 | // handles secret text, so it runs under runtime/secret when available. | |
| 386 | 439 | func (k *keyboard) doSecret() { | |
| 387 | 440 | w := &k.w | |
| 388 | - | w.kind, w.changed, w.err = kindNone, false, nil | |
| 389 | 441 | defer clear(k.scratch[:]) | |
| 390 | 442 | switch { | |
| 391 | 443 | case w.sym == raw.XKB_KEY_BackSpace: | |
| @@ -402,9 +454,12 @@ func (k *keyboard) doSecret() { | |||
| 402 | 454 | w.err = err | |
| 403 | 455 | return | |
| 404 | 456 | } | |
| 405 | - | if classifySecretText(k.scratch[:n]) { | |
| 457 | + | switch { | |
| 458 | + | case classifySecretText(k.scratch[:n]): | |
| 406 | 459 | w.kind = kindText | |
| 407 | 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 | 475 | // is fed only by physical presses: a repeat neither advances a sequence nor | |
| 421 | 476 | // replays composed text. | |
| 422 | 477 | func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) { | |
| 478 | + | k.composeUsed = false | |
| 423 | 479 | if k.compose != nil && !repeat { | |
| 424 | 480 | if _, err := k.compose.Feed(sym); err != nil { | |
| 425 | 481 | return 0, err | |
| @@ -428,6 +484,7 @@ func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) { | |||
| 428 | 484 | if err != nil { | |
| 429 | 485 | return 0, err | |
| 430 | 486 | } | |
| 487 | + | k.composeUsed = status == raw.XKB_COMPOSE_COMPOSING || status == raw.XKB_COMPOSE_CANCELLED || status == raw.XKB_COMPOSE_COMPOSED | |
| 431 | 488 | switch status { | |
| 432 | 489 | case raw.XKB_COMPOSE_COMPOSING: | |
| 433 | 490 | k.composePending = true | |
| @@ -461,11 +518,20 @@ func shortAsEmpty(n int, err error) (int, error) { | |||
| 461 | 518 | // Modifier, dead and compose keys never repeat. The compositor provides rate | |
| 462 | 519 | // and delay; its keymap is authoritative for text and layout. | |
| 463 | 520 | func repeatable(sym uint32) bool { | |
| 521 | + | return !isModifierKey(sym) && !isDeadKey(sym) | |
| 522 | + | } | |
| 523 | + | ||
| 524 | + | func isModifierKey(sym uint32) bool { | |
| 464 | 525 | switch sym { | |
| 465 | 526 | case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R, | |
| 466 | 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: | |
| 468 | - | return false | |
| 528 | + | raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock: | |
| 529 | + | return true | |
| 469 | 530 | } | |
| 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 | |
| 471 | 537 | } | |
| @@ -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 << 20 | 35 | 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 uint8 | 45 | type textKind uint8 | ||
| 40 | 46 | ||||
| 41 | const ( | 47 | const ( | ||
| 42 | - | kindNone textKind = iota | 48 | + | kindNone textKind = iota |
| 43 | - | kindText | 49 | + | kindText // printable text went into the SecretBuffer |
| 50 | + | kindPending // a dead or compose key: part of a text sequence, no text yet | |||
| 44 | kindBackspace | 51 | 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 the | 65 | // keyboard interprets wl_keyboard events with xkbcommon. It runs only on the | ||
| 48 | // owner goroutine. It never builds a string from key text: translations go | 66 | // 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 repeat | 83 | delay int32 // milliseconds before the first repeat | ||
| 66 | repeating uint32 | 84 | repeating uint32 | ||
| 67 | composePending bool | 85 | composePending bool | ||
| 86 | + | composeUsed bool // the last text() call consumed the key into a compose sequence | |||
| 68 | 87 | ||||
| 69 | tfd int // repeat timerfd | 88 | tfd int // repeat timerfd | ||
| 70 | tbuf [8]byte | 89 | tbuf [8]byte | ||
| @@ -72,23 +91,16 @@ type keyboard struct { | |||||
| 72 | secret *SecretBuffer | 91 | secret *SecretBuffer | ||
| 73 | scratch [scratchSize]byte | 92 | 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 uint32 | 96 | + | changed bool // the last translation edited the SecretBuffer |
| 79 | - | mods Modifiers | 97 | + | 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, err | 103 | + | 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.doSecret | 106 | 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, err | 113 | + | return nil, fmt.Errorf("repeat timer: %w", err) |
| 102 | } | 114 | } | ||
| 103 | return k, nil | 115 | 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 err | 162 | + | 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 err | 171 | + | 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 = true | 200 | k.haveMask = true | ||
| 185 | if k.state == nil { | 201 | if k.state == nil { | ||
| 186 | return nil | 202 | 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 err | 206 | + | 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. Both | 214 | // 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(), 0 | 217 | + | 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, the | 221 | // 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 = 0 | 226 | 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 disarm | 256 | 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.ItimerSpec | 263 | 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: one | 281 | // 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 -1 | 308 | 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 must | 318 | // 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, err | 332 | + | 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, 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}) | |||
| 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 = evdev | 345 | k.repeating = evdev | ||
| @@ -322,7 +366,10 @@ func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool { | |||||
| 322 | k.repeating = 0 | 366 | k.repeating = 0 | ||
| 323 | k.disarm() | 367 | k.disarm() | ||
| 324 | } | 368 | } | ||
| 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 | + | } | |||
| 326 | k.changed = false | 373 | k.changed = false | ||
| 327 | ev.Modifiers, ev.Group = k.mods, k.group | 374 | ev.Modifiers, ev.Group = k.mods, k.group | ||
| 328 | return true | 375 | 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, err | 392 | + | 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, err | 400 | + | 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.group | 412 | ev.Modifiers, ev.Group = k.mods, k.group | ||
| 366 | k.changed = false | 413 | k.changed = false | ||
| 367 | if k.secret != nil { | 414 | if k.secret != nil { | ||
| 368 | - | w := &k.w | 415 | + | 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.changed | 417 | + | kind, changed, err := k.w.kind, k.w.changed, k.w.err |
| 372 | - | return w.kind, 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 | |||
| 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.w | 440 | 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 = err | 454 | w.err = err | ||
| 403 | return | 455 | return | ||
| 404 | } | 456 | } | ||
| 405 | - | if classifySecretText(k.scratch[:n]) { | 457 | + | switch { |
| 458 | + | case classifySecretText(k.scratch[:n]): | |||
| 406 | w.kind = kindText | 459 | 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 nor | 475 | // 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, err | 481 | 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, err | 485 | 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 = true | 490 | 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 rate | 518 | // 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 false | 529 | + | return true |
| 469 | } | 530 | } | ||
| 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 | |||
| 471 | } | 537 | } | ||
M queue.go +1 -1
| @@ -170,7 +170,7 @@ func (q *queue) pop(ev *event) bool { | |||
| 170 | 170 | return false | |
| 171 | 171 | } | |
| 172 | 172 | *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 | |
| 174 | 174 | q.head = (q.head + 1) % ringSize | |
| 175 | 175 | full := q.n == ringSize | |
| 176 | 176 | q.n-- | |
| @@ -170,7 +170,7 @@ func (q *queue) pop(ev *event) bool { | |||||
| 170 | return false | 170 | 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 ev | 173 | + | q.ring[q.head] = event{} // ownership of a carried descriptor moved to ev; leave no key data behind |
| 174 | q.head = (q.head + 1) % ringSize | 174 | q.head = (q.head + 1) % ringSize | ||
| 175 | full := q.n == ringSize | 175 | full := q.n == ringSize | ||
| 176 | q.n-- | 176 | q.n-- | ||
M seat.go +44 -24
| @@ -10,8 +10,8 @@ import ( | |||
| 10 | 10 | "golang.org/x/sys/unix" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 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. | |
| 15 | 15 | const maxSeatVersion = 9 | |
| 16 | 16 | ||
| 17 | 17 | // CursorShape is a wp_cursor_shape_v1 shape. The values are the protocol's. | |
| @@ -95,12 +95,18 @@ type PointerEvent struct { | |||
| 95 | 95 | // KeyEvent is one key press, repeat or release. | |
| 96 | 96 | type KeyEvent struct { | |
| 97 | 97 | 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 | |
| 100 | 100 | Modifiers Modifiers | |
| 101 | 101 | Group uint32 // effective layout index | |
| 102 | 102 | Pressed bool | |
| 103 | 103 | 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 | 110 | // Text is the UTF-8 text of the key, nil for releases and keys without | |
| 105 | 111 | // text. It aliases library storage that is wiped when Handler.Key | |
| 106 | 112 | // returns. It is always nil while a [SecretBuffer] is installed. | |
| @@ -109,7 +115,8 @@ type KeyEvent struct { | |||
| 109 | 115 | ||
| 110 | 116 | // Seat is the connection's first wl_seat: pointer, keyboard and cursor. It | |
| 111 | 117 | // 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 | 120 | type Seat struct { | |
| 114 | 121 | c *Conn | |
| 115 | 122 | proxy *wayland.Seat | |
| @@ -184,8 +191,15 @@ func (c *Conn) ensureSeat() { | |||
| 184 | 191 | // SetSecret installs b to receive typed text, or removes it with nil. While | |
| 185 | 192 | // installed, printable text goes only into b and never into KeyEvent.Text; | |
| 186 | 193 | // 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. | |
| 189 | 203 | func (s *Seat) SetSecret(b *SecretBuffer) { | |
| 190 | 204 | if s == nil { | |
| 191 | 205 | return | |
| @@ -203,28 +217,24 @@ func (s *Seat) SetCursor(shape CursorShape) error { | |||
| 203 | 217 | if s == nil || s.c.closed { | |
| 204 | 218 | return ErrClosed | |
| 205 | 219 | } | |
| 206 | - | limit := CursorZoomOut | |
| 207 | - | if s.shapesVer >= 2 { | |
| 208 | - | limit = CursorAllResize | |
| 209 | - | } | |
| 210 | 220 | if shape < CursorDefault || shape > CursorAllResize { | |
| 211 | 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 { | |
| 214 | - | return nil | |
| 215 | - | } | |
| 216 | - | if shape > limit { | |
| 223 | + | if shape >= CursorDndAsk && s.shapes != nil && s.shapesVer < 2 { | |
| 217 | 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 | 229 | if s.shapeDev == nil { | |
| 220 | 230 | dev, err := s.shapes.GetPointer(s.pointer) | |
| 221 | 231 | if err != nil { | |
| 222 | - | return err | |
| 232 | + | return fmt.Errorf("neferclient: cursor shape device: %w", err) | |
| 223 | 233 | } | |
| 224 | 234 | s.shapeDev = dev | |
| 225 | 235 | } | |
| 226 | 236 | if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil { | |
| 227 | - | return err | |
| 237 | + | return fmt.Errorf("neferclient: set cursor shape: %w", err) | |
| 228 | 238 | } | |
| 229 | 239 | s.shape = shape | |
| 230 | 240 | return nil | |
| @@ -258,6 +268,9 @@ func (c *Conn) seatRepeat(h Handler) { | |||
| 258 | 268 | s.deliverKey(ke, h) | |
| 259 | 269 | } | |
| 260 | 270 | } | |
| 271 | + | if err := s.kb.takeTimerErr(); err != nil { | |
| 272 | + | h.Error(err) | |
| 273 | + | } | |
| 261 | 274 | if !c.closed { | |
| 262 | 275 | if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil { | |
| 263 | 276 | h.Error(err) | |
| @@ -268,7 +281,7 @@ func (c *Conn) seatRepeat(h Handler) { | |||
| 268 | 281 | func (s *Seat) deliverKey(ke *KeyEvent, h Handler) { | |
| 269 | 282 | h.Key(ke) | |
| 270 | 283 | clear(s.kb.scratch[:]) // ordinary text must not outlive the call | |
| 271 | - | ke.Text = nil | |
| 284 | + | *ke = KeyEvent{} | |
| 272 | 285 | if s.kb.changed { | |
| 273 | 286 | h.SecretChanged(s.secret.Len()) | |
| 274 | 287 | } | |
| @@ -363,7 +376,7 @@ func (s *Seat) newPointer() error { | |||
| 363 | 376 | defer c.resume() | |
| 364 | 377 | p, err := s.proxy.GetPointer() | |
| 365 | 378 | if err != nil { | |
| 366 | - | return err | |
| 379 | + | return fmt.Errorf("neferclient: get wl_pointer: %w", err) | |
| 367 | 380 | } | |
| 368 | 381 | s.pointer = p | |
| 369 | 382 | s.ptrGen++ | |
| @@ -391,11 +404,11 @@ func (s *Seat) newKeyboard() error { | |||
| 391 | 404 | if s.kb == nil { | |
| 392 | 405 | kb, err := newKeyboard(composeLocale()) | |
| 393 | 406 | if err != nil { | |
| 394 | - | return err | |
| 407 | + | return fmt.Errorf("neferclient: keyboard: %w", err) | |
| 395 | 408 | } | |
| 396 | 409 | if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil { | |
| 397 | 410 | kb.close() | |
| 398 | - | return err | |
| 411 | + | return err // armFD wraps it | |
| 399 | 412 | } | |
| 400 | 413 | kb.secret = s.secret | |
| 401 | 414 | s.kb = kb | |
| @@ -404,7 +417,7 @@ func (s *Seat) newKeyboard() error { | |||
| 404 | 417 | defer c.resume() | |
| 405 | 418 | k, err := s.proxy.GetKeyboard() | |
| 406 | 419 | if err != nil { | |
| 407 | - | return err | |
| 420 | + | return fmt.Errorf("neferclient: get wl_keyboard: %w", err) | |
| 408 | 421 | } | |
| 409 | 422 | s.keyboard = k | |
| 410 | 423 | s.kbGen++ | |
| @@ -455,6 +468,9 @@ func (c *Conn) applySeat(ev *event, h Handler) { | |||
| 455 | 468 | default: | |
| 456 | 469 | if s.keyboard != nil && ev.version == s.kbGen { | |
| 457 | 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 | 554 | switch ev.kind { | |
| 539 | 555 | case evKbKeymap: | |
| 540 | 556 | 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 | + | } | |
| 542 | 562 | if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax { | |
| 543 | 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 | 565 | return | |
| 546 | 566 | } | |
| 547 | 567 | 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 and | 13 | + | // 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 = 9 | 15 | 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 SurfaceID | 97 | Surface SurfaceID | ||
| 98 | - | Keycode uint32 // evdev key code | 98 | + | Keycode uint32 // evdev key code; 0 when Secret is set |
| 99 | - | Keysym uint32 // xkb keysym in the current layout and modifier state | 99 | + | Keysym uint32 // xkb keysym in the current layout and modifier state; 0 when Secret is set |
| 100 | Modifiers Modifiers | 100 | Modifiers Modifiers | ||
| 101 | Group uint32 // effective layout index | 101 | Group uint32 // effective layout index | ||
| 102 | Pressed bool | 102 | Pressed bool | ||
| 103 | Repeat bool | 103 | 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 without | 110 | // 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.Key | 111 | // 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. It | 116 | // 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 a | 117 | // 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 *Conn | 121 | c *Conn | ||
| 115 | proxy *wayland.Seat | 122 | proxy *wayland.Seat | ||
| @@ -184,8 +191,15 @@ func (c *Conn) ensureSeat() { | |||||
| 184 | // SetSecret installs b to receive typed text, or removes it with nil. While | 191 | // 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 a | 193 | // 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 | return | 205 | 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 ErrClosed | 218 | 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 err | 232 | + | return fmt.Errorf("neferclient: cursor shape device: %w", err) |
| 223 | } | 233 | } | ||
| 224 | s.shapeDev = dev | 234 | 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 err | 237 | + | return fmt.Errorf("neferclient: set cursor shape: %w", err) |
| 228 | } | 238 | } | ||
| 229 | s.shape = shape | 239 | s.shape = shape | ||
| 230 | return nil | 240 | 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 call | 283 | clear(s.kb.scratch[:]) // ordinary text must not outlive the call | ||
| 271 | - | ke.Text = nil | 284 | + | *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 err | 379 | + | return fmt.Errorf("neferclient: get wl_pointer: %w", err) |
| 367 | } | 380 | } | ||
| 368 | s.pointer = p | 381 | 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 err | 407 | + | 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 err | 411 | + | return err // armFD wraps it |
| 399 | } | 412 | } | ||
| 400 | kb.secret = s.secret | 413 | kb.secret = s.secret | ||
| 401 | s.kb = kb | 414 | 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 err | 420 | + | return fmt.Errorf("neferclient: get wl_keyboard: %w", err) |
| 408 | } | 421 | } | ||
| 409 | s.keyboard = k | 422 | 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 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 | + | } | |||
| 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 | return | 565 | 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 { | ||