@@ -66,7 +66,18 @@ type evdevProxy struct {
6666 pointerNode * proxyNode
6767 heldByAnother func (* proxyNode ) bool
6868
69- bindings atomic.Pointer [map [string ]func ()]
69+ bindings atomic.Pointer [map [string ]hotkeyBinding ]
70+
71+ // heldHotkeys is the release callback owed for each key whose press
72+ // matched a chord and is still down, by key code. Owned by the run
73+ // goroutine. It is what lets a binding repeat while its key is held: the
74+ // press fires once, at match time, and the release fires when this key
75+ // comes up, whichever consumer the events reached in between.
76+ heldHotkeys map [uint16 ]func ()
77+
78+ // dispatch runs the matched bindings' callbacks, in order, off this
79+ // goroutine.
80+ dispatch * HotkeyDispatcher
7081
7182 control chan proxyCommand
7283 done chan struct {}
@@ -80,6 +91,13 @@ type proxyCommand struct {
8091 ack chan struct {}
8192}
8293
94+ // hotkeyBinding is what the idle matcher fires for a chord: press when the
95+ // chord's key goes down, release (which may be nil) when that key comes up.
96+ type hotkeyBinding struct {
97+ press func ()
98+ release func ()
99+ }
100+
83101var (
84102 sharedProxyMu sync.Mutex
85103 sharedProxy * evdevProxy
@@ -161,11 +179,13 @@ func newEvdevProxy(logger *zap.Logger) (*evdevProxy, error) {
161179 control : make (chan proxyCommand ),
162180 done : make (chan struct {}),
163181 heldByAnother : (* proxyNode ).heldByAnother ,
182+ heldHotkeys : make (map [uint16 ]func ()),
183+ dispatch : NewHotkeyDispatcher (),
164184 }
165185
166186 proxy .forwarding .Store (uinputFd >= 0 )
167187
168- empty := map [string ]func () {}
188+ empty := map [string ]hotkeyBinding {}
169189 proxy .bindings .Store (& empty )
170190
171191 go proxy .run ()
@@ -200,8 +220,8 @@ func (p *evdevProxy) alive() bool {
200220
201221// setBindings replaces the chords the idle matcher fires on. The map is read
202222// on the run goroutine without a lock, so it is swapped whole and never edited.
203- func (p * evdevProxy ) setBindings (bindings map [string ]func () ) {
204- copied := make (map [string ]func () , len (bindings ))
223+ func (p * evdevProxy ) setBindings (bindings map [string ]hotkeyBinding ) {
224+ copied := make (map [string ]hotkeyBinding , len (bindings ))
205225 maps .Copy (copied , bindings )
206226
207227 p .bindings .Store (& copied )
@@ -412,6 +432,7 @@ func (p *evdevProxy) handleKey(event waylandEvdevEvent) {
412432 case evdevValueRelease :
413433 p .capture .feedKey (code , false )
414434 p .trackGlobal (code , false )
435+ p .releaseHotkey (code )
415436
416437 forwarded := p .rule .release (code )
417438 if forwarded {
@@ -439,7 +460,8 @@ func (p *evdevProxy) trackGlobal(code uint16, isDown bool) {
439460}
440461
441462// matchHotkey fires the binding for the chord a press completes, if there is
442- // one, and reports whether it did.
463+ // one, and reports whether it did. A press fires once per hold: the kernel
464+ // reports a held key as repeats, not presses, so none of those reach here.
443465func (p * evdevProxy ) matchHotkey (code uint16 ) bool {
444466 bindings := * p .bindings .Load ()
445467 if len (bindings ) == 0 {
@@ -456,18 +478,36 @@ func (p *evdevProxy) matchHotkey(code uint16) bool {
456478 return false
457479 }
458480
459- callback := bindings [signature ]
460- if callback == nil {
481+ binding , bound := bindings [signature ]
482+ if ! bound || binding . press == nil {
461483 return false
462484 }
463485
464486 p .logger .Debug ("Global hotkey matched" , zap .String ("chord" , signature ))
465487
466- go callback ()
488+ if binding .release != nil {
489+ p .heldHotkeys [code ] = binding .release
490+ }
491+
492+ p .dispatch .Dispatch (binding .press )
467493
468494 return true
469495}
470496
497+ // releaseHotkey fires the release owed for a key whose press matched a chord.
498+ // It is keyed by the key alone, not the chord: the binder needs the release
499+ // whether or not the modifier is still down when the key comes up.
500+ func (p * evdevProxy ) releaseHotkey (code uint16 ) {
501+ release , held := p .heldHotkeys [code ]
502+ if ! held {
503+ return
504+ }
505+
506+ delete (p .heldHotkeys , code )
507+
508+ p .dispatch .Dispatch (release )
509+ }
510+
471511// emit re-emits one event on the proxy keyboard.
472512func (p * evdevProxy ) emit (event waylandEvdevEvent ) {
473513 var raw C.struct_input_event
0 commit comments