detached fe9795c

feat: add seat with pointer, keyboard, repeat timer, cursor shape and secret input

commit
fe9795cc0cf0219b585af04ba5c91c2a69b2354a
author
brice · · 2026-10-01 18:32 UTC
parent
afc6dac
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +3815 -1
M conn.go +16 -0
@@ -82,6 +82,8 @@ type Conn struct {
8282 nextLock uint32
8383 curLock *Lock // the live session lock, if any
8484 tableBuf []byte
85+ seat *Seat
86+ seatErr error // a seat bind failure, reported by the next Dispatch
8587
8688 ready bool // Connect finished; announcements go to the Handler
8789 dispatching bool
@@ -338,6 +340,10 @@ func (c *Conn) Dispatch(h Handler) error {
338340 }
339341 c.dispatching = true
340342 defer func() { c.dispatching = false }()
343+ if err := c.seatErr; err != nil {
344+ c.seatErr = nil
345+ h.Error(err)
346+ }
341347 var ev event
342348 for range ringSize {
343349 if !c.q.pop(&ev) {
@@ -364,6 +370,7 @@ func (c *Conn) apply(ev *event, h Handler) {
364370 }
365371 case evGlobalRemove:
366372 c.removeOutput(ev.global, h)
373+ c.seatGlobalRemoved(ev.global, h)
367374 case evOutputName:
368375 if o := c.entry(ev.global); o != nil {
369376 o.nameLen = copy(o.nameBuf[:], ev.nameBytes())
@@ -381,6 +388,10 @@ func (c *Conn) apply(ev *event, h Handler) {
381388 c.commitOutput(o, h)
382389 }
383390 case evFDReady:
391+ if c.seatTimer(ev.fd) {
392+ c.seatRepeat(h)
393+ return
394+ }
384395 id, ok := c.watched[ev.fd]
385396 if !ok {
386397 return // unwatched while the event was queued
@@ -395,6 +406,10 @@ func (c *Conn) apply(ev *event, h Handler) {
395406 }
396407 }
397408 default:
409+ if ev.kind >= evSeatCaps {
410+ c.applySeat(ev, h)
411+ return
412+ }
398413 c.applySurface(ev, h)
399414 }
400415 }
@@ -590,6 +605,7 @@ func (c *Conn) Close() error {
590605 if c.efd >= 0 {
591606 _ = unix.Close(c.efd)
592607 }
608+ c.closeSeat()
593609 c.q.closeFDs() // both producers are gone: close what nobody will drain
594610 return err
595611 }
@@ -82,6 +82,8 @@ type Conn struct {
82 nextLock uint3282 nextLock uint32
83 curLock *Lock // the live session lock, if any83 curLock *Lock // the live session lock, if any
84 tableBuf []byte84 tableBuf []byte
85+ seat *Seat
86+ seatErr error // a seat bind failure, reported by the next Dispatch
85 87
86 ready bool // Connect finished; announcements go to the Handler88 ready bool // Connect finished; announcements go to the Handler
87 dispatching bool89 dispatching bool
@@ -338,6 +340,10 @@ func (c *Conn) Dispatch(h Handler) error {
338 }340 }
339 c.dispatching = true341 c.dispatching = true
340 defer func() { c.dispatching = false }()342 defer func() { c.dispatching = false }()
343+ if err := c.seatErr; err != nil {
344+ c.seatErr = nil
345+ h.Error(err)
346+ }
341 var ev event347 var ev event
342 for range ringSize {348 for range ringSize {
343 if !c.q.pop(&ev) {349 if !c.q.pop(&ev) {
@@ -364,6 +370,7 @@ func (c *Conn) apply(ev *event, h Handler) {
364 }370 }
365 case evGlobalRemove:371 case evGlobalRemove:
366 c.removeOutput(ev.global, h)372 c.removeOutput(ev.global, h)
373+ c.seatGlobalRemoved(ev.global, h)
367 case evOutputName:374 case evOutputName:
368 if o := c.entry(ev.global); o != nil {375 if o := c.entry(ev.global); o != nil {
369 o.nameLen = copy(o.nameBuf[:], ev.nameBytes())376 o.nameLen = copy(o.nameBuf[:], ev.nameBytes())
@@ -381,6 +388,10 @@ func (c *Conn) apply(ev *event, h Handler) {
381 c.commitOutput(o, h)388 c.commitOutput(o, h)
382 }389 }
383 case evFDReady:390 case evFDReady:
391+ if c.seatTimer(ev.fd) {
392+ c.seatRepeat(h)
393+ return
394+ }
384 id, ok := c.watched[ev.fd]395 id, ok := c.watched[ev.fd]
385 if !ok {396 if !ok {
386 return // unwatched while the event was queued397 return // unwatched while the event was queued
@@ -395,6 +406,10 @@ func (c *Conn) apply(ev *event, h Handler) {
395 }406 }
396 }407 }
397 default:408 default:
409+ if ev.kind >= evSeatCaps {
410+ c.applySeat(ev, h)
411+ return
412+ }
398 c.applySurface(ev, h)413 c.applySurface(ev, h)
399 }414 }
400 }415 }
@@ -590,6 +605,7 @@ func (c *Conn) Close() error {
590 if c.efd >= 0 {605 if c.efd >= 0 {
591 _ = unix.Close(c.efd)606 _ = unix.Close(c.efd)
592 }607 }
608+ c.closeSeat()
593 c.q.closeFDs() // both producers are gone: close what nobody will drain609 c.q.closeFDs() // both producers are gone: close what nobody will drain
594 return err610 return err
595 }611 }
M handler.go +23 -0
@@ -50,6 +50,24 @@ type Handler interface {
5050 // LockFinished reports ext_session_lock_v1.finished: the compositor
5151 // refused or ended the lock. Destroy it with [Lock.Close].
5252 LockFinished()
53+
54+ // Pointer reports pointer input over one of the connection's surfaces:
55+ // enter, leave, motion, buttons and scrolling. ev is valid only during the
56+ // call.
57+ Pointer(ev *PointerEvent)
58+ // Key reports a key press, repeat or release for the surface that has
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
61+ // [SecretBuffer] is installed with [Seat.SetSecret], Text is always nil
62+ // and the text goes into the buffer; see [Handler.SecretChanged]. ev is
63+ // valid only during the call.
64+ Key(ev *KeyEvent)
65+ // KeyboardFocus reports that a surface gained or lost keyboard focus.
66+ KeyboardFocus(id SurfaceID, focused bool)
67+ // SecretChanged reports that a key press, repeat or backspace changed the
68+ // installed [SecretBuffer]; count is its length in code points. It is
69+ // called after the [Handler.Key] call of that key.
70+ SecretChanged(count int)
5371 }
5472
5573 // NopHandler implements every Handler method as a no-op. Embed it in a handler
@@ -69,3 +87,8 @@ func (NopHandler) Closed(SurfaceID) {}
6987 func (NopHandler) FeedbackDone(SurfaceID) {}
7088 func (NopHandler) Locked() {}
7189 func (NopHandler) LockFinished() {}
90+
91+func (NopHandler) Pointer(*PointerEvent) {}
92+func (NopHandler) Key(*KeyEvent) {}
93+func (NopHandler) KeyboardFocus(SurfaceID, bool) {}
94+func (NopHandler) SecretChanged(int) {}
@@ -50,6 +50,24 @@ type Handler interface {
50 // LockFinished reports ext_session_lock_v1.finished: the compositor50 // LockFinished reports ext_session_lock_v1.finished: the compositor
51 // refused or ended the lock. Destroy it with [Lock.Close].51 // refused or ended the lock. Destroy it with [Lock.Close].
52 LockFinished()52 LockFinished()
53+
54+ // Pointer reports pointer input over one of the connection's surfaces:
55+ // enter, leave, motion, buttons and scrolling. ev is valid only during the
56+ // call.
57+ Pointer(ev *PointerEvent)
58+ // Key reports a key press, repeat or release for the surface that has
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
61+ // [SecretBuffer] is installed with [Seat.SetSecret], Text is always nil
62+ // and the text goes into the buffer; see [Handler.SecretChanged]. ev is
63+ // valid only during the call.
64+ Key(ev *KeyEvent)
65+ // KeyboardFocus reports that a surface gained or lost keyboard focus.
66+ KeyboardFocus(id SurfaceID, focused bool)
67+ // SecretChanged reports that a key press, repeat or backspace changed the
68+ // installed [SecretBuffer]; count is its length in code points. It is
69+ // called after the [Handler.Key] call of that key.
70+ SecretChanged(count int)
53 }71 }
54 72
55 // NopHandler implements every Handler method as a no-op. Embed it in a handler73 // NopHandler implements every Handler method as a no-op. Embed it in a handler
@@ -69,3 +87,8 @@ func (NopHandler) Closed(SurfaceID) {}
69 func (NopHandler) FeedbackDone(SurfaceID) {}87 func (NopHandler) FeedbackDone(SurfaceID) {}
70 func (NopHandler) Locked() {}88 func (NopHandler) Locked() {}
71 func (NopHandler) LockFinished() {}89 func (NopHandler) LockFinished() {}
90+
91+func (NopHandler) Pointer(*PointerEvent) {}
92+func (NopHandler) Key(*KeyEvent) {}
93+func (NopHandler) KeyboardFocus(SurfaceID, bool) {}
94+func (NopHandler) SecretChanged(int) {}
M handler_mock_test.go +166 -0
@@ -296,6 +296,92 @@ func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id SurfaceID)) *MockHand
296296 return _c
297297 }
298298
299+// Key provides a mock function for the type MockHandler
300+func (_mock *MockHandler) Key(ev *KeyEvent) {
301+ _mock.Called(ev)
302+ return
303+}
304+
305+// MockHandler_Key_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Key'
306+type MockHandler_Key_Call struct {
307+ *mock.Call
308+}
309+
310+// Key is a helper method to define mock.On call
311+// - ev *KeyEvent
312+func (_e *MockHandler_Expecter) Key(ev any) *MockHandler_Key_Call {
313+ return &MockHandler_Key_Call{Call: _e.mock.On("Key", ev)}
314+}
315+
316+func (_c *MockHandler_Key_Call) Run(run func(ev *KeyEvent)) *MockHandler_Key_Call {
317+ _c.Call.Run(func(args mock.Arguments) {
318+ var arg0 *KeyEvent
319+ if args[0] != nil {
320+ arg0 = args[0].(*KeyEvent)
321+ }
322+ run(
323+ arg0,
324+ )
325+ })
326+ return _c
327+}
328+
329+func (_c *MockHandler_Key_Call) Return() *MockHandler_Key_Call {
330+ _c.Call.Return()
331+ return _c
332+}
333+
334+func (_c *MockHandler_Key_Call) RunAndReturn(run func(ev *KeyEvent)) *MockHandler_Key_Call {
335+ _c.Run(run)
336+ return _c
337+}
338+
339+// KeyboardFocus provides a mock function for the type MockHandler
340+func (_mock *MockHandler) KeyboardFocus(id SurfaceID, focused bool) {
341+ _mock.Called(id, focused)
342+ return
343+}
344+
345+// MockHandler_KeyboardFocus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'KeyboardFocus'
346+type MockHandler_KeyboardFocus_Call struct {
347+ *mock.Call
348+}
349+
350+// KeyboardFocus is a helper method to define mock.On call
351+// - id SurfaceID
352+// - focused bool
353+func (_e *MockHandler_Expecter) KeyboardFocus(id any, focused any) *MockHandler_KeyboardFocus_Call {
354+ return &MockHandler_KeyboardFocus_Call{Call: _e.mock.On("KeyboardFocus", id, focused)}
355+}
356+
357+func (_c *MockHandler_KeyboardFocus_Call) Run(run func(id SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
358+ _c.Call.Run(func(args mock.Arguments) {
359+ var arg0 SurfaceID
360+ if args[0] != nil {
361+ arg0 = args[0].(SurfaceID)
362+ }
363+ var arg1 bool
364+ if args[1] != nil {
365+ arg1 = args[1].(bool)
366+ }
367+ run(
368+ arg0,
369+ arg1,
370+ )
371+ })
372+ return _c
373+}
374+
375+func (_c *MockHandler_KeyboardFocus_Call) Return() *MockHandler_KeyboardFocus_Call {
376+ _c.Call.Return()
377+ return _c
378+}
379+
380+func (_c *MockHandler_KeyboardFocus_Call) RunAndReturn(run func(id SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
381+ _c.Run(run)
382+ return _c
383+}
384+
299385 // LockFinished provides a mock function for the type MockHandler
300386 func (_mock *MockHandler) LockFinished() {
301387 _mock.Called()
@@ -442,6 +528,46 @@ func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32))
442528 return _c
443529 }
444530
531+// Pointer provides a mock function for the type MockHandler
532+func (_mock *MockHandler) Pointer(ev *PointerEvent) {
533+ _mock.Called(ev)
534+ return
535+}
536+
537+// MockHandler_Pointer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pointer'
538+type MockHandler_Pointer_Call struct {
539+ *mock.Call
540+}
541+
542+// Pointer is a helper method to define mock.On call
543+// - ev *PointerEvent
544+func (_e *MockHandler_Expecter) Pointer(ev any) *MockHandler_Pointer_Call {
545+ return &MockHandler_Pointer_Call{Call: _e.mock.On("Pointer", ev)}
546+}
547+
548+func (_c *MockHandler_Pointer_Call) Run(run func(ev *PointerEvent)) *MockHandler_Pointer_Call {
549+ _c.Call.Run(func(args mock.Arguments) {
550+ var arg0 *PointerEvent
551+ if args[0] != nil {
552+ arg0 = args[0].(*PointerEvent)
553+ }
554+ run(
555+ arg0,
556+ )
557+ })
558+ return _c
559+}
560+
561+func (_c *MockHandler_Pointer_Call) Return() *MockHandler_Pointer_Call {
562+ _c.Call.Return()
563+ return _c
564+}
565+
566+func (_c *MockHandler_Pointer_Call) RunAndReturn(run func(ev *PointerEvent)) *MockHandler_Pointer_Call {
567+ _c.Run(run)
568+ return _c
569+}
570+
445571 // Scale provides a mock function for the type MockHandler
446572 func (_mock *MockHandler) Scale(id SurfaceID, scale float64) {
447573 _mock.Called(id, scale)
@@ -487,3 +613,43 @@ func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id SurfaceID, scale floa
487613 _c.Run(run)
488614 return _c
489615 }
616+
617+// SecretChanged provides a mock function for the type MockHandler
618+func (_mock *MockHandler) SecretChanged(count int) {
619+ _mock.Called(count)
620+ return
621+}
622+
623+// MockHandler_SecretChanged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SecretChanged'
624+type MockHandler_SecretChanged_Call struct {
625+ *mock.Call
626+}
627+
628+// SecretChanged is a helper method to define mock.On call
629+// - count int
630+func (_e *MockHandler_Expecter) SecretChanged(count any) *MockHandler_SecretChanged_Call {
631+ return &MockHandler_SecretChanged_Call{Call: _e.mock.On("SecretChanged", count)}
632+}
633+
634+func (_c *MockHandler_SecretChanged_Call) Run(run func(count int)) *MockHandler_SecretChanged_Call {
635+ _c.Call.Run(func(args mock.Arguments) {
636+ var arg0 int
637+ if args[0] != nil {
638+ arg0 = args[0].(int)
639+ }
640+ run(
641+ arg0,
642+ )
643+ })
644+ return _c
645+}
646+
647+func (_c *MockHandler_SecretChanged_Call) Return() *MockHandler_SecretChanged_Call {
648+ _c.Call.Return()
649+ return _c
650+}
651+
652+func (_c *MockHandler_SecretChanged_Call) RunAndReturn(run func(count int)) *MockHandler_SecretChanged_Call {
653+ _c.Run(run)
654+ return _c
655+}
@@ -296,6 +296,92 @@ func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id SurfaceID)) *MockHand
296 return _c296 return _c
297 }297 }
298 298
299+// Key provides a mock function for the type MockHandler
300+func (_mock *MockHandler) Key(ev *KeyEvent) {
301+ _mock.Called(ev)
302+ return
303+}
304+
305+// MockHandler_Key_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Key'
306+type MockHandler_Key_Call struct {
307+ *mock.Call
308+}
309+
310+// Key is a helper method to define mock.On call
311+// - ev *KeyEvent
312+func (_e *MockHandler_Expecter) Key(ev any) *MockHandler_Key_Call {
313+ return &MockHandler_Key_Call{Call: _e.mock.On("Key", ev)}
314+}
315+
316+func (_c *MockHandler_Key_Call) Run(run func(ev *KeyEvent)) *MockHandler_Key_Call {
317+ _c.Call.Run(func(args mock.Arguments) {
318+ var arg0 *KeyEvent
319+ if args[0] != nil {
320+ arg0 = args[0].(*KeyEvent)
321+ }
322+ run(
323+ arg0,
324+ )
325+ })
326+ return _c
327+}
328+
329+func (_c *MockHandler_Key_Call) Return() *MockHandler_Key_Call {
330+ _c.Call.Return()
331+ return _c
332+}
333+
334+func (_c *MockHandler_Key_Call) RunAndReturn(run func(ev *KeyEvent)) *MockHandler_Key_Call {
335+ _c.Run(run)
336+ return _c
337+}
338+
339+// KeyboardFocus provides a mock function for the type MockHandler
340+func (_mock *MockHandler) KeyboardFocus(id SurfaceID, focused bool) {
341+ _mock.Called(id, focused)
342+ return
343+}
344+
345+// MockHandler_KeyboardFocus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'KeyboardFocus'
346+type MockHandler_KeyboardFocus_Call struct {
347+ *mock.Call
348+}
349+
350+// KeyboardFocus is a helper method to define mock.On call
351+// - id SurfaceID
352+// - focused bool
353+func (_e *MockHandler_Expecter) KeyboardFocus(id any, focused any) *MockHandler_KeyboardFocus_Call {
354+ return &MockHandler_KeyboardFocus_Call{Call: _e.mock.On("KeyboardFocus", id, focused)}
355+}
356+
357+func (_c *MockHandler_KeyboardFocus_Call) Run(run func(id SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
358+ _c.Call.Run(func(args mock.Arguments) {
359+ var arg0 SurfaceID
360+ if args[0] != nil {
361+ arg0 = args[0].(SurfaceID)
362+ }
363+ var arg1 bool
364+ if args[1] != nil {
365+ arg1 = args[1].(bool)
366+ }
367+ run(
368+ arg0,
369+ arg1,
370+ )
371+ })
372+ return _c
373+}
374+
375+func (_c *MockHandler_KeyboardFocus_Call) Return() *MockHandler_KeyboardFocus_Call {
376+ _c.Call.Return()
377+ return _c
378+}
379+
380+func (_c *MockHandler_KeyboardFocus_Call) RunAndReturn(run func(id SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
381+ _c.Run(run)
382+ return _c
383+}
384+
299 // LockFinished provides a mock function for the type MockHandler385 // LockFinished provides a mock function for the type MockHandler
300 func (_mock *MockHandler) LockFinished() {386 func (_mock *MockHandler) LockFinished() {
301 _mock.Called()387 _mock.Called()
@@ -442,6 +528,46 @@ func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32))
442 return _c528 return _c
443 }529 }
444 530
531+// Pointer provides a mock function for the type MockHandler
532+func (_mock *MockHandler) Pointer(ev *PointerEvent) {
533+ _mock.Called(ev)
534+ return
535+}
536+
537+// MockHandler_Pointer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pointer'
538+type MockHandler_Pointer_Call struct {
539+ *mock.Call
540+}
541+
542+// Pointer is a helper method to define mock.On call
543+// - ev *PointerEvent
544+func (_e *MockHandler_Expecter) Pointer(ev any) *MockHandler_Pointer_Call {
545+ return &MockHandler_Pointer_Call{Call: _e.mock.On("Pointer", ev)}
546+}
547+
548+func (_c *MockHandler_Pointer_Call) Run(run func(ev *PointerEvent)) *MockHandler_Pointer_Call {
549+ _c.Call.Run(func(args mock.Arguments) {
550+ var arg0 *PointerEvent
551+ if args[0] != nil {
552+ arg0 = args[0].(*PointerEvent)
553+ }
554+ run(
555+ arg0,
556+ )
557+ })
558+ return _c
559+}
560+
561+func (_c *MockHandler_Pointer_Call) Return() *MockHandler_Pointer_Call {
562+ _c.Call.Return()
563+ return _c
564+}
565+
566+func (_c *MockHandler_Pointer_Call) RunAndReturn(run func(ev *PointerEvent)) *MockHandler_Pointer_Call {
567+ _c.Run(run)
568+ return _c
569+}
570+
445 // Scale provides a mock function for the type MockHandler571 // Scale provides a mock function for the type MockHandler
446 func (_mock *MockHandler) Scale(id SurfaceID, scale float64) {572 func (_mock *MockHandler) Scale(id SurfaceID, scale float64) {
447 _mock.Called(id, scale)573 _mock.Called(id, scale)
@@ -487,3 +613,43 @@ func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id SurfaceID, scale floa
487 _c.Run(run)613 _c.Run(run)
488 return _c614 return _c
489 }615 }
616+
617+// SecretChanged provides a mock function for the type MockHandler
618+func (_mock *MockHandler) SecretChanged(count int) {
619+ _mock.Called(count)
620+ return
621+}
622+
623+// MockHandler_SecretChanged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SecretChanged'
624+type MockHandler_SecretChanged_Call struct {
625+ *mock.Call
626+}
627+
628+// SecretChanged is a helper method to define mock.On call
629+// - count int
630+func (_e *MockHandler_Expecter) SecretChanged(count any) *MockHandler_SecretChanged_Call {
631+ return &MockHandler_SecretChanged_Call{Call: _e.mock.On("SecretChanged", count)}
632+}
633+
634+func (_c *MockHandler_SecretChanged_Call) Run(run func(count int)) *MockHandler_SecretChanged_Call {
635+ _c.Call.Run(func(args mock.Arguments) {
636+ var arg0 int
637+ if args[0] != nil {
638+ arg0 = args[0].(int)
639+ }
640+ run(
641+ arg0,
642+ )
643+ })
644+ return _c
645+}
646+
647+func (_c *MockHandler_SecretChanged_Call) Return() *MockHandler_SecretChanged_Call {
648+ _c.Call.Return()
649+ return _c
650+}
651+
652+func (_c *MockHandler_SecretChanged_Call) RunAndReturn(run func(count int)) *MockHandler_SecretChanged_Call {
653+ _c.Run(run)
654+ return _c
655+}
A keyboard.go +463 -0
@@ -0,0 +1,463 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "io"
6+ "os"
7+ "time"
8+
9+ xkb "github.com/bnema/purego-xkbcommon"
10+ "github.com/bnema/purego-xkbcommon/raw"
11+ "golang.org/x/sys/unix"
12+
13+ "github.com/bnema/neferclient/internal/secret"
14+)
15+
16+// Modifiers is the set of active keyboard modifiers.
17+type Modifiers uint8
18+
19+const (
20+ ModShift Modifiers = 1 << iota
21+ ModCtrl
22+ ModAlt
23+ ModSuper
24+ ModCapsLock
25+ ModNumLock
26+)
27+
28+// modNames are the xkb modifier names behind each Modifiers bit, in order.
29+var modNames = [...]string{"Shift", "Control", "Mod1", "Mod4", "Lock", "Mod2"}
30+
31+const (
32+ maxHeldKeys = 32
33+ scratchSize = 64 // longest key or compose text kept; longer yields no text
34+ keymapMax = 16 << 20
35+)
36+
37+type heldKey struct{ evdev, sym uint32 }
38+
39+type textKind uint8
40+
41+const (
42+ kindNone textKind = iota
43+ kindText
44+ kindBackspace
45+)
46+
47+// 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
49+// through the fixed scratch array, which is wiped after every use.
50+type keyboard struct {
51+ ctx *xkb.Context
52+ keymap *xkb.Keymap
53+ state *xkb.State
54+ compose *xkb.ComposeState
55+
56+ modBits [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent
57+ lastMask [6]uint32
58+ haveMask bool
59+
60+ held []heldKey
61+ focus bool
62+ rate int32 // repeats per second, 0 disables
63+ delay int32 // milliseconds before the first repeat
64+ repeating uint32
65+ composePending bool
66+
67+ tfd int // repeat timerfd
68+ tbuf [8]byte
69+
70+ secret *SecretBuffer
71+ scratch [scratchSize]byte
72+
73+ // Work area of doSecret, which secret.Do runs without arguments.
74+ secretFn func()
75+ w struct {
76+ evdev, sym uint32
77+ mods Modifiers
78+ repeat bool
79+ kind textKind
80+ changed bool
81+ err error
82+ }
83+ changed bool // the last translation edited the SecretBuffer
84+}
85+
86+func newKeyboard(locale string) (*keyboard, error) {
87+ c, err := xkb.NewContext()
88+ if err != nil {
89+ return nil, err
90+ }
91+ k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}
92+ k.secretFn = k.doSecret
93+ if t, e := c.NewComposeTable(locale); e == nil {
94+ k.compose, _ = t.NewState() // compose is optional: without it, dead keys still come from xkb
95+ _ = t.Close()
96+ }
97+ if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {
98+ _ = c.Close()
99+ return nil, err
100+ }
101+ return k, nil
102+}
103+
104+// composeLocale picks the locale for the compose table the way libc does.
105+func composeLocale() string {
106+ for _, name := range []string{"LC_ALL", "LC_CTYPE", "LANG"} {
107+ if v := os.Getenv(name); v != "" {
108+ return v
109+ }
110+ }
111+ return "C"
112+}
113+
114+func (k *keyboard) close() {
115+ k.disarm()
116+ if k.tfd >= 0 {
117+ _ = unix.Close(k.tfd)
118+ k.tfd = -1
119+ }
120+ if k.compose != nil {
121+ _ = k.compose.Close()
122+ k.compose = nil
123+ }
124+ if k.state != nil {
125+ _ = k.state.Close()
126+ k.state = nil
127+ }
128+ if k.keymap != nil {
129+ _ = k.keymap.Close()
130+ k.keymap = nil
131+ }
132+ if k.ctx != nil {
133+ _ = k.ctx.Close()
134+ k.ctx = nil
135+ }
136+}
137+
138+// replaceFD installs the keymap in fd. It consumes fd even on failure; the
139+// previous keymap stays valid on failure.
140+func (k *keyboard) replaceFD(fd, size int) error {
141+ f := os.NewFile(uintptr(fd), "keymap")
142+ if f == nil {
143+ return errors.New("neferclient: invalid keymap descriptor")
144+ }
145+ defer f.Close()
146+ km, err := k.ctx.NewKeymapFD(fd, size)
147+ if err != nil {
148+ return err
149+ }
150+ return k.replace(km)
151+}
152+
153+func (k *keyboard) replace(km *xkb.Keymap) error {
154+ s, err := km.NewState()
155+ if err != nil {
156+ _ = km.Close()
157+ return err
158+ }
159+ k.reset()
160+ if k.state != nil {
161+ _ = k.state.Close()
162+ }
163+ if k.keymap != nil {
164+ _ = k.keymap.Close()
165+ }
166+ k.keymap, k.state = km, s
167+ for i, name := range modNames {
168+ k.modBits[i] = 0
169+ if idx, e := km.ModIndex(name); e == nil && idx < 32 {
170+ k.modBits[i] = 1 << idx
171+ }
172+ }
173+ if k.haveMask { // the compositor's modifiers survive a keymap change
174+ _, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])
175+ }
176+ return nil
177+}
178+
179+func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
180+ k.lastMask = [6]uint32{depressed, latched, locked, 0, 0, group}
181+ k.haveMask = true
182+ if k.state == nil {
183+ return nil
184+ }
185+ _, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
186+ return err
187+}
188+
189+// reset drops held keys, repeat and compose state. It keeps the keymap, the
190+// modifier mask and the focus flag.
191+func (k *keyboard) reset() {
192+ k.held = k.held[:0]
193+ k.repeating = 0
194+ k.disarm()
195+ k.composePending = false
196+ if k.compose != nil {
197+ _ = k.compose.Reset()
198+ }
199+}
200+
201+func (k *keyboard) setFocus(on bool) {
202+ k.focus = on
203+ k.reset()
204+}
205+
206+func (k *keyboard) setSecret(b *SecretBuffer) {
207+ k.secret = b
208+ k.reset() // switching paths cancels held keys, repeat and compose
209+}
210+
211+func (k *keyboard) setRepeat(rate, delay int32) {
212+ k.rate, k.delay = max(rate, 0), max(delay, 0)
213+ if k.rate == 0 {
214+ k.repeating = 0
215+ k.disarm()
216+ }
217+}
218+
219+// arm starts the repeat timer: first expiry after the delay, then at the rate.
220+func (k *keyboard) arm() {
221+ spec := unix.ItimerSpec{
222+ Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),
223+ Value: unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm
224+ }
225+ _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)
226+}
227+
228+func (k *keyboard) disarm() {
229+ if k.tfd >= 0 {
230+ var spec unix.ItimerSpec
231+ _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)
232+ }
233+}
234+
235+// expired consumes a timer expiration. Missed ticks are not accumulated: one
236+// call yields at most one repeat.
237+func (k *keyboard) expired() bool {
238+ n, err := unix.Read(k.tfd, k.tbuf[:])
239+ return err == nil && n == 8
240+}
241+
242+func (k *keyboard) modifiers() Modifiers {
243+ mods, err := k.state.Mods()
244+ if err != nil {
245+ return 0
246+ }
247+ var m Modifiers
248+ for i, bit := range k.modBits {
249+ if bit != 0 && mods&bit != 0 {
250+ m |= 1 << i
251+ }
252+ }
253+ return m
254+}
255+
256+func (k *keyboard) find(evdev uint32) int {
257+ for i := range k.held {
258+ if k.held[i].evdev == evdev {
259+ return i
260+ }
261+ }
262+ return -1
263+}
264+
265+func (k *keyboard) drop(i int) { k.held = append(k.held[:i], k.held[i+1:]...) }
266+
267+// press translates a key press into ev. It reports false when the press must
268+// not be delivered (no focus or keymap).
269+func (k *keyboard) press(evdev uint32, ev *KeyEvent) (bool, error) {
270+ if !k.focus || k.state == nil {
271+ return false, nil
272+ }
273+ if i := k.find(evdev); i >= 0 {
274+ k.drop(i)
275+ }
276+ if len(k.held) == cap(k.held) {
277+ return false, nil
278+ }
279+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
280+ if err != nil {
281+ return false, err
282+ }
283+ kind, err := k.translate(evdev, sym, false, ev)
284+ if err != nil {
285+ return false, err
286+ }
287+ k.held = append(k.held, heldKey{evdev, sym})
288+ if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {
289+ if k.rate > 0 {
290+ k.repeating = evdev
291+ k.arm()
292+ }
293+ } else if k.secret != nil && k.repeating != 0 && repeatable(sym) {
294+ // A control or rejected key stops a previous printable repeat;
295+ // a modifier does not.
296+ k.repeating = 0
297+ k.disarm()
298+ }
299+ return true, nil
300+}
301+
302+// release fills ev for a key released after a delivered press.
303+func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
304+ i := k.find(evdev)
305+ if i < 0 {
306+ return false
307+ }
308+ h := k.held[i]
309+ k.drop(i)
310+ if k.repeating == evdev {
311+ k.repeating = 0
312+ k.disarm()
313+ }
314+ ev.Keycode, ev.Keysym, ev.Pressed = evdev, h.sym, false
315+ if k.state != nil {
316+ ev.Modifiers = k.modifiers()
317+ ev.Group, _ = k.state.Layout()
318+ }
319+ return true
320+}
321+
322+// repeat produces one repeat of the held key whose timer fired.
323+func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
324+ if !k.focus || k.state == nil || k.rate <= 0 || k.repeating == 0 || k.find(k.repeating) < 0 {
325+ return false, nil
326+ }
327+ evdev := k.repeating
328+ stop := func() (bool, error) {
329+ k.repeating = 0
330+ k.disarm()
331+ return false, nil
332+ }
333+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
334+ if err != nil {
335+ _, _ = stop()
336+ return false, err
337+ }
338+ if k.secret == nil && !repeatable(sym) {
339+ return stop()
340+ }
341+ kind, err := k.translate(evdev, sym, true, ev)
342+ if err != nil {
343+ _, _ = stop()
344+ return false, err
345+ }
346+ if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {
347+ return stop()
348+ }
349+ return true, nil
350+}
351+
352+// translate fills ev for a press or repeat and returns what the key meant to
353+// the SecretBuffer, if one is set. In secret mode ev.Text stays nil.
354+func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {
355+ ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat
356+ ev.Modifiers = k.modifiers()
357+ ev.Group, _ = k.state.Layout()
358+ k.changed = false
359+ if k.secret != nil {
360+ w := &k.w
361+ w.evdev, w.sym, w.mods, w.repeat = evdev, sym, ev.Modifiers, repeat
362+ secret.Do(k.secretFn)
363+ k.changed = w.changed
364+ return w.kind, w.err
365+ }
366+ n, err := k.text(evdev, sym, repeat)
367+ if err != nil {
368+ return kindNone, err
369+ }
370+ if n > 0 {
371+ ev.Text = k.scratch[:n:n]
372+ }
373+ return kindNone, nil
374+}
375+
376+// doSecret translates one key into the SecretBuffer. It is the only code that
377+// handles secret text, so it runs under runtime/secret when available.
378+func (k *keyboard) doSecret() {
379+ w := &k.w
380+ w.kind, w.changed, w.err = kindNone, false, nil
381+ defer clear(k.scratch[:])
382+ switch {
383+ case w.sym == raw.XKB_KEY_BackSpace:
384+ w.kind = kindBackspace
385+ k.resetCompose()
386+ w.changed = k.secret.backspace()
387+ case w.sym == raw.XKB_KEY_Return || w.sym == raw.XKB_KEY_KP_Enter || w.sym == raw.XKB_KEY_Escape:
388+ k.resetCompose() // controls are delivered as keys only
389+ case w.mods&ModCtrl != 0:
390+ // Other Control combinations are neither secret text nor ours to handle.
391+ default:
392+ n, err := k.text(w.evdev, w.sym, w.repeat)
393+ if err != nil {
394+ w.err = err
395+ return
396+ }
397+ if classifySecretText(k.scratch[:n]) {
398+ w.kind = kindText
399+ w.changed = k.secret.appendText(k.scratch[:n])
400+ }
401+ }
402+}
403+
404+func (k *keyboard) resetCompose() {
405+ if k.compose != nil {
406+ _ = k.compose.Reset()
407+ }
408+ k.composePending = false
409+}
410+
411+// text writes the text of a key into scratch and returns its length. Compose
412+// is fed only by physical presses: a repeat neither advances a sequence nor
413+// replays composed text.
414+func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
415+ if k.compose != nil && !repeat {
416+ if _, err := k.compose.Feed(sym); err != nil {
417+ return 0, err
418+ }
419+ status, err := k.compose.Status()
420+ if err != nil {
421+ return 0, err
422+ }
423+ switch status {
424+ case raw.XKB_COMPOSE_COMPOSING:
425+ k.composePending = true
426+ return 0, nil
427+ case raw.XKB_COMPOSE_CANCELLED:
428+ k.resetCompose()
429+ return 0, nil
430+ case raw.XKB_COMPOSE_COMPOSED:
431+ n, err := k.compose.UTF8Into(k.scratch[:])
432+ k.resetCompose()
433+ return shortAsEmpty(n, err)
434+ default:
435+ k.composePending = false
436+ }
437+ }
438+ if k.composePending {
439+ return 0, nil
440+ }
441+ n, err := k.state.UTF8Into(xkb.WaylandKeycode(evdev), k.scratch[:])
442+ return shortAsEmpty(n, err)
443+}
444+
445+// shortAsEmpty maps a too-long text to no text; xkb already wiped the buffer.
446+func shortAsEmpty(n int, err error) (int, error) {
447+ if errors.Is(err, io.ErrShortBuffer) {
448+ return 0, nil
449+ }
450+ return n, err
451+}
452+
453+// Modifier, dead and compose keys never repeat. The compositor provides rate
454+// and delay; its keymap is authoritative for text and layout.
455+func repeatable(sym uint32) bool {
456+ switch sym {
457+ case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
458+ raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
459+ raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock, raw.XKB_KEY_Multi_key:
460+ return false
461+ }
462+ return sym < raw.XKB_KEY_dead_grave || sym > raw.XKB_KEY_dead_currency
463+}
@@ -0,0 +1,463 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "io"
6+ "os"
7+ "time"
8+
9+ xkb "github.com/bnema/purego-xkbcommon"
10+ "github.com/bnema/purego-xkbcommon/raw"
11+ "golang.org/x/sys/unix"
12+
13+ "github.com/bnema/neferclient/internal/secret"
14+)
15+
16+// Modifiers is the set of active keyboard modifiers.
17+type Modifiers uint8
18+
19+const (
20+ ModShift Modifiers = 1 << iota
21+ ModCtrl
22+ ModAlt
23+ ModSuper
24+ ModCapsLock
25+ ModNumLock
26+)
27+
28+// modNames are the xkb modifier names behind each Modifiers bit, in order.
29+var modNames = [...]string{"Shift", "Control", "Mod1", "Mod4", "Lock", "Mod2"}
30+
31+const (
32+ maxHeldKeys = 32
33+ scratchSize = 64 // longest key or compose text kept; longer yields no text
34+ keymapMax = 16 << 20
35+)
36+
37+type heldKey struct{ evdev, sym uint32 }
38+
39+type textKind uint8
40+
41+const (
42+ kindNone textKind = iota
43+ kindText
44+ kindBackspace
45+)
46+
47+// 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
49+// through the fixed scratch array, which is wiped after every use.
50+type keyboard struct {
51+ ctx *xkb.Context
52+ keymap *xkb.Keymap
53+ state *xkb.State
54+ compose *xkb.ComposeState
55+
56+ modBits [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent
57+ lastMask [6]uint32
58+ haveMask bool
59+
60+ held []heldKey
61+ focus bool
62+ rate int32 // repeats per second, 0 disables
63+ delay int32 // milliseconds before the first repeat
64+ repeating uint32
65+ composePending bool
66+
67+ tfd int // repeat timerfd
68+ tbuf [8]byte
69+
70+ secret *SecretBuffer
71+ scratch [scratchSize]byte
72+
73+ // Work area of doSecret, which secret.Do runs without arguments.
74+ secretFn func()
75+ w struct {
76+ evdev, sym uint32
77+ mods Modifiers
78+ repeat bool
79+ kind textKind
80+ changed bool
81+ err error
82+ }
83+ changed bool // the last translation edited the SecretBuffer
84+}
85+
86+func newKeyboard(locale string) (*keyboard, error) {
87+ c, err := xkb.NewContext()
88+ if err != nil {
89+ return nil, err
90+ }
91+ k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}
92+ k.secretFn = k.doSecret
93+ if t, e := c.NewComposeTable(locale); e == nil {
94+ k.compose, _ = t.NewState() // compose is optional: without it, dead keys still come from xkb
95+ _ = t.Close()
96+ }
97+ if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {
98+ _ = c.Close()
99+ return nil, err
100+ }
101+ return k, nil
102+}
103+
104+// composeLocale picks the locale for the compose table the way libc does.
105+func composeLocale() string {
106+ for _, name := range []string{"LC_ALL", "LC_CTYPE", "LANG"} {
107+ if v := os.Getenv(name); v != "" {
108+ return v
109+ }
110+ }
111+ return "C"
112+}
113+
114+func (k *keyboard) close() {
115+ k.disarm()
116+ if k.tfd >= 0 {
117+ _ = unix.Close(k.tfd)
118+ k.tfd = -1
119+ }
120+ if k.compose != nil {
121+ _ = k.compose.Close()
122+ k.compose = nil
123+ }
124+ if k.state != nil {
125+ _ = k.state.Close()
126+ k.state = nil
127+ }
128+ if k.keymap != nil {
129+ _ = k.keymap.Close()
130+ k.keymap = nil
131+ }
132+ if k.ctx != nil {
133+ _ = k.ctx.Close()
134+ k.ctx = nil
135+ }
136+}
137+
138+// replaceFD installs the keymap in fd. It consumes fd even on failure; the
139+// previous keymap stays valid on failure.
140+func (k *keyboard) replaceFD(fd, size int) error {
141+ f := os.NewFile(uintptr(fd), "keymap")
142+ if f == nil {
143+ return errors.New("neferclient: invalid keymap descriptor")
144+ }
145+ defer f.Close()
146+ km, err := k.ctx.NewKeymapFD(fd, size)
147+ if err != nil {
148+ return err
149+ }
150+ return k.replace(km)
151+}
152+
153+func (k *keyboard) replace(km *xkb.Keymap) error {
154+ s, err := km.NewState()
155+ if err != nil {
156+ _ = km.Close()
157+ return err
158+ }
159+ k.reset()
160+ if k.state != nil {
161+ _ = k.state.Close()
162+ }
163+ if k.keymap != nil {
164+ _ = k.keymap.Close()
165+ }
166+ k.keymap, k.state = km, s
167+ for i, name := range modNames {
168+ k.modBits[i] = 0
169+ if idx, e := km.ModIndex(name); e == nil && idx < 32 {
170+ k.modBits[i] = 1 << idx
171+ }
172+ }
173+ if k.haveMask { // the compositor's modifiers survive a keymap change
174+ _, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])
175+ }
176+ return nil
177+}
178+
179+func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
180+ k.lastMask = [6]uint32{depressed, latched, locked, 0, 0, group}
181+ k.haveMask = true
182+ if k.state == nil {
183+ return nil
184+ }
185+ _, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
186+ return err
187+}
188+
189+// reset drops held keys, repeat and compose state. It keeps the keymap, the
190+// modifier mask and the focus flag.
191+func (k *keyboard) reset() {
192+ k.held = k.held[:0]
193+ k.repeating = 0
194+ k.disarm()
195+ k.composePending = false
196+ if k.compose != nil {
197+ _ = k.compose.Reset()
198+ }
199+}
200+
201+func (k *keyboard) setFocus(on bool) {
202+ k.focus = on
203+ k.reset()
204+}
205+
206+func (k *keyboard) setSecret(b *SecretBuffer) {
207+ k.secret = b
208+ k.reset() // switching paths cancels held keys, repeat and compose
209+}
210+
211+func (k *keyboard) setRepeat(rate, delay int32) {
212+ k.rate, k.delay = max(rate, 0), max(delay, 0)
213+ if k.rate == 0 {
214+ k.repeating = 0
215+ k.disarm()
216+ }
217+}
218+
219+// arm starts the repeat timer: first expiry after the delay, then at the rate.
220+func (k *keyboard) arm() {
221+ spec := unix.ItimerSpec{
222+ Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),
223+ Value: unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm
224+ }
225+ _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)
226+}
227+
228+func (k *keyboard) disarm() {
229+ if k.tfd >= 0 {
230+ var spec unix.ItimerSpec
231+ _ = unix.TimerfdSettime(k.tfd, 0, &spec, nil)
232+ }
233+}
234+
235+// expired consumes a timer expiration. Missed ticks are not accumulated: one
236+// call yields at most one repeat.
237+func (k *keyboard) expired() bool {
238+ n, err := unix.Read(k.tfd, k.tbuf[:])
239+ return err == nil && n == 8
240+}
241+
242+func (k *keyboard) modifiers() Modifiers {
243+ mods, err := k.state.Mods()
244+ if err != nil {
245+ return 0
246+ }
247+ var m Modifiers
248+ for i, bit := range k.modBits {
249+ if bit != 0 && mods&bit != 0 {
250+ m |= 1 << i
251+ }
252+ }
253+ return m
254+}
255+
256+func (k *keyboard) find(evdev uint32) int {
257+ for i := range k.held {
258+ if k.held[i].evdev == evdev {
259+ return i
260+ }
261+ }
262+ return -1
263+}
264+
265+func (k *keyboard) drop(i int) { k.held = append(k.held[:i], k.held[i+1:]...) }
266+
267+// press translates a key press into ev. It reports false when the press must
268+// not be delivered (no focus or keymap).
269+func (k *keyboard) press(evdev uint32, ev *KeyEvent) (bool, error) {
270+ if !k.focus || k.state == nil {
271+ return false, nil
272+ }
273+ if i := k.find(evdev); i >= 0 {
274+ k.drop(i)
275+ }
276+ if len(k.held) == cap(k.held) {
277+ return false, nil
278+ }
279+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
280+ if err != nil {
281+ return false, err
282+ }
283+ kind, err := k.translate(evdev, sym, false, ev)
284+ if err != nil {
285+ return false, err
286+ }
287+ k.held = append(k.held, heldKey{evdev, sym})
288+ if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {
289+ if k.rate > 0 {
290+ k.repeating = evdev
291+ k.arm()
292+ }
293+ } else if k.secret != nil && k.repeating != 0 && repeatable(sym) {
294+ // A control or rejected key stops a previous printable repeat;
295+ // a modifier does not.
296+ k.repeating = 0
297+ k.disarm()
298+ }
299+ return true, nil
300+}
301+
302+// release fills ev for a key released after a delivered press.
303+func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
304+ i := k.find(evdev)
305+ if i < 0 {
306+ return false
307+ }
308+ h := k.held[i]
309+ k.drop(i)
310+ if k.repeating == evdev {
311+ k.repeating = 0
312+ k.disarm()
313+ }
314+ ev.Keycode, ev.Keysym, ev.Pressed = evdev, h.sym, false
315+ if k.state != nil {
316+ ev.Modifiers = k.modifiers()
317+ ev.Group, _ = k.state.Layout()
318+ }
319+ return true
320+}
321+
322+// repeat produces one repeat of the held key whose timer fired.
323+func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
324+ if !k.focus || k.state == nil || k.rate <= 0 || k.repeating == 0 || k.find(k.repeating) < 0 {
325+ return false, nil
326+ }
327+ evdev := k.repeating
328+ stop := func() (bool, error) {
329+ k.repeating = 0
330+ k.disarm()
331+ return false, nil
332+ }
333+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
334+ if err != nil {
335+ _, _ = stop()
336+ return false, err
337+ }
338+ if k.secret == nil && !repeatable(sym) {
339+ return stop()
340+ }
341+ kind, err := k.translate(evdev, sym, true, ev)
342+ if err != nil {
343+ _, _ = stop()
344+ return false, err
345+ }
346+ if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {
347+ return stop()
348+ }
349+ return true, nil
350+}
351+
352+// translate fills ev for a press or repeat and returns what the key meant to
353+// the SecretBuffer, if one is set. In secret mode ev.Text stays nil.
354+func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {
355+ ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat
356+ ev.Modifiers = k.modifiers()
357+ ev.Group, _ = k.state.Layout()
358+ k.changed = false
359+ if k.secret != nil {
360+ w := &k.w
361+ w.evdev, w.sym, w.mods, w.repeat = evdev, sym, ev.Modifiers, repeat
362+ secret.Do(k.secretFn)
363+ k.changed = w.changed
364+ return w.kind, w.err
365+ }
366+ n, err := k.text(evdev, sym, repeat)
367+ if err != nil {
368+ return kindNone, err
369+ }
370+ if n > 0 {
371+ ev.Text = k.scratch[:n:n]
372+ }
373+ return kindNone, nil
374+}
375+
376+// doSecret translates one key into the SecretBuffer. It is the only code that
377+// handles secret text, so it runs under runtime/secret when available.
378+func (k *keyboard) doSecret() {
379+ w := &k.w
380+ w.kind, w.changed, w.err = kindNone, false, nil
381+ defer clear(k.scratch[:])
382+ switch {
383+ case w.sym == raw.XKB_KEY_BackSpace:
384+ w.kind = kindBackspace
385+ k.resetCompose()
386+ w.changed = k.secret.backspace()
387+ case w.sym == raw.XKB_KEY_Return || w.sym == raw.XKB_KEY_KP_Enter || w.sym == raw.XKB_KEY_Escape:
388+ k.resetCompose() // controls are delivered as keys only
389+ case w.mods&ModCtrl != 0:
390+ // Other Control combinations are neither secret text nor ours to handle.
391+ default:
392+ n, err := k.text(w.evdev, w.sym, w.repeat)
393+ if err != nil {
394+ w.err = err
395+ return
396+ }
397+ if classifySecretText(k.scratch[:n]) {
398+ w.kind = kindText
399+ w.changed = k.secret.appendText(k.scratch[:n])
400+ }
401+ }
402+}
403+
404+func (k *keyboard) resetCompose() {
405+ if k.compose != nil {
406+ _ = k.compose.Reset()
407+ }
408+ k.composePending = false
409+}
410+
411+// text writes the text of a key into scratch and returns its length. Compose
412+// is fed only by physical presses: a repeat neither advances a sequence nor
413+// replays composed text.
414+func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
415+ if k.compose != nil && !repeat {
416+ if _, err := k.compose.Feed(sym); err != nil {
417+ return 0, err
418+ }
419+ status, err := k.compose.Status()
420+ if err != nil {
421+ return 0, err
422+ }
423+ switch status {
424+ case raw.XKB_COMPOSE_COMPOSING:
425+ k.composePending = true
426+ return 0, nil
427+ case raw.XKB_COMPOSE_CANCELLED:
428+ k.resetCompose()
429+ return 0, nil
430+ case raw.XKB_COMPOSE_COMPOSED:
431+ n, err := k.compose.UTF8Into(k.scratch[:])
432+ k.resetCompose()
433+ return shortAsEmpty(n, err)
434+ default:
435+ k.composePending = false
436+ }
437+ }
438+ if k.composePending {
439+ return 0, nil
440+ }
441+ n, err := k.state.UTF8Into(xkb.WaylandKeycode(evdev), k.scratch[:])
442+ return shortAsEmpty(n, err)
443+}
444+
445+// shortAsEmpty maps a too-long text to no text; xkb already wiped the buffer.
446+func shortAsEmpty(n int, err error) (int, error) {
447+ if errors.Is(err, io.ErrShortBuffer) {
448+ return 0, nil
449+ }
450+ return n, err
451+}
452+
453+// Modifier, dead and compose keys never repeat. The compositor provides rate
454+// and delay; its keymap is authoritative for text and layout.
455+func repeatable(sym uint32) bool {
456+ switch sym {
457+ case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
458+ raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
459+ raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock, raw.XKB_KEY_Multi_key:
460+ return false
461+ }
462+ return sym < raw.XKB_KEY_dead_grave || sym > raw.XKB_KEY_dead_currency
463+}
A keyboard_test.go +283 -0
@@ -0,0 +1,283 @@
1+package neferclient
2+
3+import (
4+ "os"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+ "golang.org/x/sys/unix"
9+
10+ "github.com/bnema/purego-xkbcommon/raw"
11+)
12+
13+// Evdev codes used below (US QWERTY positions).
14+const (
15+ keyQ = 16
16+ keyE = 18
17+ keyBackspace = 14
18+ keyEnter = 28
19+ keyU = 22
20+ keyLeftShift = 42
21+ keyCircum = 41 // de: dead_circumflex
22+)
23+
24+// testKeyboard is a keyboard on a real xkb keymap compiled from rules.
25+func testKeyboard(t testing.TB, locale, layout string) *keyboard {
26+ t.Helper()
27+ k, err := newKeyboard(locale)
28+ require.NoError(t, err)
29+ t.Cleanup(k.close)
30+ km, err := k.ctx.NewKeymapRules("evdev", "pc105", layout, "", "")
31+ require.NoError(t, err)
32+ require.NoError(t, k.replace(km))
33+ k.setFocus(true)
34+ return k
35+}
36+
37+func pressKey(t testing.TB, k *keyboard, code uint32) (KeyEvent, bool) {
38+ t.Helper()
39+ var ev KeyEvent
40+ ok, err := k.press(code, &ev)
41+ require.NoError(t, err)
42+ return ev, ok
43+}
44+
45+func releaseKey(k *keyboard, code uint32) (KeyEvent, bool) {
46+ var ev KeyEvent
47+ ok := k.release(code, &ev)
48+ return ev, ok
49+}
50+
51+func TestKeyboardLayoutsAndModifiers(t *testing.T) {
52+ for _, tc := range []struct {
53+ layout string
54+ code uint32
55+ want string
56+ }{{"us", keyQ, "q"}, {"fr", keyQ, "a"}, {"de", 21, "z"}} {
57+ k := testKeyboard(t, "en_US.UTF-8", tc.layout)
58+ ev, ok := pressKey(t, k, tc.code)
59+ require.True(t, ok)
60+ require.Equal(t, tc.want, string(ev.Text), tc.layout)
61+ require.True(t, ev.Pressed)
62+ require.NotZero(t, ev.Keysym)
63+ require.Equal(t, tc.code, ev.Keycode)
64+ rel, ok := releaseKey(k, tc.code)
65+ require.True(t, ok)
66+ require.False(t, rel.Pressed)
67+ require.Nil(t, rel.Text)
68+ require.Equal(t, ev.Keysym, rel.Keysym)
69+ }
70+
71+ k := testKeyboard(t, "en_US.UTF-8", "us")
72+ idx, err := k.keymap.ModIndex("Shift")
73+ require.NoError(t, err)
74+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
75+ ev, _ := pressKey(t, k, keyQ)
76+ require.Equal(t, "Q", string(ev.Text))
77+ require.Equal(t, ModShift, ev.Modifiers)
78+
79+ // The compositor's mask survives a keymap change.
80+ km, err := k.ctx.NewKeymapRules("evdev", "pc105", "fr", "", "")
81+ require.NoError(t, err)
82+ require.NoError(t, k.replace(km))
83+ k.setFocus(true)
84+ ev, _ = pressKey(t, k, keyQ)
85+ require.Equal(t, "A", string(ev.Text))
86+}
87+
88+func TestKeyboardKeymapFD(t *testing.T) {
89+ data, err := os.ReadFile("testdata/fr.xkb")
90+ require.NoError(t, err)
91+ f, err := os.CreateTemp(t.TempDir(), "keymap")
92+ require.NoError(t, err)
93+ defer f.Close()
94+ _, err = f.Write(append(data, 0))
95+ require.NoError(t, err)
96+
97+ k := testKeyboard(t, "", "us")
98+ fd, err := unix.Dup(int(f.Fd()))
99+ require.NoError(t, err)
100+ require.NoError(t, k.replaceFD(fd, len(data)+1))
101+ _, err = unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
102+ require.ErrorIs(t, err, unix.EBADF, "replaceFD consumes the descriptor")
103+ k.setFocus(true)
104+ ev, _ := pressKey(t, k, keyQ)
105+ require.Equal(t, "a", string(ev.Text))
106+
107+ // A failing map keeps the previous one and still consumes its fd.
108+ bad, err := unix.Dup(int(f.Fd()))
109+ require.NoError(t, err)
110+ require.Error(t, k.replaceFD(bad, 5))
111+ _, err = unix.FcntlInt(uintptr(bad), unix.F_GETFD, 0)
112+ require.ErrorIs(t, err, unix.EBADF)
113+ ev, _ = pressKey(t, k, keyQ)
114+ require.Equal(t, "a", string(ev.Text))
115+}
116+
117+func TestKeyboardComposeAndFocus(t *testing.T) {
118+ k := testKeyboard(t, "en_US.UTF-8", "de")
119+ dead, _ := pressKey(t, k, keyCircum)
120+ require.Equal(t, uint32(raw.XKB_KEY_dead_circumflex), dead.Keysym)
121+ require.Nil(t, dead.Text)
122+ releaseKey(k, keyCircum)
123+ composed, _ := pressKey(t, k, keyE)
124+ require.Equal(t, "ê", string(composed.Text))
125+ releaseKey(k, keyE)
126+
127+ pressKey(t, k, keyCircum)
128+ k.setFocus(false)
129+ _, ok := pressKey2(k, keyE)
130+ require.False(t, ok, "no key without focus")
131+ k.setFocus(true)
132+ ev, _ := pressKey(t, k, keyE)
133+ require.Equal(t, "e", string(ev.Text), "focus loss resets compose")
134+}
135+
136+func pressKey2(k *keyboard, code uint32) (KeyEvent, bool) {
137+ var ev KeyEvent
138+ ok, _ := k.press(code, &ev)
139+ return ev, ok
140+}
141+
142+func TestKeyboardRepeatTimer(t *testing.T) {
143+ k := testKeyboard(t, "", "us")
144+ k.setRepeat(20, 50)
145+ _, ok := pressKey(t, k, keyQ)
146+ require.True(t, ok)
147+ require.Equal(t, uint32(keyQ), k.repeating)
148+ var ev KeyEvent
149+ require.Eventually(t, k.expired, 2e9, 5e6)
150+ ok, err := k.repeat(&ev)
151+ require.NoError(t, err)
152+ require.True(t, ok)
153+ require.True(t, ev.Repeat)
154+ require.Equal(t, "q", string(ev.Text))
155+
156+ releaseKey(k, keyQ)
157+ require.Zero(t, k.repeating)
158+ require.False(t, k.expired(), "timer disarmed on release")
159+
160+ // Modifiers never repeat.
161+ pressKey(t, k, keyLeftShift)
162+ require.Zero(t, k.repeating)
163+ // A rate of 0 disables repeat.
164+ k.setRepeat(0, 0)
165+ pressKey(t, k, keyQ)
166+ require.Zero(t, k.repeating)
167+}
168+
169+func TestKeyboardRepeatTracksCurrentMask(t *testing.T) {
170+ k := testKeyboard(t, "", "us")
171+ k.setRepeat(100, 1)
172+ pressKey(t, k, keyQ)
173+ idx, err := k.keymap.ModIndex("Shift")
174+ require.NoError(t, err)
175+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
176+ require.Eventually(t, k.expired, 2e9, 1e6)
177+ var ev KeyEvent
178+ ok, err := k.repeat(&ev)
179+ require.NoError(t, err)
180+ require.True(t, ok)
181+ require.Equal(t, "Q", string(ev.Text))
182+}
183+
184+// TestSecretKeysNeverReachText covers the secret rules end to end on the
185+// keyboard: printable text only into the buffer, BackSpace by code point,
186+// every other key delivered without text.
187+func TestSecretKeysNeverReachText(t *testing.T) {
188+ k := testKeyboard(t, "en_US.UTF-8", "de")
189+ buf := NewSecretBuffer(8)
190+ k.setSecret(buf)
191+
192+ press := func(code uint32) KeyEvent {
193+ ev, ok := pressKey(t, k, code)
194+ require.True(t, ok)
195+ require.Nil(t, ev.Text, "secret mode never fills KeyEvent.Text")
196+ releaseKey(k, code)
197+ return ev
198+ }
199+ press(keyQ)
200+ press(keyCircum) // dead key
201+ press(keyE) // composes ê
202+ require.Equal(t, "qê", string(buf.Bytes()))
203+ require.Equal(t, 2, buf.Len())
204+
205+ press(keyBackspace)
206+ require.Equal(t, 1, buf.Len(), "backspace removes one code point")
207+ press(keyBackspace)
208+ press(keyBackspace) // empty: nothing to remove
209+ require.Zero(t, buf.Len())
210+
211+ press(keyQ)
212+ enter := press(keyEnter)
213+ require.Equal(t, uint32(raw.XKB_KEY_Return), enter.Keysym)
214+ require.Equal(t, 1, buf.Len(), "Enter is delivered, not typed")
215+
216+ // Control combinations are keys, not text.
217+ idx, err := k.keymap.ModIndex("Control")
218+ require.NoError(t, err)
219+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
220+ ev := press(keyU)
221+ require.Equal(t, ModCtrl, ev.Modifiers)
222+ require.Equal(t, 1, buf.Len())
223+ require.NoError(t, k.setMask(0, 0, 0, 0))
224+
225+ // Capacity: extra text is dropped, the key is still delivered.
226+ for range 10 {
227+ press(keyQ)
228+ }
229+ require.Equal(t, 8, buf.Len())
230+
231+ // Scratch is wiped after use.
232+ require.Equal(t, make([]byte, scratchSize), k.scratch[:])
233+
234+ k.setSecret(nil)
235+ ev, _ = pressKey(t, k, keyQ)
236+ require.Equal(t, "q", string(ev.Text), "ordinary text again once the buffer is removed")
237+}
238+
239+func TestSecretChangedFlag(t *testing.T) {
240+ k := testKeyboard(t, "", "us")
241+ buf := NewSecretBuffer(2)
242+ k.setSecret(buf)
243+ pressKey(t, k, keyQ)
244+ require.True(t, k.changed)
245+ pressKey(t, k, keyEnter)
246+ require.False(t, k.changed)
247+ pressKey(t, k, keyBackspace)
248+ require.True(t, k.changed)
249+ pressKey(t, k, keyBackspace)
250+ require.False(t, k.changed, "nothing left to remove")
251+}
252+
253+func TestSecretRepeat(t *testing.T) {
254+ k := testKeyboard(t, "", "us")
255+ buf := NewSecretBuffer(16)
256+ k.setSecret(buf)
257+ k.setRepeat(100, 1)
258+ var ev KeyEvent
259+ pressKey(t, k, keyQ)
260+ for range 2 {
261+ require.Eventually(t, k.expired, 2e9, 1e6)
262+ ok, err := k.repeat(&ev)
263+ require.NoError(t, err)
264+ require.True(t, ok)
265+ require.Nil(t, ev.Text)
266+ }
267+ require.Equal(t, "qqq", string(buf.Bytes()))
268+
269+ // Enter cancels a previous printable repeat.
270+ pressKey(t, k, keyEnter)
271+ require.Zero(t, k.repeating)
272+ releaseKey(k, keyQ)
273+ releaseKey(k, keyEnter)
274+
275+ // Held backspace repeats.
276+ pressKey(t, k, keyBackspace)
277+ require.Equal(t, uint32(keyBackspace), k.repeating)
278+ require.Eventually(t, k.expired, 2e9, 1e6)
279+ ok, err := k.repeat(&ev)
280+ require.NoError(t, err)
281+ require.True(t, ok)
282+ require.Equal(t, 1, buf.Len())
283+}
@@ -0,0 +1,283 @@
1+package neferclient
2+
3+import (
4+ "os"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+ "golang.org/x/sys/unix"
9+
10+ "github.com/bnema/purego-xkbcommon/raw"
11+)
12+
13+// Evdev codes used below (US QWERTY positions).
14+const (
15+ keyQ = 16
16+ keyE = 18
17+ keyBackspace = 14
18+ keyEnter = 28
19+ keyU = 22
20+ keyLeftShift = 42
21+ keyCircum = 41 // de: dead_circumflex
22+)
23+
24+// testKeyboard is a keyboard on a real xkb keymap compiled from rules.
25+func testKeyboard(t testing.TB, locale, layout string) *keyboard {
26+ t.Helper()
27+ k, err := newKeyboard(locale)
28+ require.NoError(t, err)
29+ t.Cleanup(k.close)
30+ km, err := k.ctx.NewKeymapRules("evdev", "pc105", layout, "", "")
31+ require.NoError(t, err)
32+ require.NoError(t, k.replace(km))
33+ k.setFocus(true)
34+ return k
35+}
36+
37+func pressKey(t testing.TB, k *keyboard, code uint32) (KeyEvent, bool) {
38+ t.Helper()
39+ var ev KeyEvent
40+ ok, err := k.press(code, &ev)
41+ require.NoError(t, err)
42+ return ev, ok
43+}
44+
45+func releaseKey(k *keyboard, code uint32) (KeyEvent, bool) {
46+ var ev KeyEvent
47+ ok := k.release(code, &ev)
48+ return ev, ok
49+}
50+
51+func TestKeyboardLayoutsAndModifiers(t *testing.T) {
52+ for _, tc := range []struct {
53+ layout string
54+ code uint32
55+ want string
56+ }{{"us", keyQ, "q"}, {"fr", keyQ, "a"}, {"de", 21, "z"}} {
57+ k := testKeyboard(t, "en_US.UTF-8", tc.layout)
58+ ev, ok := pressKey(t, k, tc.code)
59+ require.True(t, ok)
60+ require.Equal(t, tc.want, string(ev.Text), tc.layout)
61+ require.True(t, ev.Pressed)
62+ require.NotZero(t, ev.Keysym)
63+ require.Equal(t, tc.code, ev.Keycode)
64+ rel, ok := releaseKey(k, tc.code)
65+ require.True(t, ok)
66+ require.False(t, rel.Pressed)
67+ require.Nil(t, rel.Text)
68+ require.Equal(t, ev.Keysym, rel.Keysym)
69+ }
70+
71+ k := testKeyboard(t, "en_US.UTF-8", "us")
72+ idx, err := k.keymap.ModIndex("Shift")
73+ require.NoError(t, err)
74+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
75+ ev, _ := pressKey(t, k, keyQ)
76+ require.Equal(t, "Q", string(ev.Text))
77+ require.Equal(t, ModShift, ev.Modifiers)
78+
79+ // The compositor's mask survives a keymap change.
80+ km, err := k.ctx.NewKeymapRules("evdev", "pc105", "fr", "", "")
81+ require.NoError(t, err)
82+ require.NoError(t, k.replace(km))
83+ k.setFocus(true)
84+ ev, _ = pressKey(t, k, keyQ)
85+ require.Equal(t, "A", string(ev.Text))
86+}
87+
88+func TestKeyboardKeymapFD(t *testing.T) {
89+ data, err := os.ReadFile("testdata/fr.xkb")
90+ require.NoError(t, err)
91+ f, err := os.CreateTemp(t.TempDir(), "keymap")
92+ require.NoError(t, err)
93+ defer f.Close()
94+ _, err = f.Write(append(data, 0))
95+ require.NoError(t, err)
96+
97+ k := testKeyboard(t, "", "us")
98+ fd, err := unix.Dup(int(f.Fd()))
99+ require.NoError(t, err)
100+ require.NoError(t, k.replaceFD(fd, len(data)+1))
101+ _, err = unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
102+ require.ErrorIs(t, err, unix.EBADF, "replaceFD consumes the descriptor")
103+ k.setFocus(true)
104+ ev, _ := pressKey(t, k, keyQ)
105+ require.Equal(t, "a", string(ev.Text))
106+
107+ // A failing map keeps the previous one and still consumes its fd.
108+ bad, err := unix.Dup(int(f.Fd()))
109+ require.NoError(t, err)
110+ require.Error(t, k.replaceFD(bad, 5))
111+ _, err = unix.FcntlInt(uintptr(bad), unix.F_GETFD, 0)
112+ require.ErrorIs(t, err, unix.EBADF)
113+ ev, _ = pressKey(t, k, keyQ)
114+ require.Equal(t, "a", string(ev.Text))
115+}
116+
117+func TestKeyboardComposeAndFocus(t *testing.T) {
118+ k := testKeyboard(t, "en_US.UTF-8", "de")
119+ dead, _ := pressKey(t, k, keyCircum)
120+ require.Equal(t, uint32(raw.XKB_KEY_dead_circumflex), dead.Keysym)
121+ require.Nil(t, dead.Text)
122+ releaseKey(k, keyCircum)
123+ composed, _ := pressKey(t, k, keyE)
124+ require.Equal(t, "ê", string(composed.Text))
125+ releaseKey(k, keyE)
126+
127+ pressKey(t, k, keyCircum)
128+ k.setFocus(false)
129+ _, ok := pressKey2(k, keyE)
130+ require.False(t, ok, "no key without focus")
131+ k.setFocus(true)
132+ ev, _ := pressKey(t, k, keyE)
133+ require.Equal(t, "e", string(ev.Text), "focus loss resets compose")
134+}
135+
136+func pressKey2(k *keyboard, code uint32) (KeyEvent, bool) {
137+ var ev KeyEvent
138+ ok, _ := k.press(code, &ev)
139+ return ev, ok
140+}
141+
142+func TestKeyboardRepeatTimer(t *testing.T) {
143+ k := testKeyboard(t, "", "us")
144+ k.setRepeat(20, 50)
145+ _, ok := pressKey(t, k, keyQ)
146+ require.True(t, ok)
147+ require.Equal(t, uint32(keyQ), k.repeating)
148+ var ev KeyEvent
149+ require.Eventually(t, k.expired, 2e9, 5e6)
150+ ok, err := k.repeat(&ev)
151+ require.NoError(t, err)
152+ require.True(t, ok)
153+ require.True(t, ev.Repeat)
154+ require.Equal(t, "q", string(ev.Text))
155+
156+ releaseKey(k, keyQ)
157+ require.Zero(t, k.repeating)
158+ require.False(t, k.expired(), "timer disarmed on release")
159+
160+ // Modifiers never repeat.
161+ pressKey(t, k, keyLeftShift)
162+ require.Zero(t, k.repeating)
163+ // A rate of 0 disables repeat.
164+ k.setRepeat(0, 0)
165+ pressKey(t, k, keyQ)
166+ require.Zero(t, k.repeating)
167+}
168+
169+func TestKeyboardRepeatTracksCurrentMask(t *testing.T) {
170+ k := testKeyboard(t, "", "us")
171+ k.setRepeat(100, 1)
172+ pressKey(t, k, keyQ)
173+ idx, err := k.keymap.ModIndex("Shift")
174+ require.NoError(t, err)
175+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
176+ require.Eventually(t, k.expired, 2e9, 1e6)
177+ var ev KeyEvent
178+ ok, err := k.repeat(&ev)
179+ require.NoError(t, err)
180+ require.True(t, ok)
181+ require.Equal(t, "Q", string(ev.Text))
182+}
183+
184+// TestSecretKeysNeverReachText covers the secret rules end to end on the
185+// keyboard: printable text only into the buffer, BackSpace by code point,
186+// every other key delivered without text.
187+func TestSecretKeysNeverReachText(t *testing.T) {
188+ k := testKeyboard(t, "en_US.UTF-8", "de")
189+ buf := NewSecretBuffer(8)
190+ k.setSecret(buf)
191+
192+ press := func(code uint32) KeyEvent {
193+ ev, ok := pressKey(t, k, code)
194+ require.True(t, ok)
195+ require.Nil(t, ev.Text, "secret mode never fills KeyEvent.Text")
196+ releaseKey(k, code)
197+ return ev
198+ }
199+ press(keyQ)
200+ press(keyCircum) // dead key
201+ press(keyE) // composes ê
202+ require.Equal(t, "qê", string(buf.Bytes()))
203+ require.Equal(t, 2, buf.Len())
204+
205+ press(keyBackspace)
206+ require.Equal(t, 1, buf.Len(), "backspace removes one code point")
207+ press(keyBackspace)
208+ press(keyBackspace) // empty: nothing to remove
209+ require.Zero(t, buf.Len())
210+
211+ press(keyQ)
212+ enter := press(keyEnter)
213+ require.Equal(t, uint32(raw.XKB_KEY_Return), enter.Keysym)
214+ require.Equal(t, 1, buf.Len(), "Enter is delivered, not typed")
215+
216+ // Control combinations are keys, not text.
217+ idx, err := k.keymap.ModIndex("Control")
218+ require.NoError(t, err)
219+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
220+ ev := press(keyU)
221+ require.Equal(t, ModCtrl, ev.Modifiers)
222+ require.Equal(t, 1, buf.Len())
223+ require.NoError(t, k.setMask(0, 0, 0, 0))
224+
225+ // Capacity: extra text is dropped, the key is still delivered.
226+ for range 10 {
227+ press(keyQ)
228+ }
229+ require.Equal(t, 8, buf.Len())
230+
231+ // Scratch is wiped after use.
232+ require.Equal(t, make([]byte, scratchSize), k.scratch[:])
233+
234+ k.setSecret(nil)
235+ ev, _ = pressKey(t, k, keyQ)
236+ require.Equal(t, "q", string(ev.Text), "ordinary text again once the buffer is removed")
237+}
238+
239+func TestSecretChangedFlag(t *testing.T) {
240+ k := testKeyboard(t, "", "us")
241+ buf := NewSecretBuffer(2)
242+ k.setSecret(buf)
243+ pressKey(t, k, keyQ)
244+ require.True(t, k.changed)
245+ pressKey(t, k, keyEnter)
246+ require.False(t, k.changed)
247+ pressKey(t, k, keyBackspace)
248+ require.True(t, k.changed)
249+ pressKey(t, k, keyBackspace)
250+ require.False(t, k.changed, "nothing left to remove")
251+}
252+
253+func TestSecretRepeat(t *testing.T) {
254+ k := testKeyboard(t, "", "us")
255+ buf := NewSecretBuffer(16)
256+ k.setSecret(buf)
257+ k.setRepeat(100, 1)
258+ var ev KeyEvent
259+ pressKey(t, k, keyQ)
260+ for range 2 {
261+ require.Eventually(t, k.expired, 2e9, 1e6)
262+ ok, err := k.repeat(&ev)
263+ require.NoError(t, err)
264+ require.True(t, ok)
265+ require.Nil(t, ev.Text)
266+ }
267+ require.Equal(t, "qqq", string(buf.Bytes()))
268+
269+ // Enter cancels a previous printable repeat.
270+ pressKey(t, k, keyEnter)
271+ require.Zero(t, k.repeating)
272+ releaseKey(k, keyQ)
273+ releaseKey(k, keyEnter)
274+
275+ // Held backspace repeats.
276+ pressKey(t, k, keyBackspace)
277+ require.Equal(t, uint32(keyBackspace), k.repeating)
278+ require.Eventually(t, k.expired, 2e9, 1e6)
279+ ok, err := k.repeat(&ev)
280+ require.NoError(t, err)
281+ require.True(t, ok)
282+ require.Equal(t, 1, buf.Len())
283+}
M mocks/handler_mock.go +166 -0
@@ -297,6 +297,92 @@ func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id neferclient.SurfaceID
297297 return _c
298298 }
299299
300+// Key provides a mock function for the type MockHandler
301+func (_mock *MockHandler) Key(ev *neferclient.KeyEvent) {
302+ _mock.Called(ev)
303+ return
304+}
305+
306+// MockHandler_Key_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Key'
307+type MockHandler_Key_Call struct {
308+ *mock.Call
309+}
310+
311+// Key is a helper method to define mock.On call
312+// - ev *neferclient.KeyEvent
313+func (_e *MockHandler_Expecter) Key(ev any) *MockHandler_Key_Call {
314+ return &MockHandler_Key_Call{Call: _e.mock.On("Key", ev)}
315+}
316+
317+func (_c *MockHandler_Key_Call) Run(run func(ev *neferclient.KeyEvent)) *MockHandler_Key_Call {
318+ _c.Call.Run(func(args mock.Arguments) {
319+ var arg0 *neferclient.KeyEvent
320+ if args[0] != nil {
321+ arg0 = args[0].(*neferclient.KeyEvent)
322+ }
323+ run(
324+ arg0,
325+ )
326+ })
327+ return _c
328+}
329+
330+func (_c *MockHandler_Key_Call) Return() *MockHandler_Key_Call {
331+ _c.Call.Return()
332+ return _c
333+}
334+
335+func (_c *MockHandler_Key_Call) RunAndReturn(run func(ev *neferclient.KeyEvent)) *MockHandler_Key_Call {
336+ _c.Run(run)
337+ return _c
338+}
339+
340+// KeyboardFocus provides a mock function for the type MockHandler
341+func (_mock *MockHandler) KeyboardFocus(id neferclient.SurfaceID, focused bool) {
342+ _mock.Called(id, focused)
343+ return
344+}
345+
346+// MockHandler_KeyboardFocus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'KeyboardFocus'
347+type MockHandler_KeyboardFocus_Call struct {
348+ *mock.Call
349+}
350+
351+// KeyboardFocus is a helper method to define mock.On call
352+// - id neferclient.SurfaceID
353+// - focused bool
354+func (_e *MockHandler_Expecter) KeyboardFocus(id any, focused any) *MockHandler_KeyboardFocus_Call {
355+ return &MockHandler_KeyboardFocus_Call{Call: _e.mock.On("KeyboardFocus", id, focused)}
356+}
357+
358+func (_c *MockHandler_KeyboardFocus_Call) Run(run func(id neferclient.SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
359+ _c.Call.Run(func(args mock.Arguments) {
360+ var arg0 neferclient.SurfaceID
361+ if args[0] != nil {
362+ arg0 = args[0].(neferclient.SurfaceID)
363+ }
364+ var arg1 bool
365+ if args[1] != nil {
366+ arg1 = args[1].(bool)
367+ }
368+ run(
369+ arg0,
370+ arg1,
371+ )
372+ })
373+ return _c
374+}
375+
376+func (_c *MockHandler_KeyboardFocus_Call) Return() *MockHandler_KeyboardFocus_Call {
377+ _c.Call.Return()
378+ return _c
379+}
380+
381+func (_c *MockHandler_KeyboardFocus_Call) RunAndReturn(run func(id neferclient.SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
382+ _c.Run(run)
383+ return _c
384+}
385+
300386 // LockFinished provides a mock function for the type MockHandler
301387 func (_mock *MockHandler) LockFinished() {
302388 _mock.Called()
@@ -443,6 +529,46 @@ func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32))
443529 return _c
444530 }
445531
532+// Pointer provides a mock function for the type MockHandler
533+func (_mock *MockHandler) Pointer(ev *neferclient.PointerEvent) {
534+ _mock.Called(ev)
535+ return
536+}
537+
538+// MockHandler_Pointer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pointer'
539+type MockHandler_Pointer_Call struct {
540+ *mock.Call
541+}
542+
543+// Pointer is a helper method to define mock.On call
544+// - ev *neferclient.PointerEvent
545+func (_e *MockHandler_Expecter) Pointer(ev any) *MockHandler_Pointer_Call {
546+ return &MockHandler_Pointer_Call{Call: _e.mock.On("Pointer", ev)}
547+}
548+
549+func (_c *MockHandler_Pointer_Call) Run(run func(ev *neferclient.PointerEvent)) *MockHandler_Pointer_Call {
550+ _c.Call.Run(func(args mock.Arguments) {
551+ var arg0 *neferclient.PointerEvent
552+ if args[0] != nil {
553+ arg0 = args[0].(*neferclient.PointerEvent)
554+ }
555+ run(
556+ arg0,
557+ )
558+ })
559+ return _c
560+}
561+
562+func (_c *MockHandler_Pointer_Call) Return() *MockHandler_Pointer_Call {
563+ _c.Call.Return()
564+ return _c
565+}
566+
567+func (_c *MockHandler_Pointer_Call) RunAndReturn(run func(ev *neferclient.PointerEvent)) *MockHandler_Pointer_Call {
568+ _c.Run(run)
569+ return _c
570+}
571+
446572 // Scale provides a mock function for the type MockHandler
447573 func (_mock *MockHandler) Scale(id neferclient.SurfaceID, scale float64) {
448574 _mock.Called(id, scale)
@@ -488,3 +614,43 @@ func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id neferclient.SurfaceID
488614 _c.Run(run)
489615 return _c
490616 }
617+
618+// SecretChanged provides a mock function for the type MockHandler
619+func (_mock *MockHandler) SecretChanged(count int) {
620+ _mock.Called(count)
621+ return
622+}
623+
624+// MockHandler_SecretChanged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SecretChanged'
625+type MockHandler_SecretChanged_Call struct {
626+ *mock.Call
627+}
628+
629+// SecretChanged is a helper method to define mock.On call
630+// - count int
631+func (_e *MockHandler_Expecter) SecretChanged(count any) *MockHandler_SecretChanged_Call {
632+ return &MockHandler_SecretChanged_Call{Call: _e.mock.On("SecretChanged", count)}
633+}
634+
635+func (_c *MockHandler_SecretChanged_Call) Run(run func(count int)) *MockHandler_SecretChanged_Call {
636+ _c.Call.Run(func(args mock.Arguments) {
637+ var arg0 int
638+ if args[0] != nil {
639+ arg0 = args[0].(int)
640+ }
641+ run(
642+ arg0,
643+ )
644+ })
645+ return _c
646+}
647+
648+func (_c *MockHandler_SecretChanged_Call) Return() *MockHandler_SecretChanged_Call {
649+ _c.Call.Return()
650+ return _c
651+}
652+
653+func (_c *MockHandler_SecretChanged_Call) RunAndReturn(run func(count int)) *MockHandler_SecretChanged_Call {
654+ _c.Run(run)
655+ return _c
656+}
@@ -297,6 +297,92 @@ func (_c *MockHandler_Frame_Call) RunAndReturn(run func(id neferclient.SurfaceID
297 return _c297 return _c
298 }298 }
299 299
300+// Key provides a mock function for the type MockHandler
301+func (_mock *MockHandler) Key(ev *neferclient.KeyEvent) {
302+ _mock.Called(ev)
303+ return
304+}
305+
306+// MockHandler_Key_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Key'
307+type MockHandler_Key_Call struct {
308+ *mock.Call
309+}
310+
311+// Key is a helper method to define mock.On call
312+// - ev *neferclient.KeyEvent
313+func (_e *MockHandler_Expecter) Key(ev any) *MockHandler_Key_Call {
314+ return &MockHandler_Key_Call{Call: _e.mock.On("Key", ev)}
315+}
316+
317+func (_c *MockHandler_Key_Call) Run(run func(ev *neferclient.KeyEvent)) *MockHandler_Key_Call {
318+ _c.Call.Run(func(args mock.Arguments) {
319+ var arg0 *neferclient.KeyEvent
320+ if args[0] != nil {
321+ arg0 = args[0].(*neferclient.KeyEvent)
322+ }
323+ run(
324+ arg0,
325+ )
326+ })
327+ return _c
328+}
329+
330+func (_c *MockHandler_Key_Call) Return() *MockHandler_Key_Call {
331+ _c.Call.Return()
332+ return _c
333+}
334+
335+func (_c *MockHandler_Key_Call) RunAndReturn(run func(ev *neferclient.KeyEvent)) *MockHandler_Key_Call {
336+ _c.Run(run)
337+ return _c
338+}
339+
340+// KeyboardFocus provides a mock function for the type MockHandler
341+func (_mock *MockHandler) KeyboardFocus(id neferclient.SurfaceID, focused bool) {
342+ _mock.Called(id, focused)
343+ return
344+}
345+
346+// MockHandler_KeyboardFocus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'KeyboardFocus'
347+type MockHandler_KeyboardFocus_Call struct {
348+ *mock.Call
349+}
350+
351+// KeyboardFocus is a helper method to define mock.On call
352+// - id neferclient.SurfaceID
353+// - focused bool
354+func (_e *MockHandler_Expecter) KeyboardFocus(id any, focused any) *MockHandler_KeyboardFocus_Call {
355+ return &MockHandler_KeyboardFocus_Call{Call: _e.mock.On("KeyboardFocus", id, focused)}
356+}
357+
358+func (_c *MockHandler_KeyboardFocus_Call) Run(run func(id neferclient.SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
359+ _c.Call.Run(func(args mock.Arguments) {
360+ var arg0 neferclient.SurfaceID
361+ if args[0] != nil {
362+ arg0 = args[0].(neferclient.SurfaceID)
363+ }
364+ var arg1 bool
365+ if args[1] != nil {
366+ arg1 = args[1].(bool)
367+ }
368+ run(
369+ arg0,
370+ arg1,
371+ )
372+ })
373+ return _c
374+}
375+
376+func (_c *MockHandler_KeyboardFocus_Call) Return() *MockHandler_KeyboardFocus_Call {
377+ _c.Call.Return()
378+ return _c
379+}
380+
381+func (_c *MockHandler_KeyboardFocus_Call) RunAndReturn(run func(id neferclient.SurfaceID, focused bool)) *MockHandler_KeyboardFocus_Call {
382+ _c.Run(run)
383+ return _c
384+}
385+
300 // LockFinished provides a mock function for the type MockHandler386 // LockFinished provides a mock function for the type MockHandler
301 func (_mock *MockHandler) LockFinished() {387 func (_mock *MockHandler) LockFinished() {
302 _mock.Called()388 _mock.Called()
@@ -443,6 +529,46 @@ func (_c *MockHandler_OutputRemoved_Call) RunAndReturn(run func(global uint32))
443 return _c529 return _c
444 }530 }
445 531
532+// Pointer provides a mock function for the type MockHandler
533+func (_mock *MockHandler) Pointer(ev *neferclient.PointerEvent) {
534+ _mock.Called(ev)
535+ return
536+}
537+
538+// MockHandler_Pointer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Pointer'
539+type MockHandler_Pointer_Call struct {
540+ *mock.Call
541+}
542+
543+// Pointer is a helper method to define mock.On call
544+// - ev *neferclient.PointerEvent
545+func (_e *MockHandler_Expecter) Pointer(ev any) *MockHandler_Pointer_Call {
546+ return &MockHandler_Pointer_Call{Call: _e.mock.On("Pointer", ev)}
547+}
548+
549+func (_c *MockHandler_Pointer_Call) Run(run func(ev *neferclient.PointerEvent)) *MockHandler_Pointer_Call {
550+ _c.Call.Run(func(args mock.Arguments) {
551+ var arg0 *neferclient.PointerEvent
552+ if args[0] != nil {
553+ arg0 = args[0].(*neferclient.PointerEvent)
554+ }
555+ run(
556+ arg0,
557+ )
558+ })
559+ return _c
560+}
561+
562+func (_c *MockHandler_Pointer_Call) Return() *MockHandler_Pointer_Call {
563+ _c.Call.Return()
564+ return _c
565+}
566+
567+func (_c *MockHandler_Pointer_Call) RunAndReturn(run func(ev *neferclient.PointerEvent)) *MockHandler_Pointer_Call {
568+ _c.Run(run)
569+ return _c
570+}
571+
446 // Scale provides a mock function for the type MockHandler572 // Scale provides a mock function for the type MockHandler
447 func (_mock *MockHandler) Scale(id neferclient.SurfaceID, scale float64) {573 func (_mock *MockHandler) Scale(id neferclient.SurfaceID, scale float64) {
448 _mock.Called(id, scale)574 _mock.Called(id, scale)
@@ -488,3 +614,43 @@ func (_c *MockHandler_Scale_Call) RunAndReturn(run func(id neferclient.SurfaceID
488 _c.Run(run)614 _c.Run(run)
489 return _c615 return _c
490 }616 }
617+
618+// SecretChanged provides a mock function for the type MockHandler
619+func (_mock *MockHandler) SecretChanged(count int) {
620+ _mock.Called(count)
621+ return
622+}
623+
624+// MockHandler_SecretChanged_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SecretChanged'
625+type MockHandler_SecretChanged_Call struct {
626+ *mock.Call
627+}
628+
629+// SecretChanged is a helper method to define mock.On call
630+// - count int
631+func (_e *MockHandler_Expecter) SecretChanged(count any) *MockHandler_SecretChanged_Call {
632+ return &MockHandler_SecretChanged_Call{Call: _e.mock.On("SecretChanged", count)}
633+}
634+
635+func (_c *MockHandler_SecretChanged_Call) Run(run func(count int)) *MockHandler_SecretChanged_Call {
636+ _c.Call.Run(func(args mock.Arguments) {
637+ var arg0 int
638+ if args[0] != nil {
639+ arg0 = args[0].(int)
640+ }
641+ run(
642+ arg0,
643+ )
644+ })
645+ return _c
646+}
647+
648+func (_c *MockHandler_SecretChanged_Call) Return() *MockHandler_SecretChanged_Call {
649+ _c.Call.Return()
650+ return _c
651+}
652+
653+func (_c *MockHandler_SecretChanged_Call) RunAndReturn(run func(count int)) *MockHandler_SecretChanged_Call {
654+ _c.Run(run)
655+ return _c
656+}
M queue.go +21 -1
@@ -39,6 +39,26 @@ const (
3939 evLocked // id = lock generation
4040 evLockFinished // id = lock generation
4141 evBadEvent // id, a = badEvent code: the reader met a malformed event
42+
43+ // Seat events (kind >= evSeatCaps). Pointer and keyboard events carry the
44+ // generation of the wl_pointer/wl_keyboard in version, so events of a
45+ // released device that are still queued are ignored. global is a wl_surface
46+ // object id where a surface is named.
47+ evSeatCaps // a = capabilities
48+ evSeatBad // a = bad code: a seat device event was malformed
49+ evPtrEnter // global = surface, serial, a, b = fixed x, y
50+ evPtrLeave // global = surface
51+ evPtrMotion // a, b = fixed x, y
52+ evPtrButton // flags = button, a = state
53+ evPtrAxis // flags = axis, a = fixed value
54+ evPtrAxisValue // flags = axis, a = value120
55+ evPtrAxisStop // flags = axis
56+ evKbKeymap // fd = owned descriptor, flags = size, a = format
57+ evKbEnter // global = surface, serial
58+ evKbLeave // global = surface
59+ evKbKey // flags = evdev code, a = state
60+ evKbModifiers // flags = depressed, dev = latched | locked<<32, a = group
61+ evKbRepeat // a = rate, b = delay
4262 )
4363
4464 // event is the only thing the reader and the epoll goroutine hand to the
@@ -60,7 +80,7 @@ type event struct {
6080
6181 // hasFD reports whether the event owns a received descriptor that must be
6282 // closed if the event is dropped.
63-func (e *event) hasFD() bool { return e.kind == evFeedbackTable && e.fd >= 0 }
83+func (e *event) hasFD() bool { return (e.kind == evFeedbackTable || e.kind == evKbKeymap) && e.fd >= 0 }
6484
6585 func (e *event) closeFD() {
6686 if e.hasFD() {
@@ -39,6 +39,26 @@ const (
39 evLocked // id = lock generation39 evLocked // id = lock generation
40 evLockFinished // id = lock generation40 evLockFinished // id = lock generation
41 evBadEvent // id, a = badEvent code: the reader met a malformed event41 evBadEvent // id, a = badEvent code: the reader met a malformed event
42+
43+ // Seat events (kind >= evSeatCaps). Pointer and keyboard events carry the
44+ // generation of the wl_pointer/wl_keyboard in version, so events of a
45+ // released device that are still queued are ignored. global is a wl_surface
46+ // object id where a surface is named.
47+ evSeatCaps // a = capabilities
48+ evSeatBad // a = bad code: a seat device event was malformed
49+ evPtrEnter // global = surface, serial, a, b = fixed x, y
50+ evPtrLeave // global = surface
51+ evPtrMotion // a, b = fixed x, y
52+ evPtrButton // flags = button, a = state
53+ evPtrAxis // flags = axis, a = fixed value
54+ evPtrAxisValue // flags = axis, a = value120
55+ evPtrAxisStop // flags = axis
56+ evKbKeymap // fd = owned descriptor, flags = size, a = format
57+ evKbEnter // global = surface, serial
58+ evKbLeave // global = surface
59+ evKbKey // flags = evdev code, a = state
60+ evKbModifiers // flags = depressed, dev = latched | locked<<32, a = group
61+ evKbRepeat // a = rate, b = delay
42 )62 )
43 63
44 // event is the only thing the reader and the epoll goroutine hand to the64 // event is the only thing the reader and the epoll goroutine hand to the
@@ -60,7 +80,7 @@ type event struct {
60 80
61 // hasFD reports whether the event owns a received descriptor that must be81 // hasFD reports whether the event owns a received descriptor that must be
62 // closed if the event is dropped.82 // closed if the event is dropped.
63-func (e *event) hasFD() bool { return e.kind == evFeedbackTable && e.fd >= 0 }83+func (e *event) hasFD() bool { return (e.kind == evFeedbackTable || e.kind == evKbKeymap) && e.fd >= 0 }
64 84
65 func (e *event) closeFD() {85 func (e *event) closeFD() {
66 if e.hasFD() {86 if e.hasFD() {
A seat.go +591 -0
@@ -0,0 +1,591 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/cursorshape"
8+ "github.com/bnema/go-wayland-bindings/client/wayland"
9+ "github.com/bnema/wlturbo/wl"
10+ "golang.org/x/sys/unix"
11+)
12+
13+// maxSeatVersion is the wl_seat version bound: 9 adds axis_value120 users and
14+// the release requests.
15+const maxSeatVersion = 9
16+
17+// CursorShape is a wp_cursor_shape_v1 shape. The values are the protocol's.
18+type CursorShape uint32
19+
20+const (
21+ CursorDefault CursorShape = iota + 1
22+ CursorContextMenu
23+ CursorHelp
24+ CursorPointer
25+ CursorProgress
26+ CursorWait
27+ CursorCell
28+ CursorCrosshair
29+ CursorText
30+ CursorVerticalText
31+ CursorAlias
32+ CursorCopy
33+ CursorMove
34+ CursorNoDrop
35+ CursorNotAllowed
36+ CursorGrab
37+ CursorGrabbing
38+ CursorEResize
39+ CursorNResize
40+ CursorNEResize
41+ CursorNWResize
42+ CursorSResize
43+ CursorSEResize
44+ CursorSWResize
45+ CursorWResize
46+ CursorEWResize
47+ CursorNSResize
48+ CursorNESWResize
49+ CursorNWSEResize
50+ CursorColResize
51+ CursorRowResize
52+ CursorAllScroll
53+ CursorZoomIn
54+ CursorZoomOut
55+ CursorDndAsk // needs cursor-shape version 2
56+ CursorAllResize // needs cursor-shape version 2
57+)
58+
59+// PointerKind tells which pointer event a [PointerEvent] is.
60+type PointerKind uint8
61+
62+const (
63+ PointerEnter PointerKind = iota + 1
64+ PointerLeave
65+ PointerMotion
66+ PointerButton
67+ // PointerAxis is a scroll step on Axis. DX or DY carry the continuous
68+ // value in logical pixels; Value120 carries the wheel value in 1/120 of a
69+ // notch. A compositor can send both for one physical step: use one.
70+ PointerAxis
71+ // PointerAxisStop ends a scroll sequence on Axis.
72+ PointerAxisStop
73+)
74+
75+// Scroll axes of [PointerEvent.Axis].
76+const (
77+ AxisVertical = 0
78+ AxisHorizontal = 1
79+)
80+
81+// PointerEvent is one pointer event over a surface of the connection. X and Y
82+// are surface-local logical pixels (already logical: never divide by scale)
83+// and hold the latest position for every kind.
84+type PointerEvent struct {
85+ Surface SurfaceID
86+ Kind PointerKind
87+ X, Y float64
88+ DX, DY float64 // PointerAxis: continuous scroll value
89+ Button uint32 // PointerButton: evdev button code
90+ Pressed bool // PointerButton
91+ Axis uint32 // PointerAxis, PointerAxisStop: AxisVertical or AxisHorizontal
92+ Value120 int32 // PointerAxis: wheel value in 1/120 notches
93+}
94+
95+// KeyEvent is one key press, repeat or release.
96+type KeyEvent struct {
97+ Surface SurfaceID
98+ Keycode uint32 // evdev key code
99+ Keysym uint32 // xkb keysym in the current layout and modifier state
100+ Modifiers Modifiers
101+ Group uint32 // effective layout index
102+ Pressed bool
103+ Repeat bool
104+ // 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
106+ // returns. It is always nil while a [SecretBuffer] is installed.
107+ Text []byte
108+}
109+
110+// 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
112+// no-op.
113+type Seat struct {
114+ c *Conn
115+ proxy *wayland.Seat
116+ name uint32 // registry name of the global
117+ ver uint32
118+
119+ pointer *wayland.Pointer
120+ ptrGen uint32 // generation of pointer; bumped on every create and drop
121+ ptrSurf SurfaceID
122+ px, py float64
123+ pev PointerEvent
124+
125+ keyboard *wayland.Keyboard
126+ kbGen uint32
127+ kbSurf SurfaceID
128+ kb *keyboard
129+ kev KeyEvent
130+
131+ secret *SecretBuffer
132+
133+ shapes *cursorshape.WpCursorShapeManager
134+ shapesVer uint32
135+ shapeDev *cursorshape.WpCursorShapeDevice
136+ enterSerial uint32 // latest pointer enter serial, 0 while outside
137+ shape CursorShape
138+}
139+
140+// Seat returns the connection's seat, binding wl_seat on first use. The seat
141+// is also bound when the first surface is created. Input starts flowing to the
142+// Handler once the compositor announces the capabilities.
143+func (c *Conn) Seat() *Seat {
144+ c.ensureSeat()
145+ return c.seat
146+}
147+
148+// ensureSeat binds wl_seat and the optional cursor-shape manager once. A
149+// failure is reported by the next Dispatch through Handler.Error.
150+func (c *Conn) ensureSeat() {
151+ if c.seat != nil || c.closed {
152+ return
153+ }
154+ s := &Seat{c: c}
155+ c.seat = s
156+ g, ok := c.display.Registry().FindGlobal(wayland.SeatInterface)
157+ if !ok {
158+ return
159+ }
160+ q := c.q
161+ proxy, v, err := c.Bind(wayland.SeatInterface, maxSeatVersion, func(ctx *wl.Context) *wayland.Seat {
162+ p := wayland.NewSeat(ctx)
163+ p.OnCapabilities(func(caps uint32) {
164+ e := event{kind: evSeatCaps, a: int32(caps)}
165+ q.post(&e)
166+ })
167+ return p
168+ })
169+ if err != nil {
170+ c.seatErr = fmt.Errorf("neferclient: bind wl_seat: %w", err)
171+ return
172+ }
173+ s.proxy, s.name, s.ver = proxy, g.Name, v
174+ m, mv, err := c.Bind(cursorshape.WpCursorShapeManagerInterface, 2, cursorshape.NewWpCursorShapeManager)
175+ if err != nil {
176+ if !errors.Is(err, ErrGlobalNotFound) {
177+ c.seatErr = fmt.Errorf("neferclient: bind cursor shape manager: %w", err)
178+ }
179+ return
180+ }
181+ s.shapes, s.shapesVer = m, mv
182+}
183+
184+// 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;
186+// 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.
189+func (s *Seat) SetSecret(b *SecretBuffer) {
190+ s.secret = b
191+ if s.kb != nil {
192+ s.kb.setSecret(b)
193+ }
194+}
195+
196+// SetCursor asks the compositor to show shape for the pointer. It is a no-op
197+// when the compositor has no cursor-shape support, the pointer is outside the
198+// connection's surfaces, or shape is unchanged since the last enter.
199+func (s *Seat) SetCursor(shape CursorShape) error {
200+ if s.c.closed {
201+ return ErrClosed
202+ }
203+ limit := CursorZoomOut
204+ if s.shapesVer >= 2 {
205+ limit = CursorAllResize
206+ }
207+ if shape < CursorDefault || shape > CursorAllResize {
208+ return fmt.Errorf("neferclient: invalid cursor shape %d", shape)
209+ }
210+ if s.shapes == nil || s.pointer == nil || s.enterSerial == 0 || shape == s.shape {
211+ return nil
212+ }
213+ if shape > limit {
214+ return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}
215+ }
216+ if s.shapeDev == nil {
217+ dev, err := s.shapes.GetPointer(s.pointer)
218+ if err != nil {
219+ return err
220+ }
221+ s.shapeDev = dev
222+ }
223+ if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {
224+ return err
225+ }
226+ s.shape = shape
227+ return nil
228+}
229+
230+// surfaceByObject finds the surface whose wl_surface has object id obj.
231+func (c *Conn) surfaceByObject(obj uint32) *Surface {
232+ for _, s := range c.surfaces {
233+ if !s.closed && s.surf != nil && s.surf.ID() == obj {
234+ return s
235+ }
236+ }
237+ return nil
238+}
239+
240+func (c *Conn) seatTimer(fd int32) bool {
241+ return c.seat != nil && c.seat.kb != nil && int(fd) == c.seat.kb.tfd
242+}
243+
244+// seatRepeat handles the repeat timer: at most one repeat per expiration.
245+func (c *Conn) seatRepeat(h Handler) {
246+ s := c.seat
247+ if s.kb.expired() {
248+ ke := &s.kev
249+ *ke = KeyEvent{Surface: s.kbSurf}
250+ ok, err := s.kb.repeat(ke)
251+ switch {
252+ case err != nil:
253+ h.Error(err)
254+ case ok:
255+ s.deliverKey(ke, h)
256+ }
257+ }
258+ if !c.closed {
259+ if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {
260+ h.Error(err)
261+ }
262+ }
263+}
264+
265+func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {
266+ h.Key(ke)
267+ clear(s.kb.scratch[:]) // ordinary text must not outlive the call
268+ ke.Text = nil
269+ if s.kb.changed {
270+ h.SecretChanged(s.secret.Len())
271+ }
272+}
273+
274+// seatSurfaceGone forgets focus on a closing surface.
275+func (c *Conn) seatSurfaceGone(sf *Surface) {
276+ s := c.seat
277+ if s == nil {
278+ return
279+ }
280+ if s.ptrSurf == sf.id {
281+ s.ptrSurf = 0
282+ s.enterSerial, s.shape = 0, 0
283+ }
284+ if s.kbSurf == sf.id {
285+ s.kbSurf = 0
286+ if s.kb != nil {
287+ s.kb.setFocus(false)
288+ }
289+ }
290+}
291+
292+func (c *Conn) seatGlobalRemoved(global uint32, h Handler) {
293+ if s := c.seat; s != nil && s.proxy != nil && s.name == global {
294+ s.release(h)
295+ }
296+}
297+
298+// release drops the devices and the seat proxy.
299+func (s *Seat) release(h Handler) {
300+ s.dropPointer(h)
301+ s.dropKeyboard(h)
302+ if s.ver >= 5 {
303+ _ = s.proxy.Release()
304+ } else {
305+ s.c.wlctx.Unregister(s.proxy)
306+ }
307+ s.proxy = nil
308+}
309+
310+func releaseDevice(ver uint32, c *wl.Context, p interface {
311+ wl.Proxy
312+ Release() error
313+}) {
314+ if ver >= 3 {
315+ _ = p.Release()
316+ } else {
317+ c.Unregister(p)
318+ }
319+}
320+
321+func (s *Seat) dropPointer(h Handler) {
322+ if s.pointer == nil {
323+ return
324+ }
325+ if s.shapeDev != nil {
326+ _ = s.shapeDev.Destroy()
327+ s.shapeDev = nil
328+ }
329+ s.enterSerial, s.shape = 0, 0
330+ releaseDevice(s.ver, s.c.wlctx, s.pointer)
331+ s.pointer = nil
332+ s.ptrGen++
333+ if id := s.ptrSurf; id != 0 {
334+ s.ptrSurf = 0
335+ pe := &s.pev
336+ *pe = PointerEvent{Surface: id, Kind: PointerLeave, X: s.px, Y: s.py}
337+ h.Pointer(pe)
338+ }
339+}
340+
341+func (s *Seat) dropKeyboard(h Handler) {
342+ if s.keyboard == nil {
343+ return
344+ }
345+ releaseDevice(s.ver, s.c.wlctx, s.keyboard)
346+ s.keyboard = nil
347+ s.kbGen++
348+ if id := s.kbSurf; id != 0 {
349+ s.kbSurf = 0
350+ s.kb.setFocus(false)
351+ h.KeyboardFocus(id, false)
352+ } else if s.kb != nil {
353+ s.kb.setFocus(false)
354+ }
355+}
356+
357+func (s *Seat) newPointer() error {
358+ c := s.c
359+ c.pause() // handlers go on after the request, before any event is read
360+ defer c.resume()
361+ p, err := s.proxy.GetPointer()
362+ if err != nil {
363+ return err
364+ }
365+ s.pointer = p
366+ s.ptrGen++
367+ gen, q := s.ptrGen, c.q
368+ post := func(e *event) {
369+ e.version = gen
370+ q.post(e)
371+ }
372+ p.OnEnter(func(serial, surf uint32, x, y wl.Fixed) {
373+ post(&event{kind: evPtrEnter, global: surf, serial: serial, a: int32(x), b: int32(y)})
374+ })
375+ p.OnLeave(func(_, surf uint32) { post(&event{kind: evPtrLeave, global: surf}) })
376+ p.OnMotion(func(_ uint32, x, y wl.Fixed) { post(&event{kind: evPtrMotion, a: int32(x), b: int32(y)}) })
377+ p.OnButton(func(serial, _, button, state uint32) {
378+ post(&event{kind: evPtrButton, serial: serial, flags: button, a: int32(state)})
379+ })
380+ p.OnAxis(func(_, axis uint32, v wl.Fixed) { post(&event{kind: evPtrAxis, flags: axis, a: int32(v)}) })
381+ p.OnAxisValue120(func(axis uint32, v int32) { post(&event{kind: evPtrAxisValue, flags: axis, a: v}) })
382+ p.OnAxisStop(func(_, axis uint32) { post(&event{kind: evPtrAxisStop, flags: axis}) })
383+ return nil
384+}
385+
386+func (s *Seat) newKeyboard() error {
387+ c := s.c
388+ if s.kb == nil {
389+ kb, err := newKeyboard(composeLocale())
390+ if err != nil {
391+ return err
392+ }
393+ if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {
394+ kb.close()
395+ return err
396+ }
397+ kb.secret = s.secret
398+ s.kb = kb
399+ }
400+ c.pause()
401+ defer c.resume()
402+ k, err := s.proxy.GetKeyboard()
403+ if err != nil {
404+ return err
405+ }
406+ s.keyboard = k
407+ s.kbGen++
408+ gen, q := s.kbGen, c.q
409+ post := func(e *event) {
410+ e.version = gen
411+ q.post(e)
412+ }
413+ k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
414+ n, err := fd.Take()
415+ if err != nil {
416+ post(&event{kind: evSeatBad, a: badKeymapFD})
417+ return
418+ }
419+ post(&event{kind: evKbKeymap, fd: int32(n), flags: size, a: int32(format)})
420+ })
421+ k.OnEnter(func(serial, surf uint32, _ []byte) { post(&event{kind: evKbEnter, global: surf, serial: serial}) })
422+ k.OnLeave(func(_, surf uint32) { post(&event{kind: evKbLeave, global: surf}) })
423+ k.OnKey(func(serial, _, key, state uint32) {
424+ post(&event{kind: evKbKey, serial: serial, flags: key, a: int32(state)})
425+ })
426+ k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
427+ post(&event{kind: evKbModifiers, flags: depressed, dev: uint64(latched) | uint64(locked)<<32, a: int32(group)})
428+ })
429+ k.OnRepeatInfo(func(rate, delay int32) { post(&event{kind: evKbRepeat, a: rate, b: delay}) })
430+ return nil
431+}
432+
433+// Codes of evSeatBad.
434+const badKeymapFD int32 = 100
435+
436+// applySeat handles the seat events on the owner.
437+func (c *Conn) applySeat(ev *event, h Handler) {
438+ defer ev.closeFD()
439+ s := c.seat
440+ if s == nil {
441+ return
442+ }
443+ switch ev.kind {
444+ case evSeatBad:
445+ h.Error(errors.New("neferclient: keymap descriptor unavailable"))
446+ case evSeatCaps:
447+ s.applyCaps(uint32(ev.a), h)
448+ case evPtrEnter, evPtrLeave, evPtrMotion, evPtrButton, evPtrAxis, evPtrAxisValue, evPtrAxisStop:
449+ if s.pointer != nil && ev.version == s.ptrGen {
450+ s.applyPointer(ev, h)
451+ }
452+ default:
453+ if s.keyboard != nil && ev.version == s.kbGen {
454+ s.applyKeyboard(ev, h)
455+ }
456+ }
457+}
458+
459+func (s *Seat) applyCaps(caps uint32, h Handler) {
460+ if s.proxy == nil {
461+ return
462+ }
463+ if caps&1 == 0 {
464+ s.dropPointer(h)
465+ }
466+ if caps&2 == 0 {
467+ s.dropKeyboard(h)
468+ }
469+ if caps&1 != 0 && s.pointer == nil {
470+ if err := s.newPointer(); err != nil {
471+ h.Error(err)
472+ }
473+ }
474+ if caps&2 != 0 && s.keyboard == nil {
475+ if err := s.newKeyboard(); err != nil {
476+ h.Error(err)
477+ }
478+ }
479+}
480+
481+func (s *Seat) applyPointer(ev *event, h Handler) {
482+ pe := &s.pev
483+ *pe = PointerEvent{}
484+ fixed := func(v int32) float64 { return wl.Fixed(v).Float64() }
485+ switch ev.kind {
486+ case evPtrEnter:
487+ sf := s.c.surfaceByObject(ev.global)
488+ if sf == nil {
489+ return
490+ }
491+ s.ptrSurf = sf.id
492+ s.px, s.py = fixed(ev.a), fixed(ev.b)
493+ s.enterSerial, s.shape = ev.serial, 0 // the compositor forgets the cursor on leave
494+ pe.Kind = PointerEnter
495+ case evPtrLeave:
496+ if sf := s.c.surfaceByObject(ev.global); s.ptrSurf == 0 || sf == nil || sf.id != s.ptrSurf {
497+ return
498+ }
499+ pe.Kind = PointerLeave
500+ pe.Surface = s.ptrSurf
501+ pe.X, pe.Y = s.px, s.py
502+ s.ptrSurf = 0
503+ s.enterSerial, s.shape = 0, 0
504+ h.Pointer(pe)
505+ return
506+ default:
507+ if s.ptrSurf == 0 {
508+ return
509+ }
510+ switch ev.kind {
511+ case evPtrMotion:
512+ s.px, s.py = fixed(ev.a), fixed(ev.b)
513+ pe.Kind = PointerMotion
514+ case evPtrButton:
515+ pe.Kind, pe.Button, pe.Pressed = PointerButton, ev.flags, ev.a == 1
516+ case evPtrAxis:
517+ pe.Kind, pe.Axis = PointerAxis, ev.flags
518+ if ev.flags == AxisVertical {
519+ pe.DY = fixed(ev.a)
520+ } else {
521+ pe.DX = fixed(ev.a)
522+ }
523+ case evPtrAxisValue:
524+ pe.Kind, pe.Axis, pe.Value120 = PointerAxis, ev.flags, ev.a
525+ case evPtrAxisStop:
526+ pe.Kind, pe.Axis = PointerAxisStop, ev.flags
527+ }
528+ }
529+ pe.Surface, pe.X, pe.Y = s.ptrSurf, s.px, s.py
530+ h.Pointer(pe)
531+}
532+
533+func (s *Seat) applyKeyboard(ev *event, h Handler) {
534+ kb := s.kb
535+ switch ev.kind {
536+ case evKbKeymap:
537+ fd := int(ev.fd)
538+ ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again
539+ if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {
540+ _ = unix.Close(fd)
541+ h.Error(errors.New("neferclient: unsupported keymap format or size"))
542+ return
543+ }
544+ if err := kb.replaceFD(fd, int(ev.flags)); err != nil {
545+ h.Error(fmt.Errorf("neferclient: keymap: %w", err))
546+ }
547+ case evKbEnter:
548+ sf := s.c.surfaceByObject(ev.global)
549+ if sf == nil {
550+ return
551+ }
552+ s.kbSurf = sf.id
553+ kb.setFocus(true)
554+ h.KeyboardFocus(sf.id, true)
555+ case evKbLeave:
556+ sf := s.c.surfaceByObject(ev.global)
557+ if sf == nil || s.kbSurf != sf.id {
558+ return
559+ }
560+ s.kbSurf = 0
561+ kb.setFocus(false)
562+ h.KeyboardFocus(sf.id, false)
563+ case evKbKey:
564+ ke := &s.kev
565+ *ke = KeyEvent{Surface: s.kbSurf}
566+ if ev.a == 1 {
567+ ok, err := kb.press(ev.flags, ke)
568+ switch {
569+ case err != nil:
570+ h.Error(err)
571+ case ok:
572+ s.deliverKey(ke, h)
573+ }
574+ } else if kb.release(ev.flags, ke) {
575+ s.deliverKey(ke, h)
576+ }
577+ case evKbModifiers:
578+ if err := kb.setMask(ev.flags, uint32(ev.dev), uint32(ev.dev>>32), uint32(ev.a)); err != nil {
579+ h.Error(err)
580+ }
581+ case evKbRepeat:
582+ kb.setRepeat(ev.a, ev.b)
583+ }
584+}
585+
586+func (c *Conn) closeSeat() {
587+ if s := c.seat; s != nil && s.kb != nil {
588+ s.kb.close()
589+ s.kb = nil
590+ }
591+}
@@ -0,0 +1,591 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+
7+ "github.com/bnema/go-wayland-bindings/client/cursorshape"
8+ "github.com/bnema/go-wayland-bindings/client/wayland"
9+ "github.com/bnema/wlturbo/wl"
10+ "golang.org/x/sys/unix"
11+)
12+
13+// maxSeatVersion is the wl_seat version bound: 9 adds axis_value120 users and
14+// the release requests.
15+const maxSeatVersion = 9
16+
17+// CursorShape is a wp_cursor_shape_v1 shape. The values are the protocol's.
18+type CursorShape uint32
19+
20+const (
21+ CursorDefault CursorShape = iota + 1
22+ CursorContextMenu
23+ CursorHelp
24+ CursorPointer
25+ CursorProgress
26+ CursorWait
27+ CursorCell
28+ CursorCrosshair
29+ CursorText
30+ CursorVerticalText
31+ CursorAlias
32+ CursorCopy
33+ CursorMove
34+ CursorNoDrop
35+ CursorNotAllowed
36+ CursorGrab
37+ CursorGrabbing
38+ CursorEResize
39+ CursorNResize
40+ CursorNEResize
41+ CursorNWResize
42+ CursorSResize
43+ CursorSEResize
44+ CursorSWResize
45+ CursorWResize
46+ CursorEWResize
47+ CursorNSResize
48+ CursorNESWResize
49+ CursorNWSEResize
50+ CursorColResize
51+ CursorRowResize
52+ CursorAllScroll
53+ CursorZoomIn
54+ CursorZoomOut
55+ CursorDndAsk // needs cursor-shape version 2
56+ CursorAllResize // needs cursor-shape version 2
57+)
58+
59+// PointerKind tells which pointer event a [PointerEvent] is.
60+type PointerKind uint8
61+
62+const (
63+ PointerEnter PointerKind = iota + 1
64+ PointerLeave
65+ PointerMotion
66+ PointerButton
67+ // PointerAxis is a scroll step on Axis. DX or DY carry the continuous
68+ // value in logical pixels; Value120 carries the wheel value in 1/120 of a
69+ // notch. A compositor can send both for one physical step: use one.
70+ PointerAxis
71+ // PointerAxisStop ends a scroll sequence on Axis.
72+ PointerAxisStop
73+)
74+
75+// Scroll axes of [PointerEvent.Axis].
76+const (
77+ AxisVertical = 0
78+ AxisHorizontal = 1
79+)
80+
81+// PointerEvent is one pointer event over a surface of the connection. X and Y
82+// are surface-local logical pixels (already logical: never divide by scale)
83+// and hold the latest position for every kind.
84+type PointerEvent struct {
85+ Surface SurfaceID
86+ Kind PointerKind
87+ X, Y float64
88+ DX, DY float64 // PointerAxis: continuous scroll value
89+ Button uint32 // PointerButton: evdev button code
90+ Pressed bool // PointerButton
91+ Axis uint32 // PointerAxis, PointerAxisStop: AxisVertical or AxisHorizontal
92+ Value120 int32 // PointerAxis: wheel value in 1/120 notches
93+}
94+
95+// KeyEvent is one key press, repeat or release.
96+type KeyEvent struct {
97+ Surface SurfaceID
98+ Keycode uint32 // evdev key code
99+ Keysym uint32 // xkb keysym in the current layout and modifier state
100+ Modifiers Modifiers
101+ Group uint32 // effective layout index
102+ Pressed bool
103+ Repeat bool
104+ // 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
106+ // returns. It is always nil while a [SecretBuffer] is installed.
107+ Text []byte
108+}
109+
110+// 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
112+// no-op.
113+type Seat struct {
114+ c *Conn
115+ proxy *wayland.Seat
116+ name uint32 // registry name of the global
117+ ver uint32
118+
119+ pointer *wayland.Pointer
120+ ptrGen uint32 // generation of pointer; bumped on every create and drop
121+ ptrSurf SurfaceID
122+ px, py float64
123+ pev PointerEvent
124+
125+ keyboard *wayland.Keyboard
126+ kbGen uint32
127+ kbSurf SurfaceID
128+ kb *keyboard
129+ kev KeyEvent
130+
131+ secret *SecretBuffer
132+
133+ shapes *cursorshape.WpCursorShapeManager
134+ shapesVer uint32
135+ shapeDev *cursorshape.WpCursorShapeDevice
136+ enterSerial uint32 // latest pointer enter serial, 0 while outside
137+ shape CursorShape
138+}
139+
140+// Seat returns the connection's seat, binding wl_seat on first use. The seat
141+// is also bound when the first surface is created. Input starts flowing to the
142+// Handler once the compositor announces the capabilities.
143+func (c *Conn) Seat() *Seat {
144+ c.ensureSeat()
145+ return c.seat
146+}
147+
148+// ensureSeat binds wl_seat and the optional cursor-shape manager once. A
149+// failure is reported by the next Dispatch through Handler.Error.
150+func (c *Conn) ensureSeat() {
151+ if c.seat != nil || c.closed {
152+ return
153+ }
154+ s := &Seat{c: c}
155+ c.seat = s
156+ g, ok := c.display.Registry().FindGlobal(wayland.SeatInterface)
157+ if !ok {
158+ return
159+ }
160+ q := c.q
161+ proxy, v, err := c.Bind(wayland.SeatInterface, maxSeatVersion, func(ctx *wl.Context) *wayland.Seat {
162+ p := wayland.NewSeat(ctx)
163+ p.OnCapabilities(func(caps uint32) {
164+ e := event{kind: evSeatCaps, a: int32(caps)}
165+ q.post(&e)
166+ })
167+ return p
168+ })
169+ if err != nil {
170+ c.seatErr = fmt.Errorf("neferclient: bind wl_seat: %w", err)
171+ return
172+ }
173+ s.proxy, s.name, s.ver = proxy, g.Name, v
174+ m, mv, err := c.Bind(cursorshape.WpCursorShapeManagerInterface, 2, cursorshape.NewWpCursorShapeManager)
175+ if err != nil {
176+ if !errors.Is(err, ErrGlobalNotFound) {
177+ c.seatErr = fmt.Errorf("neferclient: bind cursor shape manager: %w", err)
178+ }
179+ return
180+ }
181+ s.shapes, s.shapesVer = m, mv
182+}
183+
184+// 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;
186+// 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.
189+func (s *Seat) SetSecret(b *SecretBuffer) {
190+ s.secret = b
191+ if s.kb != nil {
192+ s.kb.setSecret(b)
193+ }
194+}
195+
196+// SetCursor asks the compositor to show shape for the pointer. It is a no-op
197+// when the compositor has no cursor-shape support, the pointer is outside the
198+// connection's surfaces, or shape is unchanged since the last enter.
199+func (s *Seat) SetCursor(shape CursorShape) error {
200+ if s.c.closed {
201+ return ErrClosed
202+ }
203+ limit := CursorZoomOut
204+ if s.shapesVer >= 2 {
205+ limit = CursorAllResize
206+ }
207+ if shape < CursorDefault || shape > CursorAllResize {
208+ return fmt.Errorf("neferclient: invalid cursor shape %d", shape)
209+ }
210+ if s.shapes == nil || s.pointer == nil || s.enterSerial == 0 || shape == s.shape {
211+ return nil
212+ }
213+ if shape > limit {
214+ return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}
215+ }
216+ if s.shapeDev == nil {
217+ dev, err := s.shapes.GetPointer(s.pointer)
218+ if err != nil {
219+ return err
220+ }
221+ s.shapeDev = dev
222+ }
223+ if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {
224+ return err
225+ }
226+ s.shape = shape
227+ return nil
228+}
229+
230+// surfaceByObject finds the surface whose wl_surface has object id obj.
231+func (c *Conn) surfaceByObject(obj uint32) *Surface {
232+ for _, s := range c.surfaces {
233+ if !s.closed && s.surf != nil && s.surf.ID() == obj {
234+ return s
235+ }
236+ }
237+ return nil
238+}
239+
240+func (c *Conn) seatTimer(fd int32) bool {
241+ return c.seat != nil && c.seat.kb != nil && int(fd) == c.seat.kb.tfd
242+}
243+
244+// seatRepeat handles the repeat timer: at most one repeat per expiration.
245+func (c *Conn) seatRepeat(h Handler) {
246+ s := c.seat
247+ if s.kb.expired() {
248+ ke := &s.kev
249+ *ke = KeyEvent{Surface: s.kbSurf}
250+ ok, err := s.kb.repeat(ke)
251+ switch {
252+ case err != nil:
253+ h.Error(err)
254+ case ok:
255+ s.deliverKey(ke, h)
256+ }
257+ }
258+ if !c.closed {
259+ if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {
260+ h.Error(err)
261+ }
262+ }
263+}
264+
265+func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {
266+ h.Key(ke)
267+ clear(s.kb.scratch[:]) // ordinary text must not outlive the call
268+ ke.Text = nil
269+ if s.kb.changed {
270+ h.SecretChanged(s.secret.Len())
271+ }
272+}
273+
274+// seatSurfaceGone forgets focus on a closing surface.
275+func (c *Conn) seatSurfaceGone(sf *Surface) {
276+ s := c.seat
277+ if s == nil {
278+ return
279+ }
280+ if s.ptrSurf == sf.id {
281+ s.ptrSurf = 0
282+ s.enterSerial, s.shape = 0, 0
283+ }
284+ if s.kbSurf == sf.id {
285+ s.kbSurf = 0
286+ if s.kb != nil {
287+ s.kb.setFocus(false)
288+ }
289+ }
290+}
291+
292+func (c *Conn) seatGlobalRemoved(global uint32, h Handler) {
293+ if s := c.seat; s != nil && s.proxy != nil && s.name == global {
294+ s.release(h)
295+ }
296+}
297+
298+// release drops the devices and the seat proxy.
299+func (s *Seat) release(h Handler) {
300+ s.dropPointer(h)
301+ s.dropKeyboard(h)
302+ if s.ver >= 5 {
303+ _ = s.proxy.Release()
304+ } else {
305+ s.c.wlctx.Unregister(s.proxy)
306+ }
307+ s.proxy = nil
308+}
309+
310+func releaseDevice(ver uint32, c *wl.Context, p interface {
311+ wl.Proxy
312+ Release() error
313+}) {
314+ if ver >= 3 {
315+ _ = p.Release()
316+ } else {
317+ c.Unregister(p)
318+ }
319+}
320+
321+func (s *Seat) dropPointer(h Handler) {
322+ if s.pointer == nil {
323+ return
324+ }
325+ if s.shapeDev != nil {
326+ _ = s.shapeDev.Destroy()
327+ s.shapeDev = nil
328+ }
329+ s.enterSerial, s.shape = 0, 0
330+ releaseDevice(s.ver, s.c.wlctx, s.pointer)
331+ s.pointer = nil
332+ s.ptrGen++
333+ if id := s.ptrSurf; id != 0 {
334+ s.ptrSurf = 0
335+ pe := &s.pev
336+ *pe = PointerEvent{Surface: id, Kind: PointerLeave, X: s.px, Y: s.py}
337+ h.Pointer(pe)
338+ }
339+}
340+
341+func (s *Seat) dropKeyboard(h Handler) {
342+ if s.keyboard == nil {
343+ return
344+ }
345+ releaseDevice(s.ver, s.c.wlctx, s.keyboard)
346+ s.keyboard = nil
347+ s.kbGen++
348+ if id := s.kbSurf; id != 0 {
349+ s.kbSurf = 0
350+ s.kb.setFocus(false)
351+ h.KeyboardFocus(id, false)
352+ } else if s.kb != nil {
353+ s.kb.setFocus(false)
354+ }
355+}
356+
357+func (s *Seat) newPointer() error {
358+ c := s.c
359+ c.pause() // handlers go on after the request, before any event is read
360+ defer c.resume()
361+ p, err := s.proxy.GetPointer()
362+ if err != nil {
363+ return err
364+ }
365+ s.pointer = p
366+ s.ptrGen++
367+ gen, q := s.ptrGen, c.q
368+ post := func(e *event) {
369+ e.version = gen
370+ q.post(e)
371+ }
372+ p.OnEnter(func(serial, surf uint32, x, y wl.Fixed) {
373+ post(&event{kind: evPtrEnter, global: surf, serial: serial, a: int32(x), b: int32(y)})
374+ })
375+ p.OnLeave(func(_, surf uint32) { post(&event{kind: evPtrLeave, global: surf}) })
376+ p.OnMotion(func(_ uint32, x, y wl.Fixed) { post(&event{kind: evPtrMotion, a: int32(x), b: int32(y)}) })
377+ p.OnButton(func(serial, _, button, state uint32) {
378+ post(&event{kind: evPtrButton, serial: serial, flags: button, a: int32(state)})
379+ })
380+ p.OnAxis(func(_, axis uint32, v wl.Fixed) { post(&event{kind: evPtrAxis, flags: axis, a: int32(v)}) })
381+ p.OnAxisValue120(func(axis uint32, v int32) { post(&event{kind: evPtrAxisValue, flags: axis, a: v}) })
382+ p.OnAxisStop(func(_, axis uint32) { post(&event{kind: evPtrAxisStop, flags: axis}) })
383+ return nil
384+}
385+
386+func (s *Seat) newKeyboard() error {
387+ c := s.c
388+ if s.kb == nil {
389+ kb, err := newKeyboard(composeLocale())
390+ if err != nil {
391+ return err
392+ }
393+ if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {
394+ kb.close()
395+ return err
396+ }
397+ kb.secret = s.secret
398+ s.kb = kb
399+ }
400+ c.pause()
401+ defer c.resume()
402+ k, err := s.proxy.GetKeyboard()
403+ if err != nil {
404+ return err
405+ }
406+ s.keyboard = k
407+ s.kbGen++
408+ gen, q := s.kbGen, c.q
409+ post := func(e *event) {
410+ e.version = gen
411+ q.post(e)
412+ }
413+ k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
414+ n, err := fd.Take()
415+ if err != nil {
416+ post(&event{kind: evSeatBad, a: badKeymapFD})
417+ return
418+ }
419+ post(&event{kind: evKbKeymap, fd: int32(n), flags: size, a: int32(format)})
420+ })
421+ k.OnEnter(func(serial, surf uint32, _ []byte) { post(&event{kind: evKbEnter, global: surf, serial: serial}) })
422+ k.OnLeave(func(_, surf uint32) { post(&event{kind: evKbLeave, global: surf}) })
423+ k.OnKey(func(serial, _, key, state uint32) {
424+ post(&event{kind: evKbKey, serial: serial, flags: key, a: int32(state)})
425+ })
426+ k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
427+ post(&event{kind: evKbModifiers, flags: depressed, dev: uint64(latched) | uint64(locked)<<32, a: int32(group)})
428+ })
429+ k.OnRepeatInfo(func(rate, delay int32) { post(&event{kind: evKbRepeat, a: rate, b: delay}) })
430+ return nil
431+}
432+
433+// Codes of evSeatBad.
434+const badKeymapFD int32 = 100
435+
436+// applySeat handles the seat events on the owner.
437+func (c *Conn) applySeat(ev *event, h Handler) {
438+ defer ev.closeFD()
439+ s := c.seat
440+ if s == nil {
441+ return
442+ }
443+ switch ev.kind {
444+ case evSeatBad:
445+ h.Error(errors.New("neferclient: keymap descriptor unavailable"))
446+ case evSeatCaps:
447+ s.applyCaps(uint32(ev.a), h)
448+ case evPtrEnter, evPtrLeave, evPtrMotion, evPtrButton, evPtrAxis, evPtrAxisValue, evPtrAxisStop:
449+ if s.pointer != nil && ev.version == s.ptrGen {
450+ s.applyPointer(ev, h)
451+ }
452+ default:
453+ if s.keyboard != nil && ev.version == s.kbGen {
454+ s.applyKeyboard(ev, h)
455+ }
456+ }
457+}
458+
459+func (s *Seat) applyCaps(caps uint32, h Handler) {
460+ if s.proxy == nil {
461+ return
462+ }
463+ if caps&1 == 0 {
464+ s.dropPointer(h)
465+ }
466+ if caps&2 == 0 {
467+ s.dropKeyboard(h)
468+ }
469+ if caps&1 != 0 && s.pointer == nil {
470+ if err := s.newPointer(); err != nil {
471+ h.Error(err)
472+ }
473+ }
474+ if caps&2 != 0 && s.keyboard == nil {
475+ if err := s.newKeyboard(); err != nil {
476+ h.Error(err)
477+ }
478+ }
479+}
480+
481+func (s *Seat) applyPointer(ev *event, h Handler) {
482+ pe := &s.pev
483+ *pe = PointerEvent{}
484+ fixed := func(v int32) float64 { return wl.Fixed(v).Float64() }
485+ switch ev.kind {
486+ case evPtrEnter:
487+ sf := s.c.surfaceByObject(ev.global)
488+ if sf == nil {
489+ return
490+ }
491+ s.ptrSurf = sf.id
492+ s.px, s.py = fixed(ev.a), fixed(ev.b)
493+ s.enterSerial, s.shape = ev.serial, 0 // the compositor forgets the cursor on leave
494+ pe.Kind = PointerEnter
495+ case evPtrLeave:
496+ if sf := s.c.surfaceByObject(ev.global); s.ptrSurf == 0 || sf == nil || sf.id != s.ptrSurf {
497+ return
498+ }
499+ pe.Kind = PointerLeave
500+ pe.Surface = s.ptrSurf
501+ pe.X, pe.Y = s.px, s.py
502+ s.ptrSurf = 0
503+ s.enterSerial, s.shape = 0, 0
504+ h.Pointer(pe)
505+ return
506+ default:
507+ if s.ptrSurf == 0 {
508+ return
509+ }
510+ switch ev.kind {
511+ case evPtrMotion:
512+ s.px, s.py = fixed(ev.a), fixed(ev.b)
513+ pe.Kind = PointerMotion
514+ case evPtrButton:
515+ pe.Kind, pe.Button, pe.Pressed = PointerButton, ev.flags, ev.a == 1
516+ case evPtrAxis:
517+ pe.Kind, pe.Axis = PointerAxis, ev.flags
518+ if ev.flags == AxisVertical {
519+ pe.DY = fixed(ev.a)
520+ } else {
521+ pe.DX = fixed(ev.a)
522+ }
523+ case evPtrAxisValue:
524+ pe.Kind, pe.Axis, pe.Value120 = PointerAxis, ev.flags, ev.a
525+ case evPtrAxisStop:
526+ pe.Kind, pe.Axis = PointerAxisStop, ev.flags
527+ }
528+ }
529+ pe.Surface, pe.X, pe.Y = s.ptrSurf, s.px, s.py
530+ h.Pointer(pe)
531+}
532+
533+func (s *Seat) applyKeyboard(ev *event, h Handler) {
534+ kb := s.kb
535+ switch ev.kind {
536+ case evKbKeymap:
537+ fd := int(ev.fd)
538+ ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again
539+ if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {
540+ _ = unix.Close(fd)
541+ h.Error(errors.New("neferclient: unsupported keymap format or size"))
542+ return
543+ }
544+ if err := kb.replaceFD(fd, int(ev.flags)); err != nil {
545+ h.Error(fmt.Errorf("neferclient: keymap: %w", err))
546+ }
547+ case evKbEnter:
548+ sf := s.c.surfaceByObject(ev.global)
549+ if sf == nil {
550+ return
551+ }
552+ s.kbSurf = sf.id
553+ kb.setFocus(true)
554+ h.KeyboardFocus(sf.id, true)
555+ case evKbLeave:
556+ sf := s.c.surfaceByObject(ev.global)
557+ if sf == nil || s.kbSurf != sf.id {
558+ return
559+ }
560+ s.kbSurf = 0
561+ kb.setFocus(false)
562+ h.KeyboardFocus(sf.id, false)
563+ case evKbKey:
564+ ke := &s.kev
565+ *ke = KeyEvent{Surface: s.kbSurf}
566+ if ev.a == 1 {
567+ ok, err := kb.press(ev.flags, ke)
568+ switch {
569+ case err != nil:
570+ h.Error(err)
571+ case ok:
572+ s.deliverKey(ke, h)
573+ }
574+ } else if kb.release(ev.flags, ke) {
575+ s.deliverKey(ke, h)
576+ }
577+ case evKbModifiers:
578+ if err := kb.setMask(ev.flags, uint32(ev.dev), uint32(ev.dev>>32), uint32(ev.a)); err != nil {
579+ h.Error(err)
580+ }
581+ case evKbRepeat:
582+ kb.setRepeat(ev.a, ev.b)
583+ }
584+}
585+
586+func (c *Conn) closeSeat() {
587+ if s := c.seat; s != nil && s.kb != nil {
588+ s.kb.close()
589+ s.kb = nil
590+ }
591+}
M surface.go +2 -0
@@ -236,6 +236,7 @@ func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) {
236236 if err := c.ensureCore(); err != nil {
237237 return nil, err
238238 }
239+ c.ensureSeat() // optional: input flows to the Handler once the seat is bound
239240 g := &c.g
240241 c.nextSurf++
241242 s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1,
@@ -728,6 +729,7 @@ func (s *Surface) destroy() error {
728729 // server-side delete_id handling in wlturbo (see TestCloseWithPendingFrame).
729730 s.closed = true
730731 delete(s.c.surfaces, s.id)
732+ s.c.seatSurfaceGone(s)
731733 var errs []error
732734 add := func(err error) {
733735 if err != nil {
@@ -236,6 +236,7 @@ func (c *Conn) newSurface(role surfaceRole, w, h int32) (*Surface, error) {
236 if err := c.ensureCore(); err != nil {236 if err := c.ensureCore(); err != nil {
237 return nil, err237 return nil, err
238 }238 }
239+ c.ensureSeat() // optional: input flows to the Handler once the seat is bound
239 g := &c.g240 g := &c.g
240 c.nextSurf++241 c.nextSurf++
241 s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1,242 s := &Surface{c: c, id: c.nextSurf, role: role, width: w, height: h, scale: 1, sentBufScale: 1,
@@ -728,6 +729,7 @@ func (s *Surface) destroy() error {
728 // server-side delete_id handling in wlturbo (see TestCloseWithPendingFrame).729 // server-side delete_id handling in wlturbo (see TestCloseWithPendingFrame).
729 s.closed = true730 s.closed = true
730 delete(s.c.surfaces, s.id)731 delete(s.c.surfaces, s.id)
732+ s.c.seatSurfaceGone(s)
731 var errs []error733 var errs []error
732 add := func(err error) {734 add := func(err error) {
733 if err != nil {735 if err != nil {
A testdata/fr.xkb +2084 -0 large diff (2084 lines changed): collapsed

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