Skip to content

Commit 00715b3

Browse files
authored
fix(linux): release forwarded keys on the proxy keyboard when it fails open (#1619)
1 parent ad4dc5c commit 00715b3

3 files changed

Lines changed: 84 additions & 3 deletions

File tree

docs/adr/0014-the-wayland-keyboard-is-a-proxy.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,16 @@ it, so a hint label typed while Super is still coming up is the label.
145145
probe against a real kernel is
146146
`TestProxyNode_HeldByAnother_SeesAGrabFromAnotherFd`). The remapper's
147147
output returns to the compositor, so the user's keys and remaps work; what
148-
is lost is capturing keys for a mode. The setup guide tells remapper users
149-
to exclude the `neru-` devices instead.
148+
is lost is capturing keys for a mode. Failing open is the one time the
149+
proxy lets a keyboard go without waiting for it to be idle, so a key down
150+
at that instant, whose press the proxy re-emitted, is released on the proxy
151+
keyboard first (`releaseForwarded`, pinned by
152+
`TestEvdevProxy_FailOpen_ReleasesEveryKeyItForwarded`): its physical
153+
release goes to the physical device, whose libinput never saw the press
154+
and drops it, and a key left down on the proxy would stay down for the
155+
daemon's lifetime, a Super that Hyprland merged into every key typed
156+
afterwards. The setup guide tells remapper users to exclude the `neru-`
157+
devices instead.
150158
- A remapper's output keyboard also advertises relative motion and mouse
151159
buttons, so a key can move the pointer, and so does a receiver that exposes a
152160
mouse and a keyboard on one node. Grabbing such a device takes its motion, so

internal/adapter/eventtap/linux/evdev_proxy_cgo.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,14 +502,20 @@ func (p *evdevProxy) emitPointer(event waylandEvdevEvent) {
502502
// emitKey re-emits a key event of the proxy's own making, with the sync report
503503
// that makes it a complete frame.
504504
func (p *evdevProxy) emitKey(code uint16, value int32) {
505+
frame := keyFrame(code, value)
506+
p.write(p.uinputFd, &frame[0], len(frame))
507+
}
508+
509+
// keyFrame is one key event and the sync report that completes it.
510+
func keyFrame(code uint16, value int32) [2]C.struct_input_event {
505511
var frame [2]C.struct_input_event
506512
frame[0]._type = C.ushort(evdevEventKey)
507513
frame[0].code = C.ushort(code)
508514
frame[0].value = C.int(value)
509515
frame[1]._type = C.ushort(evdevEventSyn)
510516
frame[1].code = C.ushort(evdevSynReport)
511517

512-
p.write(p.uinputFd, &frame[0], len(frame))
518+
return frame
513519
}
514520

515521
// write puts count events on one proxy device, whole. Anything less means the
@@ -542,6 +548,8 @@ func (p *evdevProxy) failOpen(err error) {
542548
return
543549
}
544550

551+
p.releaseForwarded()
552+
545553
p.capture.ungrabAll()
546554

547555
// The keys now reach the compositor as well, so a session reading them
@@ -571,6 +579,38 @@ func (p *evdevProxy) failOpen(err error) {
571579
)
572580
}
573581

582+
// releaseForwarded re-emits a release on the proxy keyboard for every key the
583+
// rule counts as forwarded and down, then zeroes the rule. Failing open is the
584+
// one time a keyboard is let go without waiting for it to be idle: a key down
585+
// at that instant had its press re-emitted here, and its release goes to the
586+
// physical device next, whose libinput never saw the press and drops it. Left
587+
// down on the proxy keyboard it would stay down for the daemon's lifetime, and
588+
// a compositor that merges modifiers across keyboards (Hyprland) would put
589+
// that Super on every key typed afterwards. Base keys go up before modifiers,
590+
// the way a chord is let go of. The writes bypass the forwarding flag, which
591+
// failOpen has cleared; on a proxy that stopped taking writes they fail as the
592+
// last one did, and there is nothing more to do for it.
593+
func (p *evdevProxy) releaseForwarded() {
594+
var keys, modifiers []uint16
595+
596+
for code := range uint16(evdevKeyCodeCount) {
597+
switch {
598+
case !p.rule.isDown(code):
599+
case p.capture.modifierName(code) != "":
600+
modifiers = append(modifiers, code)
601+
default:
602+
keys = append(keys, code)
603+
}
604+
}
605+
606+
p.rule = forwardRule{}
607+
608+
for _, code := range append(keys, modifiers...) {
609+
frame := keyFrame(code, evdevValueRelease)
610+
C.neru_evdev_write_events(p.uinputFd, &frame[0], C.int(len(frame)))
611+
}
612+
}
613+
574614
// forwardWithheld hands a press the session had withheld to the compositor
575615
// after all, together with every modifier the user is physically holding that
576616
// the session withheld too. Each is marked forwarded, so its repeats and its

internal/adapter/eventtap/linux/evdev_proxy_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,39 @@ func TestEvdevProxy_FailOpenReleasesTheKeyboardsAndRefusesSessions(t *testing.T)
479479
}
480480
}
481481

482+
// A key down when the proxy fails open had its press re-emitted on the proxy
483+
// keyboard, and its release goes to the physical device next, where libinput
484+
// never saw the press: the proxy releases what it forwarded before it lets
485+
// the keyboards go, or the key stays down on the proxy for the daemon's life.
486+
func TestEvdevProxy_FailOpen_ReleasesEveryKeyItForwarded(t *testing.T) {
487+
t.Parallel()
488+
489+
proxy := newTestProxy()
490+
491+
// The activation chord, forwarded: no session is capturing.
492+
proxy.handle(keyEvent(evdevKeyLeftMeta, evdevValuePress))
493+
proxy.handle(keyEvent(evdevKeyJ, evdevValuePress))
494+
495+
if !proxy.rule.isDown(evdevKeyLeftMeta) || !proxy.rule.isDown(evdevKeyJ) {
496+
t.Fatal("the chord was not forwarded before the proxy failed open")
497+
}
498+
499+
proxy.forwarding.Store(true)
500+
proxy.capture.grab = true
501+
proxy.failOpen(errWaylandEvdevProxyGrabbed)
502+
503+
for _, code := range []uint16{evdevKeyLeftMeta, evdevKeyJ} {
504+
if proxy.rule.isDown(code) {
505+
t.Errorf("key %d is still down on the proxy keyboard after it failed open", code)
506+
}
507+
}
508+
509+
// The physical releases that follow are the physical device's now.
510+
if proxy.rule.release(evdevKeyJ) || proxy.rule.release(evdevKeyLeftMeta) {
511+
t.Error("a release was forwarded for a key the fail-open already released")
512+
}
513+
}
514+
482515
// A keyboard yielded to a remapper is with the compositor until the remapper
483516
// claims it or it is taken back; a mode started meanwhile would get nothing
484517
// while its keys went to the focused app, so it is refused and falls back.

0 commit comments

Comments
 (0)