detached 3e34ac2

Merge branch 'nc/seat': seat, keyboard and secret input

commit
3e34ac28c7389bfce821f3235c45e696bfaf2b12
author
brice · · 2026-10-01 18:58 UTC
merge of
e904307 + bfb1f6d · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
20 files · +4699 -4
M Makefile +4 -2
@@ -1,7 +1,9 @@
1-.PHONY: build test vet race mocks mocks-check fakes-check perf-check check
1+.PHONY: build clean-build test vet race mocks mocks-check fakes-check perf-check check
22
33 build:
44 CGO_ENABLED=0 go build ./...
5+clean-build:
6+ GOWORK=off CGO_ENABLED=0 go build ./...
57 test:
68 CGO_ENABLED=0 go test ./...
79 vet:
@@ -18,5 +20,5 @@ fakes-check:
1820 [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && printf '%s\n' 'handwritten test double: generate with Mockery v3' >&2; exit 1; }
1921 perf-check:
2022 CGO_ENABLED=0 go test -run 'TestAlloc' -count=1 ./...
21-check: build vet test fakes-check perf-check
23+check: build clean-build vet test fakes-check perf-check
2224 CGO_ENABLED=0 staticcheck ./...
@@ -1,7 +1,9 @@
1-.PHONY: build test vet race mocks mocks-check fakes-check perf-check check1+.PHONY: build clean-build test vet race mocks mocks-check fakes-check perf-check check
2 2
3 build:3 build:
4 CGO_ENABLED=0 go build ./...4 CGO_ENABLED=0 go build ./...
5+clean-build:
6+ GOWORK=off CGO_ENABLED=0 go build ./...
5 test:7 test:
6 CGO_ENABLED=0 go test ./...8 CGO_ENABLED=0 go test ./...
7 vet:9 vet:
@@ -18,5 +20,5 @@ fakes-check:
18 [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && printf '%s\n' 'handwritten test double: generate with Mockery v3' >&2; exit 1; }20 [ $$rc -eq 1 ] || { [ $$rc -eq 0 ] && printf '%s\n' 'handwritten test double: generate with Mockery v3' >&2; exit 1; }
19 perf-check:21 perf-check:
20 CGO_ENABLED=0 go test -run 'TestAlloc' -count=1 ./...22 CGO_ENABLED=0 go test -run 'TestAlloc' -count=1 ./...
21-check: build vet test fakes-check perf-check23+check: build clean-build vet test fakes-check perf-check
22 CGO_ENABLED=0 staticcheck ./...24 CGO_ENABLED=0 staticcheck ./...
M alloc_test.go +13 -0
@@ -17,12 +17,25 @@ import (
1717 type countingHandler struct {
1818 neferclient.NopHandler
1919 fdReady, errs, frames int
20+ keys, pointers int
21+ lastText string // set only by the allocation-free comparison below
2022 }
2123
2224 func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
2325 func (h *countingHandler) Error(error) { h.errs++ }
2426 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
2527
28+func (h *countingHandler) Pointer(*neferclient.PointerEvent) { h.pointers++ }
29+
30+// Key records the text through a comparison that does not allocate: the
31+// string is only built the first time a different text shows up.
32+func (h *countingHandler) Key(ev *neferclient.KeyEvent) {
33+ h.keys++
34+ if string(ev.Text) != h.lastText && ev.Pressed {
35+ h.lastText = string(ev.Text)
36+ }
37+}
38+
2639 // TestAllocDispatch measures the whole steady-state pipeline over a real
2740 // socketpair: wire frames in, reader decode, queue, Dispatch (output state
2841 // updates, an output commit and a watched-fd notification with epoll re-arm). testing's
@@ -17,12 +17,25 @@ import (
17 type countingHandler struct {17 type countingHandler struct {
18 neferclient.NopHandler18 neferclient.NopHandler
19 fdReady, errs, frames int19 fdReady, errs, frames int
20+ keys, pointers int
21+ lastText string // set only by the allocation-free comparison below
20 }22 }
21 23
22 func (h *countingHandler) FDReady(uint64) { h.fdReady++ }24 func (h *countingHandler) FDReady(uint64) { h.fdReady++ }
23 func (h *countingHandler) Error(error) { h.errs++ }25 func (h *countingHandler) Error(error) { h.errs++ }
24 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }26 func (h *countingHandler) Frame(neferclient.SurfaceID) { h.frames++ }
25 27
28+func (h *countingHandler) Pointer(*neferclient.PointerEvent) { h.pointers++ }
29+
30+// Key records the text through a comparison that does not allocate: the
31+// string is only built the first time a different text shows up.
32+func (h *countingHandler) Key(ev *neferclient.KeyEvent) {
33+ h.keys++
34+ if string(ev.Text) != h.lastText && ev.Pressed {
35+ h.lastText = string(ev.Text)
36+ }
37+}
38+
26 // TestAllocDispatch measures the whole steady-state pipeline over a real39 // TestAllocDispatch measures the whole steady-state pipeline over a real
27 // socketpair: wire frames in, reader decode, queue, Dispatch (output state40 // socketpair: wire frames in, reader decode, queue, Dispatch (output state
28 // updates, an output commit and a watched-fd notification with epoll re-arm). testing's41 // updates, an output commit and a watched-fd notification with epoll re-arm). testing's
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 go.mod +2 -0
@@ -4,12 +4,14 @@ go 1.27
44
55 require (
66 github.com/bnema/go-wayland-bindings v0.1.0
7+ github.com/bnema/purego-xkbcommon v0.1.0
78 github.com/bnema/wlturbo v0.6.0
89 github.com/stretchr/testify v1.12.1
910 golang.org/x/sys v0.48.0
1011 )
1112
1213 require (
14+ github.com/bnema/purego v0.13.0-bnema.1 // indirect
1315 github.com/stretchr/objx v0.5.3 // indirect
1416 go.yaml.in/yaml/v3 v3.0.5 // indirect
1517 )
@@ -4,12 +4,14 @@ go 1.27
4 4
5 require (5 require (
6 github.com/bnema/go-wayland-bindings v0.1.06 github.com/bnema/go-wayland-bindings v0.1.0
7+ github.com/bnema/purego-xkbcommon v0.1.0
7 github.com/bnema/wlturbo v0.6.08 github.com/bnema/wlturbo v0.6.0
8 github.com/stretchr/testify v1.12.19 github.com/stretchr/testify v1.12.1
9 golang.org/x/sys v0.48.010 golang.org/x/sys v0.48.0
10 )11 )
11 12
12 require (13 require (
14+ github.com/bnema/purego v0.13.0-bnema.1 // indirect
13 github.com/stretchr/objx v0.5.3 // indirect15 github.com/stretchr/objx v0.5.3 // indirect
14 go.yaml.in/yaml/v3 v3.0.5 // indirect16 go.yaml.in/yaml/v3 v3.0.5 // indirect
15 )17 )
M go.sum +4 -0
@@ -1,5 +1,9 @@
11 github.com/bnema/go-wayland-bindings v0.1.0 h1:mi2vNa8BB5ZCo/hq/9d6Af01yhqY6QXAZ6pkljFI1xk=
22 github.com/bnema/go-wayland-bindings v0.1.0/go.mod h1:eWOgjp9vzZEqxm059q1G9F5NXplmv9UAmrDJiivxVjA=
3+github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=
4+github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=
5+github.com/bnema/purego-xkbcommon v0.1.0 h1:VHNf9shPpafKxdTERcwCNJKHhSd4IG2tjmLsDACKnuw=
6+github.com/bnema/purego-xkbcommon v0.1.0/go.mod h1:GE8JGmR/rqdklz96tFaFU7M5kI7jN1ma5mh+rz+XXrg=
37 github.com/bnema/wlturbo v0.6.0 h1:sEEf22j+PIrSV9NTlognbSJ1/CUoeQj/lwpUkE6D1nY=
48 github.com/bnema/wlturbo v0.6.0/go.mod h1:4i1k1wyw7FDWOkh2hg1ZwPfrPRn3+E6WrYg9Qrt74CA=
59 github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
@@ -1,5 +1,9 @@
1 github.com/bnema/go-wayland-bindings v0.1.0 h1:mi2vNa8BB5ZCo/hq/9d6Af01yhqY6QXAZ6pkljFI1xk=1 github.com/bnema/go-wayland-bindings v0.1.0 h1:mi2vNa8BB5ZCo/hq/9d6Af01yhqY6QXAZ6pkljFI1xk=
2 github.com/bnema/go-wayland-bindings v0.1.0/go.mod h1:eWOgjp9vzZEqxm059q1G9F5NXplmv9UAmrDJiivxVjA=2 github.com/bnema/go-wayland-bindings v0.1.0/go.mod h1:eWOgjp9vzZEqxm059q1G9F5NXplmv9UAmrDJiivxVjA=
3+github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=
4+github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=
5+github.com/bnema/purego-xkbcommon v0.1.0 h1:VHNf9shPpafKxdTERcwCNJKHhSd4IG2tjmLsDACKnuw=
6+github.com/bnema/purego-xkbcommon v0.1.0/go.mod h1:GE8JGmR/rqdklz96tFaFU7M5kI7jN1ma5mh+rz+XXrg=
3 github.com/bnema/wlturbo v0.6.0 h1:sEEf22j+PIrSV9NTlognbSJ1/CUoeQj/lwpUkE6D1nY=7 github.com/bnema/wlturbo v0.6.0 h1:sEEf22j+PIrSV9NTlognbSJ1/CUoeQj/lwpUkE6D1nY=
4 github.com/bnema/wlturbo v0.6.0/go.mod h1:4i1k1wyw7FDWOkh2hg1ZwPfrPRn3+E6WrYg9Qrt74CA=8 github.com/bnema/wlturbo v0.6.0/go.mod h1:4i1k1wyw7FDWOkh2hg1ZwPfrPRn3+E6WrYg9Qrt74CA=
5 github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=9 github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
M handler.go +26 -0
@@ -50,6 +50,27 @@ 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]. For
63+ // those keys (press, repeat and release) ev.Secret is set and Keycode and
64+ // Keysym are 0, so the event does not reveal the typed character; Enter,
65+ // Escape, BackSpace and Control combinations keep their keysym. ev is
66+ // valid only during the call.
67+ Key(ev *KeyEvent)
68+ // KeyboardFocus reports that a surface gained or lost keyboard focus.
69+ KeyboardFocus(id SurfaceID, focused bool)
70+ // SecretChanged reports that a key press, repeat or backspace changed the
71+ // installed [SecretBuffer]; count is its length in code points. It is
72+ // called after the [Handler.Key] call of that key.
73+ SecretChanged(count int)
5374 }
5475
5576 // NopHandler implements every Handler method as a no-op. Embed it in a handler
@@ -69,3 +90,8 @@ func (NopHandler) Closed(SurfaceID) {}
6990 func (NopHandler) FeedbackDone(SurfaceID) {}
7091 func (NopHandler) Locked() {}
7192 func (NopHandler) LockFinished() {}
93+
94+func (NopHandler) Pointer(*PointerEvent) {}
95+func (NopHandler) Key(*KeyEvent) {}
96+func (NopHandler) KeyboardFocus(SurfaceID, bool) {}
97+func (NopHandler) SecretChanged(int) {}
@@ -50,6 +50,27 @@ 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]. For
63+ // those keys (press, repeat and release) ev.Secret is set and Keycode and
64+ // Keysym are 0, so the event does not reveal the typed character; Enter,
65+ // Escape, BackSpace and Control combinations keep their keysym. ev is
66+ // valid only during the call.
67+ Key(ev *KeyEvent)
68+ // KeyboardFocus reports that a surface gained or lost keyboard focus.
69+ KeyboardFocus(id SurfaceID, focused bool)
70+ // SecretChanged reports that a key press, repeat or backspace changed the
71+ // installed [SecretBuffer]; count is its length in code points. It is
72+ // called after the [Handler.Key] call of that key.
73+ SecretChanged(count int)
53 }74 }
54 75
55 // NopHandler implements every Handler method as a no-op. Embed it in a handler76 // NopHandler implements every Handler method as a no-op. Embed it in a handler
@@ -69,3 +90,8 @@ func (NopHandler) Closed(SurfaceID) {}
69 func (NopHandler) FeedbackDone(SurfaceID) {}90 func (NopHandler) FeedbackDone(SurfaceID) {}
70 func (NopHandler) Locked() {}91 func (NopHandler) Locked() {}
71 func (NopHandler) LockFinished() {}92 func (NopHandler) LockFinished() {}
93+
94+func (NopHandler) Pointer(*PointerEvent) {}
95+func (NopHandler) Key(*KeyEvent) {}
96+func (NopHandler) KeyboardFocus(SurfaceID, bool) {}
97+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+}
M headless_surface_test.go +31 -0
@@ -104,3 +104,34 @@ func TestHeadlessLockUnlock(t *testing.T) {
104104 }
105105 require.NoError(t, c.Roundtrip())
106106 }
107+
108+// TestHeadlessSeat binds the seat against a real compositor and drives it
109+// through a surface: the seat bind, capability handling and cursor request
110+// must not fail, whatever devices the headless backend offers.
111+func TestHeadlessSeat(t *testing.T) {
112+ c := connectHeadless(t)
113+ s, err := c.NewLayerSurface(neferclient.LayerConfig{
114+ Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
115+ Width: 100, Height: 40, Keyboard: neferclient.KeyboardOnDemand,
116+ })
117+ require.NoError(t, err)
118+ seat := c.Seat()
119+ require.NotNil(t, seat)
120+ seat.SetSecret(neferclient.NewSecretBuffer(16))
121+ seat.SetSecret(nil)
122+
123+ var configured bool
124+ h := neferclientmocks.NewMockHandler(t)
125+ h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
126+ h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
127+ h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
128+ h.EXPECT().Pointer(mock.Anything).Return().Maybe()
129+ h.EXPECT().Key(mock.Anything).Return().Maybe()
130+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return().Maybe()
131+ h.EXPECT().SecretChanged(mock.Anything).Return().Maybe()
132+ dispatchUntil(t, c, h, func() bool { return configured })
133+ require.NoError(t, c.Roundtrip())
134+ require.NoError(t, c.Dispatch(h))
135+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer))
136+ require.NoError(t, s.Close())
137+}
@@ -104,3 +104,34 @@ func TestHeadlessLockUnlock(t *testing.T) {
104 }104 }
105 require.NoError(t, c.Roundtrip())105 require.NoError(t, c.Roundtrip())
106 }106 }
107+
108+// TestHeadlessSeat binds the seat against a real compositor and drives it
109+// through a surface: the seat bind, capability handling and cursor request
110+// must not fail, whatever devices the headless backend offers.
111+func TestHeadlessSeat(t *testing.T) {
112+ c := connectHeadless(t)
113+ s, err := c.NewLayerSurface(neferclient.LayerConfig{
114+ Level: neferclient.LayerTop, Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
115+ Width: 100, Height: 40, Keyboard: neferclient.KeyboardOnDemand,
116+ })
117+ require.NoError(t, err)
118+ seat := c.Seat()
119+ require.NotNil(t, seat)
120+ seat.SetSecret(neferclient.NewSecretBuffer(16))
121+ seat.SetSecret(nil)
122+
123+ var configured bool
124+ h := neferclientmocks.NewMockHandler(t)
125+ h.EXPECT().Configure(s.ID(), mock.Anything, mock.Anything).Run(func(neferclient.SurfaceID, int32, int32) { configured = true }).Return().Maybe()
126+ h.EXPECT().FeedbackDone(mock.Anything).Return().Maybe()
127+ h.EXPECT().Scale(mock.Anything, mock.Anything).Return().Maybe()
128+ h.EXPECT().Pointer(mock.Anything).Return().Maybe()
129+ h.EXPECT().Key(mock.Anything).Return().Maybe()
130+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return().Maybe()
131+ h.EXPECT().SecretChanged(mock.Anything).Return().Maybe()
132+ dispatchUntil(t, c, h, func() bool { return configured })
133+ require.NoError(t, c.Roundtrip())
134+ require.NoError(t, c.Dispatch(h))
135+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer))
136+ require.NoError(t, s.Close())
137+}
A internal/secret/do_off.go +10 -0
@@ -0,0 +1,10 @@
1+//go:build !goexperiment.runtimesecret
2+
3+// Package secret runs key translation that touches secret text under
4+// runtime/secret when the toolchain provides it (GOEXPERIMENT=runtimesecret),
5+// and plainly otherwise.
6+package secret
7+
8+// Do runs f. Without GOEXPERIMENT=runtimesecret there is nothing to erase
9+// beyond what the caller wipes itself.
10+func Do(f func()) { f() }
@@ -0,0 +1,10 @@
1+//go:build !goexperiment.runtimesecret
2+
3+// Package secret runs key translation that touches secret text under
4+// runtime/secret when the toolchain provides it (GOEXPERIMENT=runtimesecret),
5+// and plainly otherwise.
6+package secret
7+
8+// Do runs f. Without GOEXPERIMENT=runtimesecret there is nothing to erase
9+// beyond what the caller wipes itself.
10+func Do(f func()) { f() }
A internal/secret/do_on.go +11 -0
@@ -0,0 +1,11 @@
1+//go:build goexperiment.runtimesecret
2+
3+// Package secret runs key translation that touches secret text under
4+// runtime/secret when the toolchain provides it (GOEXPERIMENT=runtimesecret),
5+// and plainly otherwise.
6+package secret
7+
8+import "runtime/secret"
9+
10+// Do runs f with its registers, stack and heap temporaries erased afterwards.
11+func Do(f func()) { secret.Do(f) }
@@ -0,0 +1,11 @@
1+//go:build goexperiment.runtimesecret
2+
3+// Package secret runs key translation that touches secret text under
4+// runtime/secret when the toolchain provides it (GOEXPERIMENT=runtimesecret),
5+// and plainly otherwise.
6+package secret
7+
8+import "runtime/secret"
9+
10+// Do runs f with its registers, stack and heap temporaries erased afterwards.
11+func Do(f func()) { secret.Do(f) }
A keyboard.go +537 -0
@@ -0,0 +1,537 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "io"
7+ "os"
8+ "time"
9+
10+ xkb "github.com/bnema/purego-xkbcommon"
11+ "github.com/bnema/purego-xkbcommon/raw"
12+ "golang.org/x/sys/unix"
13+
14+ "github.com/bnema/neferclient/internal/secret"
15+)
16+
17+// Modifiers is the set of active keyboard modifiers.
18+type Modifiers uint8
19+
20+const (
21+ ModShift Modifiers = 1 << iota
22+ ModCtrl
23+ ModAlt
24+ ModSuper
25+ ModCapsLock
26+ ModNumLock
27+)
28+
29+// modNames are the xkb modifier names behind each Modifiers bit, in order.
30+var modNames = [...]string{"Shift", "Control", "Mod1", "Mod4", "Lock", "Mod2"}
31+
32+const (
33+ maxHeldKeys = 32
34+ scratchSize = 64 // longest key or compose text kept; longer yields no text
35+ keymapMax = 16 << 20
36+)
37+
38+// heldKey is a pressed key. A key that went into the SecretBuffer keeps no
39+// keysym and is flagged hidden, so its release is reported without either.
40+type heldKey struct {
41+ evdev, sym uint32
42+ hidden bool
43+}
44+
45+type textKind uint8
46+
47+const (
48+ kindNone textKind = iota
49+ kindText // printable text went into the SecretBuffer
50+ kindPending // a dead or compose key: part of a text sequence, no text yet
51+ kindBackspace
52+)
53+
54+// secretWork is the argument and result area of doSecret, which secret.Do runs
55+// without arguments. translate clears it after every use.
56+type secretWork struct {
57+ evdev, sym uint32
58+ mods Modifiers
59+ repeat bool
60+ kind textKind
61+ changed bool
62+ err error
63+}
64+
65+// keyboard interprets wl_keyboard events with xkbcommon. It runs only on the
66+// owner goroutine. It never builds a string from key text: translations go
67+// through the fixed scratch array, which is wiped after every use.
68+type keyboard struct {
69+ ctx *xkb.Context
70+ keymap *xkb.Keymap
71+ state *xkb.State
72+ compose *xkb.ComposeState
73+
74+ modBits [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent
75+ mods Modifiers // effective modifiers and layout, refreshed when the mask changes
76+ group uint32
77+ lastMask [6]uint32
78+ haveMask bool
79+
80+ held []heldKey
81+ focus bool
82+ rate int32 // repeats per second, 0 disables
83+ delay int32 // milliseconds before the first repeat
84+ repeating uint32
85+ composePending bool
86+ composeUsed bool // the last text() call consumed the key into a compose sequence
87+
88+ tfd int // repeat timerfd
89+ tbuf [8]byte
90+
91+ secret *SecretBuffer
92+ scratch [scratchSize]byte
93+
94+ secretFn func()
95+ w secretWork
96+ changed bool // the last translation edited the SecretBuffer
97+ timerErr error // first repeat timer failure, reported by the seat
98+}
99+
100+func newKeyboard(locale string) (*keyboard, error) {
101+ c, err := xkb.NewContext()
102+ if err != nil {
103+ return nil, fmt.Errorf("xkb context: %w", err)
104+ }
105+ k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}
106+ k.secretFn = k.doSecret
107+ if t, e := c.NewComposeTable(locale); e == nil {
108+ k.compose, _ = t.NewState() // compose is optional: without it, dead keys still come from xkb
109+ _ = t.Close()
110+ }
111+ if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {
112+ _ = c.Close()
113+ return nil, fmt.Errorf("repeat timer: %w", err)
114+ }
115+ return k, nil
116+}
117+
118+// composeLocale picks the locale for the compose table the way libc does.
119+func composeLocale() string {
120+ for _, name := range []string{"LC_ALL", "LC_CTYPE", "LANG"} {
121+ if v := os.Getenv(name); v != "" {
122+ return v
123+ }
124+ }
125+ return "C"
126+}
127+
128+func (k *keyboard) close() {
129+ k.disarm()
130+ if k.tfd >= 0 {
131+ _ = unix.Close(k.tfd)
132+ k.tfd = -1
133+ }
134+ if k.compose != nil {
135+ _ = k.compose.Close()
136+ k.compose = nil
137+ }
138+ if k.state != nil {
139+ _ = k.state.Close()
140+ k.state = nil
141+ }
142+ if k.keymap != nil {
143+ _ = k.keymap.Close()
144+ k.keymap = nil
145+ }
146+ if k.ctx != nil {
147+ _ = k.ctx.Close()
148+ k.ctx = nil
149+ }
150+}
151+
152+// replaceFD installs the keymap in fd. It consumes fd even on failure; the
153+// previous keymap stays valid on failure.
154+func (k *keyboard) replaceFD(fd, size int) error {
155+ f := os.NewFile(uintptr(fd), "keymap")
156+ if f == nil {
157+ return errors.New("neferclient: invalid keymap descriptor")
158+ }
159+ defer f.Close()
160+ km, err := k.ctx.NewKeymapFD(fd, size)
161+ if err != nil {
162+ return fmt.Errorf("neferclient: compile keymap: %w", err)
163+ }
164+ return k.replace(km)
165+}
166+
167+func (k *keyboard) replace(km *xkb.Keymap) error {
168+ s, err := km.NewState()
169+ if err != nil {
170+ _ = km.Close()
171+ return fmt.Errorf("neferclient: xkb state: %w", err)
172+ }
173+ k.reset()
174+ if k.state != nil {
175+ _ = k.state.Close()
176+ }
177+ if k.keymap != nil {
178+ _ = k.keymap.Close()
179+ }
180+ k.keymap, k.state = km, s
181+ for i, name := range modNames {
182+ k.modBits[i] = 0
183+ if idx, e := km.ModIndex(name); e == nil && idx < 32 {
184+ k.modBits[i] = 1 << idx
185+ }
186+ }
187+ if k.haveMask { // the compositor's modifiers survive a keymap change
188+ _, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])
189+ }
190+ k.refresh()
191+ return nil
192+}
193+
194+func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
195+ mask := [6]uint32{depressed, latched, locked, 0, 0, group}
196+ if k.haveMask && mask == k.lastMask {
197+ return nil // compositors repeat identical masks; the state is current
198+ }
199+ k.lastMask = mask
200+ k.haveMask = true
201+ if k.state == nil {
202+ return nil
203+ }
204+ changed, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
205+ if err != nil {
206+ return fmt.Errorf("neferclient: update xkb mask: %w", err)
207+ }
208+ if changed != 0 {
209+ k.refresh()
210+ }
211+ return nil
212+}
213+
214+// refresh recomputes the cached modifiers and layout from the xkb state. Both
215+// only change when the mask does, so keys do not query them.
216+func (k *keyboard) refresh() {
217+ k.mods = k.queryModifiers()
218+ k.group, _ = k.state.Layout()
219+}
220+
221+// reset drops held keys, repeat and compose state. It keeps the keymap, the
222+// modifier mask and the focus flag.
223+func (k *keyboard) reset() {
224+ clear(k.held[:cap(k.held)]) // no hidden-key record outlives the reset
225+ k.held = k.held[:0]
226+ k.repeating = 0
227+ k.disarm()
228+ k.composePending = false
229+ if k.compose != nil {
230+ _ = k.compose.Reset()
231+ }
232+}
233+
234+func (k *keyboard) setFocus(on bool) {
235+ k.focus = on
236+ k.reset()
237+}
238+
239+func (k *keyboard) setSecret(b *SecretBuffer) {
240+ k.secret = b
241+ k.reset() // switching paths cancels held keys, repeat and compose
242+}
243+
244+func (k *keyboard) setRepeat(rate, delay int32) {
245+ k.rate, k.delay = max(rate, 0), max(delay, 0)
246+ if k.rate == 0 {
247+ k.repeating = 0
248+ k.disarm()
249+ }
250+}
251+
252+// arm starts the repeat timer: first expiry after the delay, then at the rate.
253+func (k *keyboard) arm() {
254+ spec := unix.ItimerSpec{
255+ Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),
256+ Value: unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm
257+ }
258+ k.setTimer(&spec)
259+}
260+
261+func (k *keyboard) disarm() {
262+ if k.tfd >= 0 {
263+ var spec unix.ItimerSpec
264+ k.setTimer(&spec)
265+ }
266+}
267+
268+func (k *keyboard) setTimer(spec *unix.ItimerSpec) {
269+ if err := unix.TimerfdSettime(k.tfd, 0, spec, nil); err != nil && k.timerErr == nil {
270+ k.timerErr = fmt.Errorf("neferclient: repeat timer: %w", err)
271+ }
272+}
273+
274+// takeTimerErr returns and clears the first timer failure.
275+func (k *keyboard) takeTimerErr() error {
276+ err := k.timerErr
277+ k.timerErr = nil
278+ return err
279+}
280+
281+// expired consumes a timer expiration. Missed ticks are not accumulated: one
282+// call yields at most one repeat.
283+func (k *keyboard) expired() bool {
284+ n, err := unix.Read(k.tfd, k.tbuf[:])
285+ return err == nil && n == 8
286+}
287+
288+func (k *keyboard) queryModifiers() Modifiers {
289+ mods, err := k.state.Mods()
290+ if err != nil {
291+ return 0
292+ }
293+ var m Modifiers
294+ for i, bit := range k.modBits {
295+ if bit != 0 && mods&bit != 0 {
296+ m |= 1 << i
297+ }
298+ }
299+ return m
300+}
301+
302+func (k *keyboard) find(evdev uint32) int {
303+ for i := range k.held {
304+ if k.held[i].evdev == evdev {
305+ return i
306+ }
307+ }
308+ return -1
309+}
310+
311+func (k *keyboard) drop(i int) {
312+ last := len(k.held) - 1
313+ copy(k.held[i:], k.held[i+1:])
314+ k.held[last] = heldKey{} // clear the vacated slot
315+ k.held = k.held[:last]
316+}
317+
318+// press translates a key press into ev. It reports false when the press must
319+// not be delivered (no focus or keymap).
320+func (k *keyboard) press(evdev uint32, ev *KeyEvent) (bool, error) {
321+ if !k.focus || k.state == nil {
322+ return false, nil
323+ }
324+ if i := k.find(evdev); i >= 0 {
325+ k.drop(i)
326+ }
327+ if len(k.held) == cap(k.held) {
328+ return false, nil
329+ }
330+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
331+ if err != nil {
332+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
333+ }
334+ kind, err := k.translate(evdev, sym, false, ev)
335+ if err != nil {
336+ return false, fmt.Errorf("neferclient: key text: %w", err)
337+ }
338+ if ev.Secret {
339+ k.held = append(k.held, heldKey{evdev: evdev, hidden: true})
340+ } else {
341+ k.held = append(k.held, heldKey{evdev: evdev, sym: sym})
342+ }
343+ if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {
344+ if k.rate > 0 {
345+ k.repeating = evdev
346+ k.arm()
347+ }
348+ } else if k.secret != nil && k.repeating != 0 && repeatable(sym) {
349+ // A control or rejected key stops a previous printable repeat;
350+ // a modifier does not.
351+ k.repeating = 0
352+ k.disarm()
353+ }
354+ return true, nil
355+}
356+
357+// release fills ev for a key released after a delivered press.
358+func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
359+ i := k.find(evdev)
360+ if i < 0 {
361+ return false
362+ }
363+ h := k.held[i]
364+ k.drop(i)
365+ if k.repeating == evdev {
366+ k.repeating = 0
367+ k.disarm()
368+ }
369+ ev.Pressed, ev.Secret = false, h.hidden
370+ if !h.hidden {
371+ ev.Keycode, ev.Keysym = evdev, h.sym
372+ }
373+ k.changed = false
374+ ev.Modifiers, ev.Group = k.mods, k.group
375+ return true
376+}
377+
378+// repeat produces one repeat of the held key whose timer fired.
379+func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
380+ if !k.focus || k.state == nil || k.rate <= 0 || k.repeating == 0 || k.find(k.repeating) < 0 {
381+ return false, nil
382+ }
383+ evdev := k.repeating
384+ stop := func() (bool, error) {
385+ k.repeating = 0
386+ k.disarm()
387+ return false, nil
388+ }
389+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
390+ if err != nil {
391+ _, _ = stop()
392+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
393+ }
394+ if k.secret == nil && !repeatable(sym) {
395+ return stop()
396+ }
397+ kind, err := k.translate(evdev, sym, true, ev)
398+ if err != nil {
399+ _, _ = stop()
400+ return false, fmt.Errorf("neferclient: key text: %w", err)
401+ }
402+ if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {
403+ return stop()
404+ }
405+ return true, nil
406+}
407+
408+// translate fills ev for a press or repeat and returns what the key meant to
409+// the SecretBuffer, if one is set. In secret mode ev.Text stays nil.
410+func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {
411+ ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat
412+ ev.Modifiers, ev.Group = k.mods, k.group
413+ k.changed = false
414+ if k.secret != nil {
415+ k.w = secretWork{evdev: evdev, sym: sym, mods: ev.Modifiers, repeat: repeat}
416+ secret.Do(k.secretFn)
417+ kind, changed, err := k.w.kind, k.w.changed, k.w.err
418+ k.w = secretWork{} // the key identity must not stay in the work area
419+ k.changed = changed
420+ if kind == kindText || kind == kindPending {
421+ // Text keys are not identified: the keycode and keysym would
422+ // reveal the secret character.
423+ ev.Keycode, ev.Keysym, ev.Secret = 0, 0, true
424+ }
425+ return kind, err
426+ }
427+ n, err := k.text(evdev, sym, repeat)
428+ if err != nil {
429+ return kindNone, err
430+ }
431+ if n > 0 {
432+ ev.Text = k.scratch[:n:n]
433+ }
434+ return kindNone, nil
435+}
436+
437+// doSecret translates one key into the SecretBuffer. It is the only code that
438+// handles secret text, so it runs under runtime/secret when available.
439+func (k *keyboard) doSecret() {
440+ w := &k.w
441+ defer clear(k.scratch[:])
442+ switch {
443+ case w.sym == raw.XKB_KEY_BackSpace:
444+ w.kind = kindBackspace
445+ k.resetCompose()
446+ w.changed = k.secret.backspace()
447+ case w.sym == raw.XKB_KEY_Return || w.sym == raw.XKB_KEY_KP_Enter || w.sym == raw.XKB_KEY_Escape:
448+ k.resetCompose() // controls are delivered as keys only
449+ case w.mods&ModCtrl != 0:
450+ // Other Control combinations are neither secret text nor ours to handle.
451+ default:
452+ n, err := k.text(w.evdev, w.sym, w.repeat)
453+ if err != nil {
454+ w.err = err
455+ return
456+ }
457+ switch {
458+ case classifySecretText(k.scratch[:n]):
459+ w.kind = kindText
460+ w.changed = k.secret.appendText(k.scratch[:n])
461+ case k.composeUsed || k.composePending || isDeadKey(w.sym):
462+ w.kind = kindPending // dead and compose keys name an accent, so hide them too
463+ }
464+ }
465+}
466+
467+func (k *keyboard) resetCompose() {
468+ if k.compose != nil {
469+ _ = k.compose.Reset()
470+ }
471+ k.composePending = false
472+}
473+
474+// text writes the text of a key into scratch and returns its length. Compose
475+// is fed only by physical presses: a repeat neither advances a sequence nor
476+// replays composed text.
477+func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
478+ k.composeUsed = false
479+ if k.compose != nil && !repeat {
480+ if _, err := k.compose.Feed(sym); err != nil {
481+ return 0, err
482+ }
483+ status, err := k.compose.Status()
484+ if err != nil {
485+ return 0, err
486+ }
487+ k.composeUsed = status == raw.XKB_COMPOSE_COMPOSING || status == raw.XKB_COMPOSE_CANCELLED || status == raw.XKB_COMPOSE_COMPOSED
488+ switch status {
489+ case raw.XKB_COMPOSE_COMPOSING:
490+ k.composePending = true
491+ return 0, nil
492+ case raw.XKB_COMPOSE_CANCELLED:
493+ k.resetCompose()
494+ return 0, nil
495+ case raw.XKB_COMPOSE_COMPOSED:
496+ n, err := k.compose.UTF8Into(k.scratch[:])
497+ k.resetCompose()
498+ return shortAsEmpty(n, err)
499+ default:
500+ k.composePending = false
501+ }
502+ }
503+ if k.composePending {
504+ return 0, nil
505+ }
506+ n, err := k.state.UTF8Into(xkb.WaylandKeycode(evdev), k.scratch[:])
507+ return shortAsEmpty(n, err)
508+}
509+
510+// shortAsEmpty maps a too-long text to no text; xkb already wiped the buffer.
511+func shortAsEmpty(n int, err error) (int, error) {
512+ if errors.Is(err, io.ErrShortBuffer) {
513+ return 0, nil
514+ }
515+ return n, err
516+}
517+
518+// Modifier, dead and compose keys never repeat. The compositor provides rate
519+// and delay; its keymap is authoritative for text and layout.
520+func repeatable(sym uint32) bool {
521+ return !isModifierKey(sym) && !isDeadKey(sym)
522+}
523+
524+func isModifierKey(sym uint32) bool {
525+ switch sym {
526+ case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
527+ raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
528+ raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock:
529+ return true
530+ }
531+ return false
532+}
533+
534+// isDeadKey reports dead keys and the compose key, which start a sequence.
535+func isDeadKey(sym uint32) bool {
536+ return sym == raw.XKB_KEY_Multi_key || sym >= raw.XKB_KEY_dead_grave && sym <= raw.XKB_KEY_dead_currency
537+}
@@ -0,0 +1,537 @@
1+package neferclient
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "io"
7+ "os"
8+ "time"
9+
10+ xkb "github.com/bnema/purego-xkbcommon"
11+ "github.com/bnema/purego-xkbcommon/raw"
12+ "golang.org/x/sys/unix"
13+
14+ "github.com/bnema/neferclient/internal/secret"
15+)
16+
17+// Modifiers is the set of active keyboard modifiers.
18+type Modifiers uint8
19+
20+const (
21+ ModShift Modifiers = 1 << iota
22+ ModCtrl
23+ ModAlt
24+ ModSuper
25+ ModCapsLock
26+ ModNumLock
27+)
28+
29+// modNames are the xkb modifier names behind each Modifiers bit, in order.
30+var modNames = [...]string{"Shift", "Control", "Mod1", "Mod4", "Lock", "Mod2"}
31+
32+const (
33+ maxHeldKeys = 32
34+ scratchSize = 64 // longest key or compose text kept; longer yields no text
35+ keymapMax = 16 << 20
36+)
37+
38+// heldKey is a pressed key. A key that went into the SecretBuffer keeps no
39+// keysym and is flagged hidden, so its release is reported without either.
40+type heldKey struct {
41+ evdev, sym uint32
42+ hidden bool
43+}
44+
45+type textKind uint8
46+
47+const (
48+ kindNone textKind = iota
49+ kindText // printable text went into the SecretBuffer
50+ kindPending // a dead or compose key: part of a text sequence, no text yet
51+ kindBackspace
52+)
53+
54+// secretWork is the argument and result area of doSecret, which secret.Do runs
55+// without arguments. translate clears it after every use.
56+type secretWork struct {
57+ evdev, sym uint32
58+ mods Modifiers
59+ repeat bool
60+ kind textKind
61+ changed bool
62+ err error
63+}
64+
65+// keyboard interprets wl_keyboard events with xkbcommon. It runs only on the
66+// owner goroutine. It never builds a string from key text: translations go
67+// through the fixed scratch array, which is wiped after every use.
68+type keyboard struct {
69+ ctx *xkb.Context
70+ keymap *xkb.Keymap
71+ state *xkb.State
72+ compose *xkb.ComposeState
73+
74+ modBits [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent
75+ mods Modifiers // effective modifiers and layout, refreshed when the mask changes
76+ group uint32
77+ lastMask [6]uint32
78+ haveMask bool
79+
80+ held []heldKey
81+ focus bool
82+ rate int32 // repeats per second, 0 disables
83+ delay int32 // milliseconds before the first repeat
84+ repeating uint32
85+ composePending bool
86+ composeUsed bool // the last text() call consumed the key into a compose sequence
87+
88+ tfd int // repeat timerfd
89+ tbuf [8]byte
90+
91+ secret *SecretBuffer
92+ scratch [scratchSize]byte
93+
94+ secretFn func()
95+ w secretWork
96+ changed bool // the last translation edited the SecretBuffer
97+ timerErr error // first repeat timer failure, reported by the seat
98+}
99+
100+func newKeyboard(locale string) (*keyboard, error) {
101+ c, err := xkb.NewContext()
102+ if err != nil {
103+ return nil, fmt.Errorf("xkb context: %w", err)
104+ }
105+ k := &keyboard{ctx: c, held: make([]heldKey, 0, maxHeldKeys), tfd: -1}
106+ k.secretFn = k.doSecret
107+ if t, e := c.NewComposeTable(locale); e == nil {
108+ k.compose, _ = t.NewState() // compose is optional: without it, dead keys still come from xkb
109+ _ = t.Close()
110+ }
111+ if k.tfd, err = unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC); err != nil {
112+ _ = c.Close()
113+ return nil, fmt.Errorf("repeat timer: %w", err)
114+ }
115+ return k, nil
116+}
117+
118+// composeLocale picks the locale for the compose table the way libc does.
119+func composeLocale() string {
120+ for _, name := range []string{"LC_ALL", "LC_CTYPE", "LANG"} {
121+ if v := os.Getenv(name); v != "" {
122+ return v
123+ }
124+ }
125+ return "C"
126+}
127+
128+func (k *keyboard) close() {
129+ k.disarm()
130+ if k.tfd >= 0 {
131+ _ = unix.Close(k.tfd)
132+ k.tfd = -1
133+ }
134+ if k.compose != nil {
135+ _ = k.compose.Close()
136+ k.compose = nil
137+ }
138+ if k.state != nil {
139+ _ = k.state.Close()
140+ k.state = nil
141+ }
142+ if k.keymap != nil {
143+ _ = k.keymap.Close()
144+ k.keymap = nil
145+ }
146+ if k.ctx != nil {
147+ _ = k.ctx.Close()
148+ k.ctx = nil
149+ }
150+}
151+
152+// replaceFD installs the keymap in fd. It consumes fd even on failure; the
153+// previous keymap stays valid on failure.
154+func (k *keyboard) replaceFD(fd, size int) error {
155+ f := os.NewFile(uintptr(fd), "keymap")
156+ if f == nil {
157+ return errors.New("neferclient: invalid keymap descriptor")
158+ }
159+ defer f.Close()
160+ km, err := k.ctx.NewKeymapFD(fd, size)
161+ if err != nil {
162+ return fmt.Errorf("neferclient: compile keymap: %w", err)
163+ }
164+ return k.replace(km)
165+}
166+
167+func (k *keyboard) replace(km *xkb.Keymap) error {
168+ s, err := km.NewState()
169+ if err != nil {
170+ _ = km.Close()
171+ return fmt.Errorf("neferclient: xkb state: %w", err)
172+ }
173+ k.reset()
174+ if k.state != nil {
175+ _ = k.state.Close()
176+ }
177+ if k.keymap != nil {
178+ _ = k.keymap.Close()
179+ }
180+ k.keymap, k.state = km, s
181+ for i, name := range modNames {
182+ k.modBits[i] = 0
183+ if idx, e := km.ModIndex(name); e == nil && idx < 32 {
184+ k.modBits[i] = 1 << idx
185+ }
186+ }
187+ if k.haveMask { // the compositor's modifiers survive a keymap change
188+ _, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])
189+ }
190+ k.refresh()
191+ return nil
192+}
193+
194+func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
195+ mask := [6]uint32{depressed, latched, locked, 0, 0, group}
196+ if k.haveMask && mask == k.lastMask {
197+ return nil // compositors repeat identical masks; the state is current
198+ }
199+ k.lastMask = mask
200+ k.haveMask = true
201+ if k.state == nil {
202+ return nil
203+ }
204+ changed, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
205+ if err != nil {
206+ return fmt.Errorf("neferclient: update xkb mask: %w", err)
207+ }
208+ if changed != 0 {
209+ k.refresh()
210+ }
211+ return nil
212+}
213+
214+// refresh recomputes the cached modifiers and layout from the xkb state. Both
215+// only change when the mask does, so keys do not query them.
216+func (k *keyboard) refresh() {
217+ k.mods = k.queryModifiers()
218+ k.group, _ = k.state.Layout()
219+}
220+
221+// reset drops held keys, repeat and compose state. It keeps the keymap, the
222+// modifier mask and the focus flag.
223+func (k *keyboard) reset() {
224+ clear(k.held[:cap(k.held)]) // no hidden-key record outlives the reset
225+ k.held = k.held[:0]
226+ k.repeating = 0
227+ k.disarm()
228+ k.composePending = false
229+ if k.compose != nil {
230+ _ = k.compose.Reset()
231+ }
232+}
233+
234+func (k *keyboard) setFocus(on bool) {
235+ k.focus = on
236+ k.reset()
237+}
238+
239+func (k *keyboard) setSecret(b *SecretBuffer) {
240+ k.secret = b
241+ k.reset() // switching paths cancels held keys, repeat and compose
242+}
243+
244+func (k *keyboard) setRepeat(rate, delay int32) {
245+ k.rate, k.delay = max(rate, 0), max(delay, 0)
246+ if k.rate == 0 {
247+ k.repeating = 0
248+ k.disarm()
249+ }
250+}
251+
252+// arm starts the repeat timer: first expiry after the delay, then at the rate.
253+func (k *keyboard) arm() {
254+ spec := unix.ItimerSpec{
255+ Interval: unix.NsecToTimespec(int64(time.Second) / int64(k.rate)),
256+ Value: unix.NsecToTimespec(max(int64(k.delay)*int64(time.Millisecond), 1)), // zero would disarm
257+ }
258+ k.setTimer(&spec)
259+}
260+
261+func (k *keyboard) disarm() {
262+ if k.tfd >= 0 {
263+ var spec unix.ItimerSpec
264+ k.setTimer(&spec)
265+ }
266+}
267+
268+func (k *keyboard) setTimer(spec *unix.ItimerSpec) {
269+ if err := unix.TimerfdSettime(k.tfd, 0, spec, nil); err != nil && k.timerErr == nil {
270+ k.timerErr = fmt.Errorf("neferclient: repeat timer: %w", err)
271+ }
272+}
273+
274+// takeTimerErr returns and clears the first timer failure.
275+func (k *keyboard) takeTimerErr() error {
276+ err := k.timerErr
277+ k.timerErr = nil
278+ return err
279+}
280+
281+// expired consumes a timer expiration. Missed ticks are not accumulated: one
282+// call yields at most one repeat.
283+func (k *keyboard) expired() bool {
284+ n, err := unix.Read(k.tfd, k.tbuf[:])
285+ return err == nil && n == 8
286+}
287+
288+func (k *keyboard) queryModifiers() Modifiers {
289+ mods, err := k.state.Mods()
290+ if err != nil {
291+ return 0
292+ }
293+ var m Modifiers
294+ for i, bit := range k.modBits {
295+ if bit != 0 && mods&bit != 0 {
296+ m |= 1 << i
297+ }
298+ }
299+ return m
300+}
301+
302+func (k *keyboard) find(evdev uint32) int {
303+ for i := range k.held {
304+ if k.held[i].evdev == evdev {
305+ return i
306+ }
307+ }
308+ return -1
309+}
310+
311+func (k *keyboard) drop(i int) {
312+ last := len(k.held) - 1
313+ copy(k.held[i:], k.held[i+1:])
314+ k.held[last] = heldKey{} // clear the vacated slot
315+ k.held = k.held[:last]
316+}
317+
318+// press translates a key press into ev. It reports false when the press must
319+// not be delivered (no focus or keymap).
320+func (k *keyboard) press(evdev uint32, ev *KeyEvent) (bool, error) {
321+ if !k.focus || k.state == nil {
322+ return false, nil
323+ }
324+ if i := k.find(evdev); i >= 0 {
325+ k.drop(i)
326+ }
327+ if len(k.held) == cap(k.held) {
328+ return false, nil
329+ }
330+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
331+ if err != nil {
332+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
333+ }
334+ kind, err := k.translate(evdev, sym, false, ev)
335+ if err != nil {
336+ return false, fmt.Errorf("neferclient: key text: %w", err)
337+ }
338+ if ev.Secret {
339+ k.held = append(k.held, heldKey{evdev: evdev, hidden: true})
340+ } else {
341+ k.held = append(k.held, heldKey{evdev: evdev, sym: sym})
342+ }
343+ if k.secret == nil && repeatable(sym) || k.secret != nil && (kind == kindBackspace || kind == kindText && repeatable(sym)) {
344+ if k.rate > 0 {
345+ k.repeating = evdev
346+ k.arm()
347+ }
348+ } else if k.secret != nil && k.repeating != 0 && repeatable(sym) {
349+ // A control or rejected key stops a previous printable repeat;
350+ // a modifier does not.
351+ k.repeating = 0
352+ k.disarm()
353+ }
354+ return true, nil
355+}
356+
357+// release fills ev for a key released after a delivered press.
358+func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
359+ i := k.find(evdev)
360+ if i < 0 {
361+ return false
362+ }
363+ h := k.held[i]
364+ k.drop(i)
365+ if k.repeating == evdev {
366+ k.repeating = 0
367+ k.disarm()
368+ }
369+ ev.Pressed, ev.Secret = false, h.hidden
370+ if !h.hidden {
371+ ev.Keycode, ev.Keysym = evdev, h.sym
372+ }
373+ k.changed = false
374+ ev.Modifiers, ev.Group = k.mods, k.group
375+ return true
376+}
377+
378+// repeat produces one repeat of the held key whose timer fired.
379+func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
380+ if !k.focus || k.state == nil || k.rate <= 0 || k.repeating == 0 || k.find(k.repeating) < 0 {
381+ return false, nil
382+ }
383+ evdev := k.repeating
384+ stop := func() (bool, error) {
385+ k.repeating = 0
386+ k.disarm()
387+ return false, nil
388+ }
389+ sym, err := k.state.KeySym(xkb.WaylandKeycode(evdev))
390+ if err != nil {
391+ _, _ = stop()
392+ return false, fmt.Errorf("neferclient: key symbol: %w", err)
393+ }
394+ if k.secret == nil && !repeatable(sym) {
395+ return stop()
396+ }
397+ kind, err := k.translate(evdev, sym, true, ev)
398+ if err != nil {
399+ _, _ = stop()
400+ return false, fmt.Errorf("neferclient: key text: %w", err)
401+ }
402+ if k.secret != nil && kind != kindBackspace && !(kind == kindText && repeatable(sym)) {
403+ return stop()
404+ }
405+ return true, nil
406+}
407+
408+// translate fills ev for a press or repeat and returns what the key meant to
409+// the SecretBuffer, if one is set. In secret mode ev.Text stays nil.
410+func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {
411+ ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat
412+ ev.Modifiers, ev.Group = k.mods, k.group
413+ k.changed = false
414+ if k.secret != nil {
415+ k.w = secretWork{evdev: evdev, sym: sym, mods: ev.Modifiers, repeat: repeat}
416+ secret.Do(k.secretFn)
417+ kind, changed, err := k.w.kind, k.w.changed, k.w.err
418+ k.w = secretWork{} // the key identity must not stay in the work area
419+ k.changed = changed
420+ if kind == kindText || kind == kindPending {
421+ // Text keys are not identified: the keycode and keysym would
422+ // reveal the secret character.
423+ ev.Keycode, ev.Keysym, ev.Secret = 0, 0, true
424+ }
425+ return kind, err
426+ }
427+ n, err := k.text(evdev, sym, repeat)
428+ if err != nil {
429+ return kindNone, err
430+ }
431+ if n > 0 {
432+ ev.Text = k.scratch[:n:n]
433+ }
434+ return kindNone, nil
435+}
436+
437+// doSecret translates one key into the SecretBuffer. It is the only code that
438+// handles secret text, so it runs under runtime/secret when available.
439+func (k *keyboard) doSecret() {
440+ w := &k.w
441+ defer clear(k.scratch[:])
442+ switch {
443+ case w.sym == raw.XKB_KEY_BackSpace:
444+ w.kind = kindBackspace
445+ k.resetCompose()
446+ w.changed = k.secret.backspace()
447+ case w.sym == raw.XKB_KEY_Return || w.sym == raw.XKB_KEY_KP_Enter || w.sym == raw.XKB_KEY_Escape:
448+ k.resetCompose() // controls are delivered as keys only
449+ case w.mods&ModCtrl != 0:
450+ // Other Control combinations are neither secret text nor ours to handle.
451+ default:
452+ n, err := k.text(w.evdev, w.sym, w.repeat)
453+ if err != nil {
454+ w.err = err
455+ return
456+ }
457+ switch {
458+ case classifySecretText(k.scratch[:n]):
459+ w.kind = kindText
460+ w.changed = k.secret.appendText(k.scratch[:n])
461+ case k.composeUsed || k.composePending || isDeadKey(w.sym):
462+ w.kind = kindPending // dead and compose keys name an accent, so hide them too
463+ }
464+ }
465+}
466+
467+func (k *keyboard) resetCompose() {
468+ if k.compose != nil {
469+ _ = k.compose.Reset()
470+ }
471+ k.composePending = false
472+}
473+
474+// text writes the text of a key into scratch and returns its length. Compose
475+// is fed only by physical presses: a repeat neither advances a sequence nor
476+// replays composed text.
477+func (k *keyboard) text(evdev, sym uint32, repeat bool) (int, error) {
478+ k.composeUsed = false
479+ if k.compose != nil && !repeat {
480+ if _, err := k.compose.Feed(sym); err != nil {
481+ return 0, err
482+ }
483+ status, err := k.compose.Status()
484+ if err != nil {
485+ return 0, err
486+ }
487+ k.composeUsed = status == raw.XKB_COMPOSE_COMPOSING || status == raw.XKB_COMPOSE_CANCELLED || status == raw.XKB_COMPOSE_COMPOSED
488+ switch status {
489+ case raw.XKB_COMPOSE_COMPOSING:
490+ k.composePending = true
491+ return 0, nil
492+ case raw.XKB_COMPOSE_CANCELLED:
493+ k.resetCompose()
494+ return 0, nil
495+ case raw.XKB_COMPOSE_COMPOSED:
496+ n, err := k.compose.UTF8Into(k.scratch[:])
497+ k.resetCompose()
498+ return shortAsEmpty(n, err)
499+ default:
500+ k.composePending = false
501+ }
502+ }
503+ if k.composePending {
504+ return 0, nil
505+ }
506+ n, err := k.state.UTF8Into(xkb.WaylandKeycode(evdev), k.scratch[:])
507+ return shortAsEmpty(n, err)
508+}
509+
510+// shortAsEmpty maps a too-long text to no text; xkb already wiped the buffer.
511+func shortAsEmpty(n int, err error) (int, error) {
512+ if errors.Is(err, io.ErrShortBuffer) {
513+ return 0, nil
514+ }
515+ return n, err
516+}
517+
518+// Modifier, dead and compose keys never repeat. The compositor provides rate
519+// and delay; its keymap is authoritative for text and layout.
520+func repeatable(sym uint32) bool {
521+ return !isModifierKey(sym) && !isDeadKey(sym)
522+}
523+
524+func isModifierKey(sym uint32) bool {
525+ switch sym {
526+ case raw.XKB_KEY_Shift_L, raw.XKB_KEY_Shift_R, raw.XKB_KEY_Control_L, raw.XKB_KEY_Control_R,
527+ raw.XKB_KEY_Alt_L, raw.XKB_KEY_Alt_R, raw.XKB_KEY_Super_L, raw.XKB_KEY_Super_R,
528+ raw.XKB_KEY_Caps_Lock, raw.XKB_KEY_Num_Lock:
529+ return true
530+ }
531+ return false
532+}
533+
534+// isDeadKey reports dead keys and the compose key, which start a sequence.
535+func isDeadKey(sym uint32) bool {
536+ return sym == raw.XKB_KEY_Multi_key || sym >= raw.XKB_KEY_dead_grave && sym <= raw.XKB_KEY_dead_currency
537+}
A keyboard_test.go +342 -0
@@ -0,0 +1,342 @@
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, "C", "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 := pressUnchecked(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 pressUnchecked(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, "C", "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, "C", "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+ rel, ok := releaseKey(k, code)
197+ require.True(t, ok)
198+ require.Equal(t, ev.Secret, rel.Secret, "the release is hidden like the press")
199+ if ev.Secret {
200+ require.Zero(t, rel.Keycode)
201+ require.Zero(t, rel.Keysym)
202+ }
203+ return ev
204+ }
205+ for _, code := range []uint32{keyQ, keyCircum, keyE} { // text, dead key, composed ê
206+ ev := press(code)
207+ require.True(t, ev.Secret, "text keys are hidden")
208+ require.Zero(t, ev.Keycode)
209+ require.Zero(t, ev.Keysym)
210+ }
211+ require.Equal(t, "qê", string(buf.Bytes()))
212+ require.Equal(t, 2, buf.Len())
213+
214+ bs := press(keyBackspace)
215+ require.False(t, bs.Secret)
216+ require.Equal(t, uint32(raw.XKB_KEY_BackSpace), bs.Keysym, "BackSpace keeps its keysym")
217+ require.Equal(t, 1, buf.Len(), "backspace removes one code point")
218+ press(keyBackspace)
219+ press(keyBackspace) // empty: nothing to remove
220+ require.Zero(t, buf.Len())
221+
222+ press(keyQ)
223+ enter := press(keyEnter)
224+ require.False(t, enter.Secret)
225+ require.Equal(t, uint32(raw.XKB_KEY_Return), enter.Keysym)
226+ require.Equal(t, 1, buf.Len(), "Enter is delivered, not typed")
227+
228+ // Control combinations are keys, not text.
229+ idx, err := k.keymap.ModIndex("Control")
230+ require.NoError(t, err)
231+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
232+ ev := press(keyU)
233+ require.False(t, ev.Secret)
234+ require.NotZero(t, ev.Keysym, "Control combinations keep their keysym")
235+ require.Equal(t, ModCtrl, ev.Modifiers)
236+ require.Equal(t, 1, buf.Len())
237+ require.NoError(t, k.setMask(0, 0, 0, 0))
238+
239+ // Capacity: extra text is dropped, the key is still delivered.
240+ for range 10 {
241+ press(keyQ)
242+ }
243+ require.Equal(t, 8, buf.Len())
244+
245+ // Scratch and the work area are wiped after use.
246+ require.Equal(t, make([]byte, scratchSize), k.scratch[:])
247+ require.Equal(t, secretWork{}, k.w)
248+ for _, h := range k.held {
249+ require.Zero(t, h, "no stale held key")
250+ }
251+
252+ k.setSecret(nil)
253+ ev, _ = pressKey(t, k, keyQ)
254+ require.False(t, ev.Secret)
255+ require.Equal(t, "q", string(ev.Text), "ordinary text again once the buffer is removed")
256+}
257+
258+func TestSecretChangedFlag(t *testing.T) {
259+ k := testKeyboard(t, "C", "us")
260+ buf := NewSecretBuffer(2)
261+ k.setSecret(buf)
262+ pressKey(t, k, keyQ)
263+ require.True(t, k.changed)
264+ pressKey(t, k, keyEnter)
265+ require.False(t, k.changed)
266+ pressKey(t, k, keyBackspace)
267+ require.True(t, k.changed)
268+ pressKey(t, k, keyBackspace)
269+ require.False(t, k.changed, "nothing left to remove")
270+}
271+
272+func TestSecretRepeat(t *testing.T) {
273+ k := testKeyboard(t, "C", "us")
274+ buf := NewSecretBuffer(16)
275+ k.setSecret(buf)
276+ k.setRepeat(100, 1)
277+ var ev KeyEvent
278+ pressKey(t, k, keyQ)
279+ for range 2 {
280+ require.Eventually(t, k.expired, 2e9, 1e6)
281+ ok, err := k.repeat(&ev)
282+ require.NoError(t, err)
283+ require.True(t, ok)
284+ require.Nil(t, ev.Text)
285+ }
286+ require.Equal(t, "qqq", string(buf.Bytes()))
287+
288+ // Enter cancels a previous printable repeat.
289+ pressKey(t, k, keyEnter)
290+ require.Zero(t, k.repeating)
291+ releaseKey(k, keyQ)
292+ releaseKey(k, keyEnter)
293+
294+ // Held backspace repeats.
295+ pressKey(t, k, keyBackspace)
296+ require.Equal(t, uint32(keyBackspace), k.repeating)
297+ require.Eventually(t, k.expired, 2e9, 1e6)
298+ ok, err := k.repeat(&ev)
299+ require.NoError(t, err)
300+ require.True(t, ok)
301+ require.Equal(t, 1, buf.Len())
302+}
303+
304+func TestSecretRepeatIsHiddenToo(t *testing.T) {
305+ k := testKeyboard(t, "C", "us")
306+ k.setSecret(NewSecretBuffer(8))
307+ k.setRepeat(100, 1)
308+ pressKey(t, k, keyQ)
309+ require.Eventually(t, k.expired, 2e9, 1e6)
310+ var ev KeyEvent
311+ ok, err := k.repeat(&ev)
312+ require.NoError(t, err)
313+ require.True(t, ok)
314+ require.True(t, ev.Secret)
315+ require.Zero(t, ev.Keycode)
316+ require.Zero(t, ev.Keysym)
317+}
318+
319+func TestKeyboardHeldTailAndResetAreCleared(t *testing.T) {
320+ k := testKeyboard(t, "C", "us")
321+ k.setSecret(NewSecretBuffer(8))
322+ pressKey(t, k, keyQ)
323+ pressKey(t, k, keyE)
324+ releaseKey(k, keyQ)
325+ require.Equal(t, heldKey{}, k.held[:cap(k.held)][1], "vacated slot cleared")
326+ k.setFocus(false)
327+ for _, h := range k.held[:cap(k.held)] {
328+ require.Zero(t, h)
329+ }
330+}
331+
332+func TestKeyboardIdenticalMaskIsNoOp(t *testing.T) {
333+ k := testKeyboard(t, "C", "us")
334+ idx, err := k.keymap.ModIndex("Shift")
335+ require.NoError(t, err)
336+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
337+ require.Equal(t, ModShift, k.mods)
338+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
339+ require.Equal(t, ModShift, k.mods)
340+ require.NoError(t, k.setMask(0, 0, 0, 0))
341+ require.Zero(t, k.mods)
342+}
@@ -0,0 +1,342 @@
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, "C", "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 := pressUnchecked(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 pressUnchecked(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, "C", "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, "C", "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+ rel, ok := releaseKey(k, code)
197+ require.True(t, ok)
198+ require.Equal(t, ev.Secret, rel.Secret, "the release is hidden like the press")
199+ if ev.Secret {
200+ require.Zero(t, rel.Keycode)
201+ require.Zero(t, rel.Keysym)
202+ }
203+ return ev
204+ }
205+ for _, code := range []uint32{keyQ, keyCircum, keyE} { // text, dead key, composed ê
206+ ev := press(code)
207+ require.True(t, ev.Secret, "text keys are hidden")
208+ require.Zero(t, ev.Keycode)
209+ require.Zero(t, ev.Keysym)
210+ }
211+ require.Equal(t, "qê", string(buf.Bytes()))
212+ require.Equal(t, 2, buf.Len())
213+
214+ bs := press(keyBackspace)
215+ require.False(t, bs.Secret)
216+ require.Equal(t, uint32(raw.XKB_KEY_BackSpace), bs.Keysym, "BackSpace keeps its keysym")
217+ require.Equal(t, 1, buf.Len(), "backspace removes one code point")
218+ press(keyBackspace)
219+ press(keyBackspace) // empty: nothing to remove
220+ require.Zero(t, buf.Len())
221+
222+ press(keyQ)
223+ enter := press(keyEnter)
224+ require.False(t, enter.Secret)
225+ require.Equal(t, uint32(raw.XKB_KEY_Return), enter.Keysym)
226+ require.Equal(t, 1, buf.Len(), "Enter is delivered, not typed")
227+
228+ // Control combinations are keys, not text.
229+ idx, err := k.keymap.ModIndex("Control")
230+ require.NoError(t, err)
231+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
232+ ev := press(keyU)
233+ require.False(t, ev.Secret)
234+ require.NotZero(t, ev.Keysym, "Control combinations keep their keysym")
235+ require.Equal(t, ModCtrl, ev.Modifiers)
236+ require.Equal(t, 1, buf.Len())
237+ require.NoError(t, k.setMask(0, 0, 0, 0))
238+
239+ // Capacity: extra text is dropped, the key is still delivered.
240+ for range 10 {
241+ press(keyQ)
242+ }
243+ require.Equal(t, 8, buf.Len())
244+
245+ // Scratch and the work area are wiped after use.
246+ require.Equal(t, make([]byte, scratchSize), k.scratch[:])
247+ require.Equal(t, secretWork{}, k.w)
248+ for _, h := range k.held {
249+ require.Zero(t, h, "no stale held key")
250+ }
251+
252+ k.setSecret(nil)
253+ ev, _ = pressKey(t, k, keyQ)
254+ require.False(t, ev.Secret)
255+ require.Equal(t, "q", string(ev.Text), "ordinary text again once the buffer is removed")
256+}
257+
258+func TestSecretChangedFlag(t *testing.T) {
259+ k := testKeyboard(t, "C", "us")
260+ buf := NewSecretBuffer(2)
261+ k.setSecret(buf)
262+ pressKey(t, k, keyQ)
263+ require.True(t, k.changed)
264+ pressKey(t, k, keyEnter)
265+ require.False(t, k.changed)
266+ pressKey(t, k, keyBackspace)
267+ require.True(t, k.changed)
268+ pressKey(t, k, keyBackspace)
269+ require.False(t, k.changed, "nothing left to remove")
270+}
271+
272+func TestSecretRepeat(t *testing.T) {
273+ k := testKeyboard(t, "C", "us")
274+ buf := NewSecretBuffer(16)
275+ k.setSecret(buf)
276+ k.setRepeat(100, 1)
277+ var ev KeyEvent
278+ pressKey(t, k, keyQ)
279+ for range 2 {
280+ require.Eventually(t, k.expired, 2e9, 1e6)
281+ ok, err := k.repeat(&ev)
282+ require.NoError(t, err)
283+ require.True(t, ok)
284+ require.Nil(t, ev.Text)
285+ }
286+ require.Equal(t, "qqq", string(buf.Bytes()))
287+
288+ // Enter cancels a previous printable repeat.
289+ pressKey(t, k, keyEnter)
290+ require.Zero(t, k.repeating)
291+ releaseKey(k, keyQ)
292+ releaseKey(k, keyEnter)
293+
294+ // Held backspace repeats.
295+ pressKey(t, k, keyBackspace)
296+ require.Equal(t, uint32(keyBackspace), k.repeating)
297+ require.Eventually(t, k.expired, 2e9, 1e6)
298+ ok, err := k.repeat(&ev)
299+ require.NoError(t, err)
300+ require.True(t, ok)
301+ require.Equal(t, 1, buf.Len())
302+}
303+
304+func TestSecretRepeatIsHiddenToo(t *testing.T) {
305+ k := testKeyboard(t, "C", "us")
306+ k.setSecret(NewSecretBuffer(8))
307+ k.setRepeat(100, 1)
308+ pressKey(t, k, keyQ)
309+ require.Eventually(t, k.expired, 2e9, 1e6)
310+ var ev KeyEvent
311+ ok, err := k.repeat(&ev)
312+ require.NoError(t, err)
313+ require.True(t, ok)
314+ require.True(t, ev.Secret)
315+ require.Zero(t, ev.Keycode)
316+ require.Zero(t, ev.Keysym)
317+}
318+
319+func TestKeyboardHeldTailAndResetAreCleared(t *testing.T) {
320+ k := testKeyboard(t, "C", "us")
321+ k.setSecret(NewSecretBuffer(8))
322+ pressKey(t, k, keyQ)
323+ pressKey(t, k, keyE)
324+ releaseKey(k, keyQ)
325+ require.Equal(t, heldKey{}, k.held[:cap(k.held)][1], "vacated slot cleared")
326+ k.setFocus(false)
327+ for _, h := range k.held[:cap(k.held)] {
328+ require.Zero(t, h)
329+ }
330+}
331+
332+func TestKeyboardIdenticalMaskIsNoOp(t *testing.T) {
333+ k := testKeyboard(t, "C", "us")
334+ idx, err := k.keymap.ModIndex("Shift")
335+ require.NoError(t, err)
336+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
337+ require.Equal(t, ModShift, k.mods)
338+ require.NoError(t, k.setMask(1<<idx, 0, 0, 0))
339+ require.Equal(t, ModShift, k.mods)
340+ require.NoError(t, k.setMask(0, 0, 0, 0))
341+ require.Zero(t, k.mods)
342+}
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 +22 -2
@@ -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() {
@@ -150,7 +170,7 @@ func (q *queue) pop(ev *event) bool {
150170 return false
151171 }
152172 *ev = q.ring[q.head]
153- q.ring[q.head].kind = 0 // ownership of a carried descriptor moved to ev
173+ q.ring[q.head] = event{} // ownership of a carried descriptor moved to ev; leave no key data behind
154174 q.head = (q.head + 1) % ringSize
155175 full := q.n == ringSize
156176 q.n--
@@ -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() {
@@ -150,7 +170,7 @@ func (q *queue) pop(ev *event) bool {
150 return false170 return false
151 }171 }
152 *ev = q.ring[q.head]172 *ev = q.ring[q.head]
153- q.ring[q.head].kind = 0 // ownership of a carried descriptor moved to ev173+ q.ring[q.head] = event{} // ownership of a carried descriptor moved to ev; leave no key data behind
154 q.head = (q.head + 1) % ringSize174 q.head = (q.head + 1) % ringSize
155 full := q.n == ringSize175 full := q.n == ringSize
156 q.n--176 q.n--
A seat.go +614 -0
@@ -0,0 +1,614 @@
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 highest wl_seat version bound. Version 5 adds the
14+// wl_seat.release request and wl_pointer.frame, 8 adds wl_pointer.axis_value120.
15+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; 0 when Secret is set
99+ Keysym uint32 // xkb keysym in the current layout and modifier state; 0 when Secret is set
100+ Modifiers Modifiers
101+ Group uint32 // effective layout index
102+ Pressed bool
103+ Repeat bool
104+ // Secret is set while a [SecretBuffer] is installed, for the press,
105+ // repeats and release of a key that produced text (or is a dead or compose
106+ // key): the text went into the buffer, and Keycode and Keysym are 0 so the
107+ // event does not reveal which character was typed. Other keys (Enter,
108+ // Escape, BackSpace, modifiers, Control combinations) keep their keysym.
109+ Secret bool
110+ // Text is the UTF-8 text of the key, nil for releases and keys without
111+ // text. It aliases library storage that is wiped when Handler.Key
112+ // returns. It is always nil while a [SecretBuffer] is installed.
113+ Text []byte
114+}
115+
116+// Seat is the connection's first wl_seat: pointer, keyboard and cursor. It
117+// belongs to the owner goroutine. Without a seat global every method is a
118+// no-op. The seat is bound once, at first use: a seat announced later is not
119+// picked up, and a removed one is not replaced (no seat hotplug).
120+type Seat struct {
121+ c *Conn
122+ proxy *wayland.Seat
123+ name uint32 // registry name of the global
124+ ver uint32
125+
126+ pointer *wayland.Pointer
127+ ptrGen uint32 // generation of pointer; bumped on every create and drop
128+ ptrSurf SurfaceID
129+ px, py float64
130+ pev PointerEvent
131+
132+ keyboard *wayland.Keyboard
133+ kbGen uint32
134+ kbSurf SurfaceID
135+ kb *keyboard
136+ kev KeyEvent
137+
138+ secret *SecretBuffer
139+
140+ shapes *cursorshape.WpCursorShapeManager
141+ shapesVer uint32
142+ shapeDev *cursorshape.WpCursorShapeDevice
143+ enterSerial uint32 // latest pointer enter serial, 0 while outside
144+ shape CursorShape
145+}
146+
147+// Seat returns the connection's seat (nil after Close), binding wl_seat on first use. The seat
148+// is also bound when the first surface is created. Input starts flowing to the
149+// Handler once the compositor announces the capabilities.
150+func (c *Conn) Seat() *Seat {
151+ c.ensureSeat()
152+ return c.seat
153+}
154+
155+// ensureSeat binds wl_seat and the optional cursor-shape manager once. A
156+// failure is reported by the next Dispatch through Handler.Error.
157+func (c *Conn) ensureSeat() {
158+ if c.seat != nil || c.closed {
159+ return
160+ }
161+ s := &Seat{c: c}
162+ c.seat = s
163+ g, ok := c.display.Registry().FindGlobal(wayland.SeatInterface)
164+ if !ok {
165+ return
166+ }
167+ q := c.q
168+ proxy, v, err := c.Bind(wayland.SeatInterface, maxSeatVersion, func(ctx *wl.Context) *wayland.Seat {
169+ p := wayland.NewSeat(ctx)
170+ p.OnCapabilities(func(caps uint32) {
171+ e := event{kind: evSeatCaps, a: int32(caps)}
172+ q.post(&e)
173+ })
174+ return p
175+ })
176+ if err != nil {
177+ c.seatErr = fmt.Errorf("neferclient: bind wl_seat: %w", err)
178+ return
179+ }
180+ s.proxy, s.name, s.ver = proxy, g.Name, v
181+ m, mv, err := c.Bind(cursorshape.WpCursorShapeManagerInterface, 2, cursorshape.NewWpCursorShapeManager)
182+ if err != nil {
183+ if !errors.Is(err, ErrGlobalNotFound) {
184+ c.seatErr = fmt.Errorf("neferclient: bind cursor shape manager: %w", err)
185+ }
186+ return
187+ }
188+ s.shapes, s.shapesVer = m, mv
189+}
190+
191+// SetSecret installs b to receive typed text, or removes it with nil. While
192+// installed, printable text goes only into b and never into KeyEvent.Text;
193+// BackSpace removes one code point; every other key is still delivered as a
194+// KeyEvent without text. Keys that produced text are delivered with
195+// KeyEvent.Secret set and Keycode and Keysym 0, for the press, repeats and
196+// release; Enter, Escape, BackSpace and Control combinations keep their
197+// keysym. Installing or removing cancels held keys and repeat. The previous
198+// buffer is not wiped: that is the caller's call.
199+//
200+// Limits: runtime/secret (GOEXPERIMENT=runtimesecret) erases Go registers,
201+// stack and heap temporaries, but not the native memory of libxkbcommon, which
202+// processes the key text.
203+func (s *Seat) SetSecret(b *SecretBuffer) {
204+ if s == nil {
205+ return
206+ }
207+ s.secret = b
208+ if s.kb != nil {
209+ s.kb.setSecret(b)
210+ }
211+}
212+
213+// SetCursor asks the compositor to show shape for the pointer. It is a no-op
214+// when the compositor has no cursor-shape support, the pointer is outside the
215+// connection's surfaces, or shape is unchanged since the last enter.
216+func (s *Seat) SetCursor(shape CursorShape) error {
217+ if s == nil || s.c.closed {
218+ return ErrClosed
219+ }
220+ if shape < CursorDefault || shape > CursorAllResize {
221+ return fmt.Errorf("neferclient: invalid cursor shape %d", shape)
222+ }
223+ if shape >= CursorDndAsk && s.shapes != nil && s.shapesVer < 2 {
224+ return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}
225+ }
226+ if s.shapes == nil || s.pointer == nil || s.ptrSurf == 0 || shape == s.shape {
227+ return nil
228+ }
229+ if s.shapeDev == nil {
230+ dev, err := s.shapes.GetPointer(s.pointer)
231+ if err != nil {
232+ return fmt.Errorf("neferclient: cursor shape device: %w", err)
233+ }
234+ s.shapeDev = dev
235+ }
236+ if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {
237+ return fmt.Errorf("neferclient: set cursor shape: %w", err)
238+ }
239+ s.shape = shape
240+ return nil
241+}
242+
243+// surfaceByObject finds the surface whose wl_surface has object id obj.
244+func (c *Conn) surfaceByObject(obj uint32) *Surface {
245+ for _, s := range c.surfaces {
246+ if !s.closed && s.surf != nil && s.surf.ID() == obj {
247+ return s
248+ }
249+ }
250+ return nil
251+}
252+
253+func (c *Conn) seatTimer(fd int32) bool {
254+ return c.seat != nil && c.seat.kb != nil && int(fd) == c.seat.kb.tfd
255+}
256+
257+// seatRepeat handles the repeat timer: at most one repeat per expiration.
258+func (c *Conn) seatRepeat(h Handler) {
259+ s := c.seat
260+ if s.kb.expired() {
261+ ke := &s.kev
262+ *ke = KeyEvent{Surface: s.kbSurf}
263+ ok, err := s.kb.repeat(ke)
264+ switch {
265+ case err != nil:
266+ h.Error(err)
267+ case ok:
268+ s.deliverKey(ke, h)
269+ }
270+ }
271+ if err := s.kb.takeTimerErr(); err != nil {
272+ h.Error(err)
273+ }
274+ if !c.closed {
275+ if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {
276+ h.Error(err)
277+ }
278+ }
279+}
280+
281+func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {
282+ h.Key(ke)
283+ clear(s.kb.scratch[:]) // ordinary text must not outlive the call
284+ *ke = KeyEvent{}
285+ if s.kb.changed {
286+ h.SecretChanged(s.secret.Len())
287+ }
288+}
289+
290+// seatSurfaceGone forgets focus on a closing surface.
291+func (c *Conn) seatSurfaceGone(sf *Surface) {
292+ s := c.seat
293+ if s == nil {
294+ return
295+ }
296+ if s.ptrSurf == sf.id {
297+ s.ptrSurf = 0
298+ s.enterSerial, s.shape = 0, 0
299+ }
300+ if s.kbSurf == sf.id {
301+ s.kbSurf = 0
302+ if s.kb != nil {
303+ s.kb.setFocus(false)
304+ }
305+ }
306+}
307+
308+func (c *Conn) seatGlobalRemoved(global uint32, h Handler) {
309+ if s := c.seat; s != nil && s.proxy != nil && s.name == global {
310+ s.release(h)
311+ }
312+}
313+
314+// release drops the devices and the seat proxy.
315+func (s *Seat) release(h Handler) {
316+ s.dropPointer(h)
317+ s.dropKeyboard(h)
318+ if s.ver >= 5 {
319+ _ = s.proxy.Release()
320+ } else {
321+ s.c.wlctx.Unregister(s.proxy)
322+ }
323+ s.proxy = nil
324+}
325+
326+func releaseDevice(ver uint32, c *wl.Context, p interface {
327+ wl.Proxy
328+ Release() error
329+}) {
330+ if ver >= 3 {
331+ _ = p.Release()
332+ } else {
333+ c.Unregister(p)
334+ }
335+}
336+
337+func (s *Seat) dropPointer(h Handler) {
338+ if s.pointer == nil {
339+ return
340+ }
341+ if s.shapeDev != nil {
342+ _ = s.shapeDev.Destroy()
343+ s.shapeDev = nil
344+ }
345+ s.enterSerial, s.shape = 0, 0
346+ releaseDevice(s.ver, s.c.wlctx, s.pointer)
347+ s.pointer = nil
348+ s.ptrGen++
349+ if id := s.ptrSurf; id != 0 {
350+ s.ptrSurf = 0
351+ pe := &s.pev
352+ *pe = PointerEvent{Surface: id, Kind: PointerLeave, X: s.px, Y: s.py}
353+ h.Pointer(pe)
354+ }
355+}
356+
357+func (s *Seat) dropKeyboard(h Handler) {
358+ if s.keyboard == nil {
359+ return
360+ }
361+ releaseDevice(s.ver, s.c.wlctx, s.keyboard)
362+ s.keyboard = nil
363+ s.kbGen++
364+ if id := s.kbSurf; id != 0 {
365+ s.kbSurf = 0
366+ s.kb.setFocus(false)
367+ h.KeyboardFocus(id, false)
368+ } else if s.kb != nil {
369+ s.kb.setFocus(false)
370+ }
371+}
372+
373+func (s *Seat) newPointer() error {
374+ c := s.c
375+ c.pause() // handlers go on after the request, before any event is read
376+ defer c.resume()
377+ p, err := s.proxy.GetPointer()
378+ if err != nil {
379+ return fmt.Errorf("neferclient: get wl_pointer: %w", err)
380+ }
381+ s.pointer = p
382+ s.ptrGen++
383+ gen, q := s.ptrGen, c.q
384+ post := func(e *event) {
385+ e.version = gen
386+ q.post(e)
387+ }
388+ p.OnEnter(func(serial, surf uint32, x, y wl.Fixed) {
389+ post(&event{kind: evPtrEnter, global: surf, serial: serial, a: int32(x), b: int32(y)})
390+ })
391+ p.OnLeave(func(_, surf uint32) { post(&event{kind: evPtrLeave, global: surf}) })
392+ p.OnMotion(func(_ uint32, x, y wl.Fixed) { post(&event{kind: evPtrMotion, a: int32(x), b: int32(y)}) })
393+ p.OnButton(func(serial, _, button, state uint32) {
394+ post(&event{kind: evPtrButton, serial: serial, flags: button, a: int32(state)})
395+ })
396+ p.OnAxis(func(_, axis uint32, v wl.Fixed) { post(&event{kind: evPtrAxis, flags: axis, a: int32(v)}) })
397+ p.OnAxisValue120(func(axis uint32, v int32) { post(&event{kind: evPtrAxisValue, flags: axis, a: v}) })
398+ p.OnAxisStop(func(_, axis uint32) { post(&event{kind: evPtrAxisStop, flags: axis}) })
399+ return nil
400+}
401+
402+func (s *Seat) newKeyboard() error {
403+ c := s.c
404+ if s.kb == nil {
405+ kb, err := newKeyboard(composeLocale())
406+ if err != nil {
407+ return fmt.Errorf("neferclient: keyboard: %w", err)
408+ }
409+ if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {
410+ kb.close()
411+ return err // armFD wraps it
412+ }
413+ kb.secret = s.secret
414+ s.kb = kb
415+ }
416+ c.pause()
417+ defer c.resume()
418+ k, err := s.proxy.GetKeyboard()
419+ if err != nil {
420+ return fmt.Errorf("neferclient: get wl_keyboard: %w", err)
421+ }
422+ s.keyboard = k
423+ s.kbGen++
424+ gen, q := s.kbGen, c.q
425+ post := func(e *event) {
426+ e.version = gen
427+ q.post(e)
428+ }
429+ k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
430+ n, err := fd.Take()
431+ if err != nil {
432+ post(&event{kind: evSeatBad, a: badKeymapFD})
433+ return
434+ }
435+ post(&event{kind: evKbKeymap, fd: int32(n), flags: size, a: int32(format)})
436+ })
437+ k.OnEnter(func(serial, surf uint32, _ []byte) { post(&event{kind: evKbEnter, global: surf, serial: serial}) })
438+ k.OnLeave(func(_, surf uint32) { post(&event{kind: evKbLeave, global: surf}) })
439+ k.OnKey(func(serial, _, key, state uint32) {
440+ post(&event{kind: evKbKey, serial: serial, flags: key, a: int32(state)})
441+ })
442+ k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
443+ post(&event{kind: evKbModifiers, flags: depressed, dev: uint64(latched) | uint64(locked)<<32, a: int32(group)})
444+ })
445+ k.OnRepeatInfo(func(rate, delay int32) { post(&event{kind: evKbRepeat, a: rate, b: delay}) })
446+ return nil
447+}
448+
449+// Codes of evSeatBad.
450+const badKeymapFD int32 = 100
451+
452+// applySeat handles the seat events on the owner.
453+func (c *Conn) applySeat(ev *event, h Handler) {
454+ defer ev.closeFD()
455+ s := c.seat
456+ if s == nil {
457+ return
458+ }
459+ switch ev.kind {
460+ case evSeatBad:
461+ h.Error(errors.New("neferclient: keymap descriptor unavailable"))
462+ case evSeatCaps:
463+ s.applyCaps(uint32(ev.a), h)
464+ case evPtrEnter, evPtrLeave, evPtrMotion, evPtrButton, evPtrAxis, evPtrAxisValue, evPtrAxisStop:
465+ if s.pointer != nil && ev.version == s.ptrGen {
466+ s.applyPointer(ev, h)
467+ }
468+ default:
469+ if s.keyboard != nil && ev.version == s.kbGen {
470+ s.applyKeyboard(ev, h)
471+ if err := s.kb.takeTimerErr(); err != nil {
472+ h.Error(err)
473+ }
474+ }
475+ }
476+}
477+
478+func (s *Seat) applyCaps(caps uint32, h Handler) {
479+ if s.proxy == nil {
480+ return
481+ }
482+ if caps&1 == 0 {
483+ s.dropPointer(h)
484+ }
485+ if caps&2 == 0 {
486+ s.dropKeyboard(h)
487+ }
488+ if caps&1 != 0 && s.pointer == nil {
489+ if err := s.newPointer(); err != nil {
490+ h.Error(err)
491+ }
492+ }
493+ if caps&2 != 0 && s.keyboard == nil {
494+ if err := s.newKeyboard(); err != nil {
495+ h.Error(err)
496+ }
497+ }
498+}
499+
500+func (s *Seat) applyPointer(ev *event, h Handler) {
501+ pe := &s.pev
502+ *pe = PointerEvent{}
503+ fixed := func(v int32) float64 { return wl.Fixed(v).Float64() }
504+ switch ev.kind {
505+ case evPtrEnter:
506+ sf := s.c.surfaceByObject(ev.global)
507+ if sf == nil {
508+ return
509+ }
510+ s.ptrSurf = sf.id
511+ s.px, s.py = fixed(ev.a), fixed(ev.b)
512+ s.enterSerial, s.shape = ev.serial, 0 // the compositor forgets the cursor on leave
513+ pe.Kind = PointerEnter
514+ case evPtrLeave:
515+ if sf := s.c.surfaceByObject(ev.global); s.ptrSurf == 0 || sf == nil || sf.id != s.ptrSurf {
516+ return
517+ }
518+ pe.Kind = PointerLeave
519+ pe.Surface = s.ptrSurf
520+ pe.X, pe.Y = s.px, s.py
521+ s.ptrSurf = 0
522+ s.enterSerial, s.shape = 0, 0
523+ h.Pointer(pe)
524+ return
525+ default:
526+ if s.ptrSurf == 0 {
527+ return
528+ }
529+ switch ev.kind {
530+ case evPtrMotion:
531+ s.px, s.py = fixed(ev.a), fixed(ev.b)
532+ pe.Kind = PointerMotion
533+ case evPtrButton:
534+ pe.Kind, pe.Button, pe.Pressed = PointerButton, ev.flags, ev.a == 1
535+ case evPtrAxis:
536+ pe.Kind, pe.Axis = PointerAxis, ev.flags
537+ if ev.flags == AxisVertical {
538+ pe.DY = fixed(ev.a)
539+ } else {
540+ pe.DX = fixed(ev.a)
541+ }
542+ case evPtrAxisValue:
543+ pe.Kind, pe.Axis, pe.Value120 = PointerAxis, ev.flags, ev.a
544+ case evPtrAxisStop:
545+ pe.Kind, pe.Axis = PointerAxisStop, ev.flags
546+ }
547+ }
548+ pe.Surface, pe.X, pe.Y = s.ptrSurf, s.px, s.py
549+ h.Pointer(pe)
550+}
551+
552+func (s *Seat) applyKeyboard(ev *event, h Handler) {
553+ kb := s.kb
554+ switch ev.kind {
555+ case evKbKeymap:
556+ fd := int(ev.fd)
557+ ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again
558+ if ev.a == 0 { // no_keymap: the compositor sends no usable map; nothing to report
559+ _ = unix.Close(fd)
560+ return
561+ }
562+ if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {
563+ _ = unix.Close(fd)
564+ h.Error(fmt.Errorf("neferclient: unsupported keymap format %d or size %d", ev.a, ev.flags))
565+ return
566+ }
567+ if err := kb.replaceFD(fd, int(ev.flags)); err != nil {
568+ h.Error(fmt.Errorf("neferclient: keymap: %w", err))
569+ }
570+ case evKbEnter:
571+ sf := s.c.surfaceByObject(ev.global)
572+ if sf == nil {
573+ return
574+ }
575+ s.kbSurf = sf.id
576+ kb.setFocus(true)
577+ h.KeyboardFocus(sf.id, true)
578+ case evKbLeave:
579+ sf := s.c.surfaceByObject(ev.global)
580+ if sf == nil || s.kbSurf != sf.id {
581+ return
582+ }
583+ s.kbSurf = 0
584+ kb.setFocus(false)
585+ h.KeyboardFocus(sf.id, false)
586+ case evKbKey:
587+ ke := &s.kev
588+ *ke = KeyEvent{Surface: s.kbSurf}
589+ if ev.a == 1 {
590+ ok, err := kb.press(ev.flags, ke)
591+ switch {
592+ case err != nil:
593+ h.Error(err)
594+ case ok:
595+ s.deliverKey(ke, h)
596+ }
597+ } else if kb.release(ev.flags, ke) {
598+ s.deliverKey(ke, h)
599+ }
600+ case evKbModifiers:
601+ if err := kb.setMask(ev.flags, uint32(ev.dev), uint32(ev.dev>>32), uint32(ev.a)); err != nil {
602+ h.Error(err)
603+ }
604+ case evKbRepeat:
605+ kb.setRepeat(ev.a, ev.b)
606+ }
607+}
608+
609+func (c *Conn) closeSeat() {
610+ if s := c.seat; s != nil && s.kb != nil {
611+ s.kb.close()
612+ s.kb = nil
613+ }
614+}
@@ -0,0 +1,614 @@
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 highest wl_seat version bound. Version 5 adds the
14+// wl_seat.release request and wl_pointer.frame, 8 adds wl_pointer.axis_value120.
15+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; 0 when Secret is set
99+ Keysym uint32 // xkb keysym in the current layout and modifier state; 0 when Secret is set
100+ Modifiers Modifiers
101+ Group uint32 // effective layout index
102+ Pressed bool
103+ Repeat bool
104+ // Secret is set while a [SecretBuffer] is installed, for the press,
105+ // repeats and release of a key that produced text (or is a dead or compose
106+ // key): the text went into the buffer, and Keycode and Keysym are 0 so the
107+ // event does not reveal which character was typed. Other keys (Enter,
108+ // Escape, BackSpace, modifiers, Control combinations) keep their keysym.
109+ Secret bool
110+ // Text is the UTF-8 text of the key, nil for releases and keys without
111+ // text. It aliases library storage that is wiped when Handler.Key
112+ // returns. It is always nil while a [SecretBuffer] is installed.
113+ Text []byte
114+}
115+
116+// Seat is the connection's first wl_seat: pointer, keyboard and cursor. It
117+// belongs to the owner goroutine. Without a seat global every method is a
118+// no-op. The seat is bound once, at first use: a seat announced later is not
119+// picked up, and a removed one is not replaced (no seat hotplug).
120+type Seat struct {
121+ c *Conn
122+ proxy *wayland.Seat
123+ name uint32 // registry name of the global
124+ ver uint32
125+
126+ pointer *wayland.Pointer
127+ ptrGen uint32 // generation of pointer; bumped on every create and drop
128+ ptrSurf SurfaceID
129+ px, py float64
130+ pev PointerEvent
131+
132+ keyboard *wayland.Keyboard
133+ kbGen uint32
134+ kbSurf SurfaceID
135+ kb *keyboard
136+ kev KeyEvent
137+
138+ secret *SecretBuffer
139+
140+ shapes *cursorshape.WpCursorShapeManager
141+ shapesVer uint32
142+ shapeDev *cursorshape.WpCursorShapeDevice
143+ enterSerial uint32 // latest pointer enter serial, 0 while outside
144+ shape CursorShape
145+}
146+
147+// Seat returns the connection's seat (nil after Close), binding wl_seat on first use. The seat
148+// is also bound when the first surface is created. Input starts flowing to the
149+// Handler once the compositor announces the capabilities.
150+func (c *Conn) Seat() *Seat {
151+ c.ensureSeat()
152+ return c.seat
153+}
154+
155+// ensureSeat binds wl_seat and the optional cursor-shape manager once. A
156+// failure is reported by the next Dispatch through Handler.Error.
157+func (c *Conn) ensureSeat() {
158+ if c.seat != nil || c.closed {
159+ return
160+ }
161+ s := &Seat{c: c}
162+ c.seat = s
163+ g, ok := c.display.Registry().FindGlobal(wayland.SeatInterface)
164+ if !ok {
165+ return
166+ }
167+ q := c.q
168+ proxy, v, err := c.Bind(wayland.SeatInterface, maxSeatVersion, func(ctx *wl.Context) *wayland.Seat {
169+ p := wayland.NewSeat(ctx)
170+ p.OnCapabilities(func(caps uint32) {
171+ e := event{kind: evSeatCaps, a: int32(caps)}
172+ q.post(&e)
173+ })
174+ return p
175+ })
176+ if err != nil {
177+ c.seatErr = fmt.Errorf("neferclient: bind wl_seat: %w", err)
178+ return
179+ }
180+ s.proxy, s.name, s.ver = proxy, g.Name, v
181+ m, mv, err := c.Bind(cursorshape.WpCursorShapeManagerInterface, 2, cursorshape.NewWpCursorShapeManager)
182+ if err != nil {
183+ if !errors.Is(err, ErrGlobalNotFound) {
184+ c.seatErr = fmt.Errorf("neferclient: bind cursor shape manager: %w", err)
185+ }
186+ return
187+ }
188+ s.shapes, s.shapesVer = m, mv
189+}
190+
191+// SetSecret installs b to receive typed text, or removes it with nil. While
192+// installed, printable text goes only into b and never into KeyEvent.Text;
193+// BackSpace removes one code point; every other key is still delivered as a
194+// KeyEvent without text. Keys that produced text are delivered with
195+// KeyEvent.Secret set and Keycode and Keysym 0, for the press, repeats and
196+// release; Enter, Escape, BackSpace and Control combinations keep their
197+// keysym. Installing or removing cancels held keys and repeat. The previous
198+// buffer is not wiped: that is the caller's call.
199+//
200+// Limits: runtime/secret (GOEXPERIMENT=runtimesecret) erases Go registers,
201+// stack and heap temporaries, but not the native memory of libxkbcommon, which
202+// processes the key text.
203+func (s *Seat) SetSecret(b *SecretBuffer) {
204+ if s == nil {
205+ return
206+ }
207+ s.secret = b
208+ if s.kb != nil {
209+ s.kb.setSecret(b)
210+ }
211+}
212+
213+// SetCursor asks the compositor to show shape for the pointer. It is a no-op
214+// when the compositor has no cursor-shape support, the pointer is outside the
215+// connection's surfaces, or shape is unchanged since the last enter.
216+func (s *Seat) SetCursor(shape CursorShape) error {
217+ if s == nil || s.c.closed {
218+ return ErrClosed
219+ }
220+ if shape < CursorDefault || shape > CursorAllResize {
221+ return fmt.Errorf("neferclient: invalid cursor shape %d", shape)
222+ }
223+ if shape >= CursorDndAsk && s.shapes != nil && s.shapesVer < 2 {
224+ return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}
225+ }
226+ if s.shapes == nil || s.pointer == nil || s.ptrSurf == 0 || shape == s.shape {
227+ return nil
228+ }
229+ if s.shapeDev == nil {
230+ dev, err := s.shapes.GetPointer(s.pointer)
231+ if err != nil {
232+ return fmt.Errorf("neferclient: cursor shape device: %w", err)
233+ }
234+ s.shapeDev = dev
235+ }
236+ if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {
237+ return fmt.Errorf("neferclient: set cursor shape: %w", err)
238+ }
239+ s.shape = shape
240+ return nil
241+}
242+
243+// surfaceByObject finds the surface whose wl_surface has object id obj.
244+func (c *Conn) surfaceByObject(obj uint32) *Surface {
245+ for _, s := range c.surfaces {
246+ if !s.closed && s.surf != nil && s.surf.ID() == obj {
247+ return s
248+ }
249+ }
250+ return nil
251+}
252+
253+func (c *Conn) seatTimer(fd int32) bool {
254+ return c.seat != nil && c.seat.kb != nil && int(fd) == c.seat.kb.tfd
255+}
256+
257+// seatRepeat handles the repeat timer: at most one repeat per expiration.
258+func (c *Conn) seatRepeat(h Handler) {
259+ s := c.seat
260+ if s.kb.expired() {
261+ ke := &s.kev
262+ *ke = KeyEvent{Surface: s.kbSurf}
263+ ok, err := s.kb.repeat(ke)
264+ switch {
265+ case err != nil:
266+ h.Error(err)
267+ case ok:
268+ s.deliverKey(ke, h)
269+ }
270+ }
271+ if err := s.kb.takeTimerErr(); err != nil {
272+ h.Error(err)
273+ }
274+ if !c.closed {
275+ if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {
276+ h.Error(err)
277+ }
278+ }
279+}
280+
281+func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {
282+ h.Key(ke)
283+ clear(s.kb.scratch[:]) // ordinary text must not outlive the call
284+ *ke = KeyEvent{}
285+ if s.kb.changed {
286+ h.SecretChanged(s.secret.Len())
287+ }
288+}
289+
290+// seatSurfaceGone forgets focus on a closing surface.
291+func (c *Conn) seatSurfaceGone(sf *Surface) {
292+ s := c.seat
293+ if s == nil {
294+ return
295+ }
296+ if s.ptrSurf == sf.id {
297+ s.ptrSurf = 0
298+ s.enterSerial, s.shape = 0, 0
299+ }
300+ if s.kbSurf == sf.id {
301+ s.kbSurf = 0
302+ if s.kb != nil {
303+ s.kb.setFocus(false)
304+ }
305+ }
306+}
307+
308+func (c *Conn) seatGlobalRemoved(global uint32, h Handler) {
309+ if s := c.seat; s != nil && s.proxy != nil && s.name == global {
310+ s.release(h)
311+ }
312+}
313+
314+// release drops the devices and the seat proxy.
315+func (s *Seat) release(h Handler) {
316+ s.dropPointer(h)
317+ s.dropKeyboard(h)
318+ if s.ver >= 5 {
319+ _ = s.proxy.Release()
320+ } else {
321+ s.c.wlctx.Unregister(s.proxy)
322+ }
323+ s.proxy = nil
324+}
325+
326+func releaseDevice(ver uint32, c *wl.Context, p interface {
327+ wl.Proxy
328+ Release() error
329+}) {
330+ if ver >= 3 {
331+ _ = p.Release()
332+ } else {
333+ c.Unregister(p)
334+ }
335+}
336+
337+func (s *Seat) dropPointer(h Handler) {
338+ if s.pointer == nil {
339+ return
340+ }
341+ if s.shapeDev != nil {
342+ _ = s.shapeDev.Destroy()
343+ s.shapeDev = nil
344+ }
345+ s.enterSerial, s.shape = 0, 0
346+ releaseDevice(s.ver, s.c.wlctx, s.pointer)
347+ s.pointer = nil
348+ s.ptrGen++
349+ if id := s.ptrSurf; id != 0 {
350+ s.ptrSurf = 0
351+ pe := &s.pev
352+ *pe = PointerEvent{Surface: id, Kind: PointerLeave, X: s.px, Y: s.py}
353+ h.Pointer(pe)
354+ }
355+}
356+
357+func (s *Seat) dropKeyboard(h Handler) {
358+ if s.keyboard == nil {
359+ return
360+ }
361+ releaseDevice(s.ver, s.c.wlctx, s.keyboard)
362+ s.keyboard = nil
363+ s.kbGen++
364+ if id := s.kbSurf; id != 0 {
365+ s.kbSurf = 0
366+ s.kb.setFocus(false)
367+ h.KeyboardFocus(id, false)
368+ } else if s.kb != nil {
369+ s.kb.setFocus(false)
370+ }
371+}
372+
373+func (s *Seat) newPointer() error {
374+ c := s.c
375+ c.pause() // handlers go on after the request, before any event is read
376+ defer c.resume()
377+ p, err := s.proxy.GetPointer()
378+ if err != nil {
379+ return fmt.Errorf("neferclient: get wl_pointer: %w", err)
380+ }
381+ s.pointer = p
382+ s.ptrGen++
383+ gen, q := s.ptrGen, c.q
384+ post := func(e *event) {
385+ e.version = gen
386+ q.post(e)
387+ }
388+ p.OnEnter(func(serial, surf uint32, x, y wl.Fixed) {
389+ post(&event{kind: evPtrEnter, global: surf, serial: serial, a: int32(x), b: int32(y)})
390+ })
391+ p.OnLeave(func(_, surf uint32) { post(&event{kind: evPtrLeave, global: surf}) })
392+ p.OnMotion(func(_ uint32, x, y wl.Fixed) { post(&event{kind: evPtrMotion, a: int32(x), b: int32(y)}) })
393+ p.OnButton(func(serial, _, button, state uint32) {
394+ post(&event{kind: evPtrButton, serial: serial, flags: button, a: int32(state)})
395+ })
396+ p.OnAxis(func(_, axis uint32, v wl.Fixed) { post(&event{kind: evPtrAxis, flags: axis, a: int32(v)}) })
397+ p.OnAxisValue120(func(axis uint32, v int32) { post(&event{kind: evPtrAxisValue, flags: axis, a: v}) })
398+ p.OnAxisStop(func(_, axis uint32) { post(&event{kind: evPtrAxisStop, flags: axis}) })
399+ return nil
400+}
401+
402+func (s *Seat) newKeyboard() error {
403+ c := s.c
404+ if s.kb == nil {
405+ kb, err := newKeyboard(composeLocale())
406+ if err != nil {
407+ return fmt.Errorf("neferclient: keyboard: %w", err)
408+ }
409+ if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {
410+ kb.close()
411+ return err // armFD wraps it
412+ }
413+ kb.secret = s.secret
414+ s.kb = kb
415+ }
416+ c.pause()
417+ defer c.resume()
418+ k, err := s.proxy.GetKeyboard()
419+ if err != nil {
420+ return fmt.Errorf("neferclient: get wl_keyboard: %w", err)
421+ }
422+ s.keyboard = k
423+ s.kbGen++
424+ gen, q := s.kbGen, c.q
425+ post := func(e *event) {
426+ e.version = gen
427+ q.post(e)
428+ }
429+ k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
430+ n, err := fd.Take()
431+ if err != nil {
432+ post(&event{kind: evSeatBad, a: badKeymapFD})
433+ return
434+ }
435+ post(&event{kind: evKbKeymap, fd: int32(n), flags: size, a: int32(format)})
436+ })
437+ k.OnEnter(func(serial, surf uint32, _ []byte) { post(&event{kind: evKbEnter, global: surf, serial: serial}) })
438+ k.OnLeave(func(_, surf uint32) { post(&event{kind: evKbLeave, global: surf}) })
439+ k.OnKey(func(serial, _, key, state uint32) {
440+ post(&event{kind: evKbKey, serial: serial, flags: key, a: int32(state)})
441+ })
442+ k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
443+ post(&event{kind: evKbModifiers, flags: depressed, dev: uint64(latched) | uint64(locked)<<32, a: int32(group)})
444+ })
445+ k.OnRepeatInfo(func(rate, delay int32) { post(&event{kind: evKbRepeat, a: rate, b: delay}) })
446+ return nil
447+}
448+
449+// Codes of evSeatBad.
450+const badKeymapFD int32 = 100
451+
452+// applySeat handles the seat events on the owner.
453+func (c *Conn) applySeat(ev *event, h Handler) {
454+ defer ev.closeFD()
455+ s := c.seat
456+ if s == nil {
457+ return
458+ }
459+ switch ev.kind {
460+ case evSeatBad:
461+ h.Error(errors.New("neferclient: keymap descriptor unavailable"))
462+ case evSeatCaps:
463+ s.applyCaps(uint32(ev.a), h)
464+ case evPtrEnter, evPtrLeave, evPtrMotion, evPtrButton, evPtrAxis, evPtrAxisValue, evPtrAxisStop:
465+ if s.pointer != nil && ev.version == s.ptrGen {
466+ s.applyPointer(ev, h)
467+ }
468+ default:
469+ if s.keyboard != nil && ev.version == s.kbGen {
470+ s.applyKeyboard(ev, h)
471+ if err := s.kb.takeTimerErr(); err != nil {
472+ h.Error(err)
473+ }
474+ }
475+ }
476+}
477+
478+func (s *Seat) applyCaps(caps uint32, h Handler) {
479+ if s.proxy == nil {
480+ return
481+ }
482+ if caps&1 == 0 {
483+ s.dropPointer(h)
484+ }
485+ if caps&2 == 0 {
486+ s.dropKeyboard(h)
487+ }
488+ if caps&1 != 0 && s.pointer == nil {
489+ if err := s.newPointer(); err != nil {
490+ h.Error(err)
491+ }
492+ }
493+ if caps&2 != 0 && s.keyboard == nil {
494+ if err := s.newKeyboard(); err != nil {
495+ h.Error(err)
496+ }
497+ }
498+}
499+
500+func (s *Seat) applyPointer(ev *event, h Handler) {
501+ pe := &s.pev
502+ *pe = PointerEvent{}
503+ fixed := func(v int32) float64 { return wl.Fixed(v).Float64() }
504+ switch ev.kind {
505+ case evPtrEnter:
506+ sf := s.c.surfaceByObject(ev.global)
507+ if sf == nil {
508+ return
509+ }
510+ s.ptrSurf = sf.id
511+ s.px, s.py = fixed(ev.a), fixed(ev.b)
512+ s.enterSerial, s.shape = ev.serial, 0 // the compositor forgets the cursor on leave
513+ pe.Kind = PointerEnter
514+ case evPtrLeave:
515+ if sf := s.c.surfaceByObject(ev.global); s.ptrSurf == 0 || sf == nil || sf.id != s.ptrSurf {
516+ return
517+ }
518+ pe.Kind = PointerLeave
519+ pe.Surface = s.ptrSurf
520+ pe.X, pe.Y = s.px, s.py
521+ s.ptrSurf = 0
522+ s.enterSerial, s.shape = 0, 0
523+ h.Pointer(pe)
524+ return
525+ default:
526+ if s.ptrSurf == 0 {
527+ return
528+ }
529+ switch ev.kind {
530+ case evPtrMotion:
531+ s.px, s.py = fixed(ev.a), fixed(ev.b)
532+ pe.Kind = PointerMotion
533+ case evPtrButton:
534+ pe.Kind, pe.Button, pe.Pressed = PointerButton, ev.flags, ev.a == 1
535+ case evPtrAxis:
536+ pe.Kind, pe.Axis = PointerAxis, ev.flags
537+ if ev.flags == AxisVertical {
538+ pe.DY = fixed(ev.a)
539+ } else {
540+ pe.DX = fixed(ev.a)
541+ }
542+ case evPtrAxisValue:
543+ pe.Kind, pe.Axis, pe.Value120 = PointerAxis, ev.flags, ev.a
544+ case evPtrAxisStop:
545+ pe.Kind, pe.Axis = PointerAxisStop, ev.flags
546+ }
547+ }
548+ pe.Surface, pe.X, pe.Y = s.ptrSurf, s.px, s.py
549+ h.Pointer(pe)
550+}
551+
552+func (s *Seat) applyKeyboard(ev *event, h Handler) {
553+ kb := s.kb
554+ switch ev.kind {
555+ case evKbKeymap:
556+ fd := int(ev.fd)
557+ ev.fd = -1 // replaceFD consumes fd; the deferred closeFD must not touch the number again
558+ if ev.a == 0 { // no_keymap: the compositor sends no usable map; nothing to report
559+ _ = unix.Close(fd)
560+ return
561+ }
562+ if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {
563+ _ = unix.Close(fd)
564+ h.Error(fmt.Errorf("neferclient: unsupported keymap format %d or size %d", ev.a, ev.flags))
565+ return
566+ }
567+ if err := kb.replaceFD(fd, int(ev.flags)); err != nil {
568+ h.Error(fmt.Errorf("neferclient: keymap: %w", err))
569+ }
570+ case evKbEnter:
571+ sf := s.c.surfaceByObject(ev.global)
572+ if sf == nil {
573+ return
574+ }
575+ s.kbSurf = sf.id
576+ kb.setFocus(true)
577+ h.KeyboardFocus(sf.id, true)
578+ case evKbLeave:
579+ sf := s.c.surfaceByObject(ev.global)
580+ if sf == nil || s.kbSurf != sf.id {
581+ return
582+ }
583+ s.kbSurf = 0
584+ kb.setFocus(false)
585+ h.KeyboardFocus(sf.id, false)
586+ case evKbKey:
587+ ke := &s.kev
588+ *ke = KeyEvent{Surface: s.kbSurf}
589+ if ev.a == 1 {
590+ ok, err := kb.press(ev.flags, ke)
591+ switch {
592+ case err != nil:
593+ h.Error(err)
594+ case ok:
595+ s.deliverKey(ke, h)
596+ }
597+ } else if kb.release(ev.flags, ke) {
598+ s.deliverKey(ke, h)
599+ }
600+ case evKbModifiers:
601+ if err := kb.setMask(ev.flags, uint32(ev.dev), uint32(ev.dev>>32), uint32(ev.a)); err != nil {
602+ h.Error(err)
603+ }
604+ case evKbRepeat:
605+ kb.setRepeat(ev.a, ev.b)
606+ }
607+}
608+
609+func (c *Conn) closeSeat() {
610+ if s := c.seat; s != nil && s.kb != nil {
611+ s.kb.close()
612+ s.kb = nil
613+ }
614+}
A seat_wire_test.go +506 -0
@@ -0,0 +1,506 @@
1+package neferclient_test
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "runtime"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+ "golang.org/x/sys/unix"
14+
15+ "github.com/bnema/neferclient"
16+ neferclientmocks "github.com/bnema/neferclient/mocks"
17+)
18+
19+const (
20+ seatGlobal = 8
21+ shapesGlobal = 9
22+)
23+
24+var seatGlobals = append(append([]wireGlobal{}, surfaceGlobals...),
25+ wireGlobal{seatGlobal, "wl_seat", 9}, wireGlobal{shapesGlobal, "wp_cursor_shape_manager_v1", 2})
26+
27+// seatPeer is the compositor side of a seat: it learns the object ids the
28+// client creates and scripts input events.
29+type seatPeer struct {
30+ t *testing.T
31+ srv *wireServer
32+
33+ mu sync.Mutex
34+ pointer, keyboard, shape uint32
35+ shapeSerial, shapeValue uint32
36+ shapeSets int
37+ pointerReleased bool
38+}
39+
40+func newSeatPeer(t *testing.T, srv *wireServer) *seatPeer {
41+ p := &seatPeer{t: t, srv: srv}
42+ srv.mu.Lock()
43+ srv.hook = func(obj uint32, op uint16, body []byte) {
44+ srv.mu.Lock()
45+ seat, shapes := srv.bound[seatGlobal].id, srv.bound[shapesGlobal].id
46+ srv.mu.Unlock()
47+ p.mu.Lock()
48+ defer p.mu.Unlock()
49+ switch {
50+ case obj == seat && seat != 0 && op == 0 && len(body) == 4:
51+ p.pointer = binary.LittleEndian.Uint32(body)
52+ case obj == seat && seat != 0 && op == 1 && len(body) == 4:
53+ p.keyboard = binary.LittleEndian.Uint32(body)
54+ case obj == shapes && shapes != 0 && op == 1 && len(body) == 8:
55+ p.shape = binary.LittleEndian.Uint32(body)
56+ case obj == p.shape && p.shape != 0 && op == 1 && len(body) == 8:
57+ p.shapeSerial = binary.LittleEndian.Uint32(body)
58+ p.shapeValue = binary.LittleEndian.Uint32(body[4:])
59+ p.shapeSets++
60+ case obj == p.pointer && p.pointer != 0 && op == 1:
61+ p.pointerReleased = true
62+ }
63+ }
64+ srv.mu.Unlock()
65+ return p
66+}
67+
68+func (p *seatPeer) ids() (pointer, keyboard uint32) {
69+ p.mu.Lock()
70+ defer p.mu.Unlock()
71+ return p.pointer, p.keyboard
72+}
73+
74+func (p *seatPeer) seatID() uint32 {
75+ p.srv.mu.Lock()
76+ defer p.srv.mu.Unlock()
77+ return p.srv.bound[seatGlobal].id
78+}
79+
80+func fixed(v float64) uint32 { return uint32(int32(v * 256)) }
81+
82+func negU32(v int32) uint32 { return uint32(-v) }
83+
84+func (p *seatPeer) capabilities(caps uint32) {
85+ p.srv.write(frame(p.seatID(), 0, appendU32(nil, caps)))
86+}
87+
88+// keymap sends wl_keyboard.keymap with testdata/fr.xkb in a descriptor.
89+func (p *seatPeer) keymap(keyboard uint32, format uint32) {
90+ p.t.Helper()
91+ data, err := os.ReadFile("testdata/fr.xkb")
92+ require.NoError(p.t, err)
93+ data = append(data, 0)
94+ fd, err := unix.MemfdCreate("keymap", unix.MFD_CLOEXEC)
95+ require.NoError(p.t, err)
96+ defer unix.Close(fd)
97+ _, err = unix.Write(fd, data)
98+ require.NoError(p.t, err)
99+ body := appendU32(nil, format)
100+ body = appendU32(body, uint32(len(data)))
101+ _, _, err = p.srv.conn.WriteMsgUnix(frame(keyboard, 0, body), unix.UnixRights(fd), nil)
102+ require.NoError(p.t, err)
103+}
104+
105+func (p *seatPeer) keyboardEnter(kb, surface uint32) {
106+ body := appendU32(appendU32(nil, 1), surface)
107+ body = appendU32(body, 0) // empty keys array
108+ p.srv.write(frame(kb, 1, body))
109+}
110+
111+func (p *seatPeer) key(kb, code, state uint32) []byte {
112+ b := appendU32(appendU32(nil, 1), 0)
113+ b = appendU32(appendU32(b, code), state)
114+ return frame(kb, 3, b)
115+}
116+
117+func seatSetup(t *testing.T) (*neferclient.Conn, *wireServer, *seatPeer, *neferclient.Surface) {
118+ t.Helper()
119+ c, srv := connectWire(t, seatGlobals, surfaceOutputs)
120+ peer := newSeatPeer(t, srv)
121+ s, err := c.NewToplevel("seat", 64, 48)
122+ require.NoError(t, err)
123+ require.NotNil(t, c.Seat())
124+ require.NoError(t, c.Roundtrip())
125+ peer.capabilities(3)
126+ require.NoError(t, c.Roundtrip())
127+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
128+ require.NoError(t, c.Roundtrip())
129+ return c, srv, peer, s
130+}
131+
132+func TestSeatPointerEvents(t *testing.T) {
133+ c, srv, peer, s := seatSetup(t)
134+ pointer, keyboard := peer.ids()
135+ require.NotZero(t, pointer)
136+ require.NotZero(t, keyboard)
137+ surf := s.SurfaceObjectID()
138+
139+ var got []neferclient.PointerEvent
140+ h := neferclientmocks.NewMockHandler(t)
141+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
142+
143+ // A motion outside any surface is ignored; the enter binds the position.
144+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(1)), appendU32(nil, fixed(2))...)))
145+ b := appendU32(appendU32(nil, 77), surf)
146+ b = appendU32(appendU32(b, fixed(10.5)), fixed(20.25))
147+ srv.write(frame(pointer, 0, b)) // enter
148+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(11)), appendU32(nil, fixed(21))...)))
149+ srv.write(frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 78), 0), 0x110), 1))) // button press
150+ srv.write(frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(15)))) // axis vertical
151+ srv.write(frame(pointer, 9, appendU32(appendU32(nil, 1), negU32(120)))) // value120 horizontal
152+ srv.write(frame(pointer, 7, appendU32(appendU32(nil, 0), 0))) // axis stop
153+ srv.write(frame(pointer, 1, appendU32(appendU32(nil, 79), surf))) // leave
154+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(3)), appendU32(nil, fixed(4))...)))
155+ require.NoError(t, c.Roundtrip())
156+ dispatchUntil(t, c, h, func() bool { return len(got) >= 7 })
157+
158+ require.Len(t, got, 7)
159+ id := s.ID()
160+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerEnter, X: 10.5, Y: 20.25}, got[0])
161+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerMotion, X: 11, Y: 21}, got[1])
162+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerButton, X: 11, Y: 21, Button: 0x110, Pressed: true}, got[2])
163+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 0, DY: 15}, got[3])
164+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 1, Value120: -120}, got[4])
165+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxisStop, X: 11, Y: 21}, got[5])
166+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerLeave, X: 11, Y: 21}, got[6])
167+}
168+
169+func TestSeatCursorShape(t *testing.T) {
170+ c, srv, peer, s := seatSetup(t)
171+ pointer, _ := peer.ids()
172+ seat := c.Seat()
173+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer), "outside: no-op")
174+ require.NoError(t, c.Roundtrip())
175+ require.Zero(t, peer.shapeSets)
176+
177+ enter := func(serial uint32) {
178+ b := appendU32(appendU32(nil, serial), s.SurfaceObjectID())
179+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
180+ require.NoError(t, c.Roundtrip())
181+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
182+ }
183+ enter(40)
184+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
185+ require.NoError(t, seat.SetCursor(neferclient.CursorText), "unchanged: nothing sent")
186+ require.NoError(t, c.Roundtrip())
187+ peer.mu.Lock()
188+ require.Equal(t, 1, peer.shapeSets)
189+ require.Equal(t, uint32(40), peer.shapeSerial)
190+ require.Equal(t, uint32(neferclient.CursorText), peer.shapeValue)
191+ peer.mu.Unlock()
192+
193+ enter(41) // a new enter resets the shape and carries a new serial
194+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
195+ require.NoError(t, c.Roundtrip())
196+ peer.mu.Lock()
197+ require.Equal(t, 2, peer.shapeSets)
198+ require.Equal(t, uint32(41), peer.shapeSerial)
199+ peer.mu.Unlock()
200+ require.Error(t, seat.SetCursor(0))
201+ require.Error(t, seat.SetCursor(neferclient.CursorAllResize+1))
202+}
203+
204+func TestSeatKeyboardFocusTextAndSecret(t *testing.T) {
205+ c, srv, peer, s := seatSetup(t)
206+ _, kb := peer.ids()
207+ surf := s.SurfaceObjectID()
208+ seat := c.Seat()
209+
210+ var keys []neferclient.KeyEvent
211+ var texts []string
212+ var focus []bool
213+ var changed []int
214+ h := neferclientmocks.NewMockHandler(t)
215+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
216+ keys = append(keys, *ev)
217+ texts = append(texts, string(ev.Text)) // the only place text is copied: the assertion of the test
218+ }).Return()
219+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
220+ h.EXPECT().SecretChanged(mock.Anything).Run(func(n int) { changed = append(changed, n) }).Return()
221+
222+ peer.keymap(kb, 1) // French AZERTY: evdev 16 is "a"
223+ srv.write(frame(kb, 5, append(appendU32(nil, 0), appendU32(nil, 0)...)))
224+ peer.keyboardEnter(kb, surf)
225+ srv.write(peer.key(kb, 16, 1))
226+ srv.write(peer.key(kb, 16, 0))
227+ require.NoError(t, c.Roundtrip())
228+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2 })
229+ require.Equal(t, []bool{true}, focus)
230+ require.Equal(t, "a", texts[0])
231+ require.True(t, keys[0].Pressed)
232+ require.Equal(t, s.ID(), keys[0].Surface)
233+ require.Equal(t, "", texts[1])
234+ require.False(t, keys[1].Pressed)
235+ require.Empty(t, changed)
236+
237+ // With a secret buffer: the text goes only into it.
238+ buf := neferclient.NewSecretBuffer(8)
239+ seat.SetSecret(buf)
240+ srv.write(peer.key(kb, 16, 1))
241+ srv.write(peer.key(kb, 16, 0))
242+ srv.write(peer.key(kb, 30, 1)) // "q" on AZERTY
243+ srv.write(peer.key(kb, 30, 0))
244+ srv.write(peer.key(kb, 14, 1)) // BackSpace
245+ srv.write(peer.key(kb, 14, 0))
246+ srv.write(peer.key(kb, 28, 1)) // Return: delivered, no text
247+ require.NoError(t, c.Roundtrip())
248+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2+7 })
249+ for _, text := range texts[2:] {
250+ require.Empty(t, text)
251+ }
252+ require.Equal(t, "a", string(buf.Bytes()))
253+ require.Equal(t, 1, buf.Len())
254+ require.Equal(t, []int{1, 2, 1}, changed)
255+
256+ srv.write(frame(kb, 2, appendU32(appendU32(nil, 5), surf))) // leave
257+ require.NoError(t, c.Roundtrip())
258+ dispatchUntil(t, c, h, func() bool { return len(focus) >= 2 })
259+ require.Equal(t, []bool{true, false}, focus)
260+ buf.Wipe()
261+}
262+
263+// TestSeatKeymapFDClosedOnClose: a keymap descriptor queued but never drained
264+// is closed by Close. The descriptor count after Close must not depend on
265+// whether a keymap was left in the queue.
266+func TestSeatKeymapFDClosedOnClose(t *testing.T) {
267+ run := func(withKeymap bool) (after int) {
268+ t.Run("run", func(t *testing.T) {
269+ c, _, peer, _ := seatSetup(t)
270+ _, kb := peer.ids()
271+ if withKeymap {
272+ before := openFDs(t)
273+ peer.keymap(kb, 1)
274+ require.NoError(t, c.Roundtrip()) // queued, not dispatched
275+ require.Greater(t, openFDs(t), before, "the queued event owns a descriptor")
276+ }
277+ require.NoError(t, c.Close())
278+ after = openFDs(t)
279+ })
280+ return after
281+ }
282+ without := run(false)
283+ require.Equal(t, without, run(true), "descriptor closed with the connection")
284+}
285+
286+// TestSeatKeymapFDClosedOnBadFormat: an unknown keymap format is reported and
287+// its descriptor closed; no_keymap (0) is closed silently.
288+func TestSeatKeymapFDClosedOnBadFormat(t *testing.T) {
289+ c, _, peer, _ := seatSetup(t)
290+ _, kb := peer.ids()
291+ before := openFDs(t)
292+
293+ peer.keymap(kb, 0) // no_keymap: no error, no leak
294+ require.NoError(t, c.Roundtrip())
295+ require.NoError(t, c.Dispatch(neferclientmocks.NewMockHandler(t))) // any Handler call fails
296+ require.Equal(t, before, openFDs(t))
297+
298+ peer.keymap(kb, 7) // unknown format
299+ require.NoError(t, c.Roundtrip())
300+ var errs int
301+ h := neferclientmocks.NewMockHandler(t)
302+ h.EXPECT().Error(mock.Anything).Run(func(error) { errs++ }).Return()
303+ dispatchUntil(t, c, h, func() bool { return errs == 1 })
304+ require.Equal(t, before, openFDs(t))
305+}
306+
307+func TestSeatCapabilityLossReleasesDevices(t *testing.T) {
308+ c, srv, peer, s := seatSetup(t)
309+ pointer, kb := peer.ids()
310+ b := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
311+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
312+ peer.keyboardEnter(kb, s.SurfaceObjectID())
313+ require.NoError(t, c.Roundtrip())
314+
315+ var got []neferclient.PointerEvent
316+ var focus []bool
317+ h := neferclientmocks.NewMockHandler(t)
318+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
319+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
320+ dispatchUntil(t, c, h, func() bool { return len(got) == 1 && len(focus) == 1 })
321+
322+ peer.capabilities(0)
323+ require.NoError(t, c.Roundtrip())
324+ dispatchUntil(t, c, h, func() bool { return len(got) == 2 && len(focus) == 2 })
325+ require.Equal(t, neferclient.PointerLeave, got[1].Kind, "a synthetic leave closes the focus")
326+ require.False(t, focus[1])
327+ require.NoError(t, c.Roundtrip())
328+ peer.mu.Lock()
329+ require.True(t, peer.pointerReleased)
330+ peer.mu.Unlock()
331+}
332+
333+// TestSeatRepeatThroughEpoll: the repeat timerfd is watched by the connection's
334+// epoll loop and the repeat arrives through Dispatch.
335+func TestSeatRepeatThroughEpoll(t *testing.T) {
336+ c, srv, peer, s := seatSetup(t)
337+ _, kb := peer.ids()
338+ peer.keymap(kb, 1)
339+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 100), 1))) // 100/s after 1 ms
340+ peer.keyboardEnter(kb, s.SurfaceObjectID())
341+ srv.write(peer.key(kb, 16, 1))
342+ require.NoError(t, c.Roundtrip())
343+ var repeats int
344+ var texts []string
345+ h := neferclientmocks.NewMockHandler(t)
346+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return()
347+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
348+ if ev.Repeat {
349+ repeats++
350+ texts = append(texts, string(ev.Text))
351+ }
352+ }).Return()
353+ dispatchUntil(t, c, h, func() bool { return repeats >= 3 })
354+ require.Equal(t, "a", texts[0])
355+
356+ srv.write(peer.key(kb, 16, 0))
357+ require.NoError(t, c.Roundtrip())
358+ require.NoError(t, c.Dispatch(h))
359+ n := repeats
360+ time.Sleep(50 * time.Millisecond) // five repeat intervals at 100/s
361+ require.NoError(t, c.Dispatch(h))
362+ require.Equal(t, n, repeats, "release stops the repeat")
363+}
364+
365+// xkbAllocsPerPress bounds the allocations of one key press. They all come
366+// from purego-xkbcommon: its functions are registered through purego's
367+// reflection-based RegisterFunc, which allocates the boxed return value (and
368+// more) on every native call, and a press makes four (key_get_one_sym,
369+// compose feed, compose status, key_get_utf8). The library itself adds none;
370+// the zero-allocation guard below proves that. The bound pins the binding's
371+// cost so a regression above it shows.
372+const xkbAllocsPerPress = 20
373+
374+// xkbAllocsPerMask bounds a mask change: update_mask, serialize_mods and
375+// serialize_layout, boxed the same way.
376+const xkbAllocsPerMask = 12
377+
378+// TestAllocKey measures the steady-state key path over a real socketpair, on
379+// the real code path (no mocks): key frames in, reader decode, queue, Dispatch
380+// with xkb state and translation, and the Handler call.
381+//
382+// Releases make no native call, so a batch of them measures the library's own
383+// cost, which must be zero. Presses add the purego-xkbcommon cost (see
384+// xkbAllocsPerPress); that is measured for ordinary text and for the secret
385+// buffer.
386+func TestAllocKey(t *testing.T) {
387+ skipUnderRace(t)
388+ c, srv, peer, s := seatSetup(t)
389+ _, kb := peer.ids()
390+ peer.keymap(kb, 1)
391+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 0), 0))) // repeat disabled
392+ peer.keyboardEnter(kb, s.SurfaceObjectID())
393+ require.NoError(t, c.Roundtrip())
394+ h := &countingHandler{}
395+ require.NoError(t, c.Dispatch(h))
396+ require.Zero(t, h.errs)
397+
398+ run := func(batch []byte, events int) func() {
399+ return func() {
400+ if _, err := srv.conn.Write(batch); err != nil {
401+ t.Fatal(err)
402+ }
403+ for c.QueueLen() < events {
404+ runtime.Gosched()
405+ }
406+ if err := c.Dispatch(h); err != nil {
407+ t.Fatal(err)
408+ }
409+ }
410+ }
411+ measure := func(name string, step func()) float64 {
412+ step()
413+ step()
414+ allocs := testing.AllocsPerRun(20, step)
415+ t.Logf("%s: allocs per Dispatch: %v", name, allocs)
416+ return allocs
417+ }
418+
419+ const pairs = 50
420+ var releases []byte // releases of keys that were never pressed: queue + decode + dispatch only
421+ for range 2 * pairs {
422+ releases = append(releases, peer.key(kb, 16, 0)...)
423+ }
424+ require.Zero(t, measure("pipeline, 100 releases", run(releases, 2*pairs)), "the library itself allocates nothing per key")
425+ require.Zero(t, h.keys, "unheld releases are not delivered")
426+
427+ var batch []byte
428+ for range pairs {
429+ batch = append(batch, peer.key(kb, 16, 1)...)
430+ batch = append(batch, peer.key(kb, 16, 0)...)
431+ }
432+ step := run(batch, 2*pairs)
433+ text := measure("text, 50 presses + releases", step)
434+ require.LessOrEqual(t, text, float64(pairs*xkbAllocsPerPress))
435+ require.Equal(t, "a", h.lastText)
436+
437+ // Modifier masks: an identical mask is a no-op and allocates nothing; a
438+ // changing one updates the xkb state (purego-xkbcommon calls, bounded).
439+ mods := func(depressed uint32) []byte {
440+ b := appendU32(appendU32(nil, 1), depressed)
441+ return frame(kb, 4, appendU32(appendU32(appendU32(b, 0), 0), 0))
442+ }
443+ var same, changing []byte
444+ for range 100 {
445+ same = append(same, mods(0)...)
446+ }
447+ for i := range 100 {
448+ changing = append(changing, mods(uint32(i%2))...)
449+ }
450+ require.Zero(t, measure("modifiers, 100 identical masks", run(same, 100)), "identical masks cost nothing")
451+ changes := measure("modifiers, 100 alternating masks", run(changing, 100))
452+ require.LessOrEqual(t, changes, float64(100*xkbAllocsPerMask))
453+
454+ buf := neferclient.NewSecretBuffer(4096)
455+ c.Seat().SetSecret(buf)
456+ secret := measure("secret, 50 presses + releases", step)
457+ require.LessOrEqual(t, secret, float64(pairs*xkbAllocsPerPress))
458+ require.Empty(t, h.lastText, "no text reaches the handler in secret mode")
459+ require.Positive(t, buf.Len())
460+ buf.Wipe()
461+ require.Zero(t, h.errs)
462+}
463+
464+// TestAllocPointer measures the pointer path over a real socketpair: a batch
465+// of motion, button and axis frames in, reader decode, queue, Dispatch and the
466+// Handler call. Pointer events need no xkb, so the whole path allocates
467+// nothing.
468+func TestAllocPointer(t *testing.T) {
469+ skipUnderRace(t)
470+ c, srv, peer, s := seatSetup(t)
471+ pointer, _ := peer.ids()
472+ enter := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
473+ srv.write(frame(pointer, 0, appendU32(appendU32(enter, fixed(1)), fixed(1))))
474+ require.NoError(t, c.Roundtrip())
475+ h := &countingHandler{}
476+ require.NoError(t, c.Dispatch(h))
477+ require.Equal(t, 1, h.pointers)
478+
479+ const rounds = 40
480+ var batch []byte
481+ for i := range rounds {
482+ batch = append(batch, frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(float64(i))), appendU32(nil, fixed(2))...))...)
483+ batch = append(batch, frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 6), 0), 0x110), uint32(i%2)))...)
484+ batch = append(batch, frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(2)))...)
485+ batch = append(batch, frame(pointer, 9, appendU32(appendU32(nil, 0), 120))...)
486+ }
487+ step := func() {
488+ if _, err := srv.conn.Write(batch); err != nil {
489+ t.Fatal(err)
490+ }
491+ for c.QueueLen() < 4*rounds {
492+ runtime.Gosched()
493+ }
494+ if err := c.Dispatch(h); err != nil {
495+ t.Fatal(err)
496+ }
497+ }
498+ step()
499+ step()
500+ before := h.pointers
501+ allocs := testing.AllocsPerRun(20, step)
502+ t.Logf("allocs per Dispatch of %d pointer events: %v", 4*rounds, allocs)
503+ require.Zero(t, allocs)
504+ require.Greater(t, h.pointers, before)
505+ require.Zero(t, h.errs)
506+}
@@ -0,0 +1,506 @@
1+package neferclient_test
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "runtime"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+ "golang.org/x/sys/unix"
14+
15+ "github.com/bnema/neferclient"
16+ neferclientmocks "github.com/bnema/neferclient/mocks"
17+)
18+
19+const (
20+ seatGlobal = 8
21+ shapesGlobal = 9
22+)
23+
24+var seatGlobals = append(append([]wireGlobal{}, surfaceGlobals...),
25+ wireGlobal{seatGlobal, "wl_seat", 9}, wireGlobal{shapesGlobal, "wp_cursor_shape_manager_v1", 2})
26+
27+// seatPeer is the compositor side of a seat: it learns the object ids the
28+// client creates and scripts input events.
29+type seatPeer struct {
30+ t *testing.T
31+ srv *wireServer
32+
33+ mu sync.Mutex
34+ pointer, keyboard, shape uint32
35+ shapeSerial, shapeValue uint32
36+ shapeSets int
37+ pointerReleased bool
38+}
39+
40+func newSeatPeer(t *testing.T, srv *wireServer) *seatPeer {
41+ p := &seatPeer{t: t, srv: srv}
42+ srv.mu.Lock()
43+ srv.hook = func(obj uint32, op uint16, body []byte) {
44+ srv.mu.Lock()
45+ seat, shapes := srv.bound[seatGlobal].id, srv.bound[shapesGlobal].id
46+ srv.mu.Unlock()
47+ p.mu.Lock()
48+ defer p.mu.Unlock()
49+ switch {
50+ case obj == seat && seat != 0 && op == 0 && len(body) == 4:
51+ p.pointer = binary.LittleEndian.Uint32(body)
52+ case obj == seat && seat != 0 && op == 1 && len(body) == 4:
53+ p.keyboard = binary.LittleEndian.Uint32(body)
54+ case obj == shapes && shapes != 0 && op == 1 && len(body) == 8:
55+ p.shape = binary.LittleEndian.Uint32(body)
56+ case obj == p.shape && p.shape != 0 && op == 1 && len(body) == 8:
57+ p.shapeSerial = binary.LittleEndian.Uint32(body)
58+ p.shapeValue = binary.LittleEndian.Uint32(body[4:])
59+ p.shapeSets++
60+ case obj == p.pointer && p.pointer != 0 && op == 1:
61+ p.pointerReleased = true
62+ }
63+ }
64+ srv.mu.Unlock()
65+ return p
66+}
67+
68+func (p *seatPeer) ids() (pointer, keyboard uint32) {
69+ p.mu.Lock()
70+ defer p.mu.Unlock()
71+ return p.pointer, p.keyboard
72+}
73+
74+func (p *seatPeer) seatID() uint32 {
75+ p.srv.mu.Lock()
76+ defer p.srv.mu.Unlock()
77+ return p.srv.bound[seatGlobal].id
78+}
79+
80+func fixed(v float64) uint32 { return uint32(int32(v * 256)) }
81+
82+func negU32(v int32) uint32 { return uint32(-v) }
83+
84+func (p *seatPeer) capabilities(caps uint32) {
85+ p.srv.write(frame(p.seatID(), 0, appendU32(nil, caps)))
86+}
87+
88+// keymap sends wl_keyboard.keymap with testdata/fr.xkb in a descriptor.
89+func (p *seatPeer) keymap(keyboard uint32, format uint32) {
90+ p.t.Helper()
91+ data, err := os.ReadFile("testdata/fr.xkb")
92+ require.NoError(p.t, err)
93+ data = append(data, 0)
94+ fd, err := unix.MemfdCreate("keymap", unix.MFD_CLOEXEC)
95+ require.NoError(p.t, err)
96+ defer unix.Close(fd)
97+ _, err = unix.Write(fd, data)
98+ require.NoError(p.t, err)
99+ body := appendU32(nil, format)
100+ body = appendU32(body, uint32(len(data)))
101+ _, _, err = p.srv.conn.WriteMsgUnix(frame(keyboard, 0, body), unix.UnixRights(fd), nil)
102+ require.NoError(p.t, err)
103+}
104+
105+func (p *seatPeer) keyboardEnter(kb, surface uint32) {
106+ body := appendU32(appendU32(nil, 1), surface)
107+ body = appendU32(body, 0) // empty keys array
108+ p.srv.write(frame(kb, 1, body))
109+}
110+
111+func (p *seatPeer) key(kb, code, state uint32) []byte {
112+ b := appendU32(appendU32(nil, 1), 0)
113+ b = appendU32(appendU32(b, code), state)
114+ return frame(kb, 3, b)
115+}
116+
117+func seatSetup(t *testing.T) (*neferclient.Conn, *wireServer, *seatPeer, *neferclient.Surface) {
118+ t.Helper()
119+ c, srv := connectWire(t, seatGlobals, surfaceOutputs)
120+ peer := newSeatPeer(t, srv)
121+ s, err := c.NewToplevel("seat", 64, 48)
122+ require.NoError(t, err)
123+ require.NotNil(t, c.Seat())
124+ require.NoError(t, c.Roundtrip())
125+ peer.capabilities(3)
126+ require.NoError(t, c.Roundtrip())
127+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
128+ require.NoError(t, c.Roundtrip())
129+ return c, srv, peer, s
130+}
131+
132+func TestSeatPointerEvents(t *testing.T) {
133+ c, srv, peer, s := seatSetup(t)
134+ pointer, keyboard := peer.ids()
135+ require.NotZero(t, pointer)
136+ require.NotZero(t, keyboard)
137+ surf := s.SurfaceObjectID()
138+
139+ var got []neferclient.PointerEvent
140+ h := neferclientmocks.NewMockHandler(t)
141+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
142+
143+ // A motion outside any surface is ignored; the enter binds the position.
144+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(1)), appendU32(nil, fixed(2))...)))
145+ b := appendU32(appendU32(nil, 77), surf)
146+ b = appendU32(appendU32(b, fixed(10.5)), fixed(20.25))
147+ srv.write(frame(pointer, 0, b)) // enter
148+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(11)), appendU32(nil, fixed(21))...)))
149+ srv.write(frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 78), 0), 0x110), 1))) // button press
150+ srv.write(frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(15)))) // axis vertical
151+ srv.write(frame(pointer, 9, appendU32(appendU32(nil, 1), negU32(120)))) // value120 horizontal
152+ srv.write(frame(pointer, 7, appendU32(appendU32(nil, 0), 0))) // axis stop
153+ srv.write(frame(pointer, 1, appendU32(appendU32(nil, 79), surf))) // leave
154+ srv.write(frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(3)), appendU32(nil, fixed(4))...)))
155+ require.NoError(t, c.Roundtrip())
156+ dispatchUntil(t, c, h, func() bool { return len(got) >= 7 })
157+
158+ require.Len(t, got, 7)
159+ id := s.ID()
160+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerEnter, X: 10.5, Y: 20.25}, got[0])
161+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerMotion, X: 11, Y: 21}, got[1])
162+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerButton, X: 11, Y: 21, Button: 0x110, Pressed: true}, got[2])
163+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 0, DY: 15}, got[3])
164+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxis, X: 11, Y: 21, Axis: 1, Value120: -120}, got[4])
165+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerAxisStop, X: 11, Y: 21}, got[5])
166+ require.Equal(t, neferclient.PointerEvent{Surface: id, Kind: neferclient.PointerLeave, X: 11, Y: 21}, got[6])
167+}
168+
169+func TestSeatCursorShape(t *testing.T) {
170+ c, srv, peer, s := seatSetup(t)
171+ pointer, _ := peer.ids()
172+ seat := c.Seat()
173+ require.NoError(t, seat.SetCursor(neferclient.CursorPointer), "outside: no-op")
174+ require.NoError(t, c.Roundtrip())
175+ require.Zero(t, peer.shapeSets)
176+
177+ enter := func(serial uint32) {
178+ b := appendU32(appendU32(nil, serial), s.SurfaceObjectID())
179+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
180+ require.NoError(t, c.Roundtrip())
181+ require.NoError(t, c.Dispatch(neferclient.NopHandler{}))
182+ }
183+ enter(40)
184+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
185+ require.NoError(t, seat.SetCursor(neferclient.CursorText), "unchanged: nothing sent")
186+ require.NoError(t, c.Roundtrip())
187+ peer.mu.Lock()
188+ require.Equal(t, 1, peer.shapeSets)
189+ require.Equal(t, uint32(40), peer.shapeSerial)
190+ require.Equal(t, uint32(neferclient.CursorText), peer.shapeValue)
191+ peer.mu.Unlock()
192+
193+ enter(41) // a new enter resets the shape and carries a new serial
194+ require.NoError(t, seat.SetCursor(neferclient.CursorText))
195+ require.NoError(t, c.Roundtrip())
196+ peer.mu.Lock()
197+ require.Equal(t, 2, peer.shapeSets)
198+ require.Equal(t, uint32(41), peer.shapeSerial)
199+ peer.mu.Unlock()
200+ require.Error(t, seat.SetCursor(0))
201+ require.Error(t, seat.SetCursor(neferclient.CursorAllResize+1))
202+}
203+
204+func TestSeatKeyboardFocusTextAndSecret(t *testing.T) {
205+ c, srv, peer, s := seatSetup(t)
206+ _, kb := peer.ids()
207+ surf := s.SurfaceObjectID()
208+ seat := c.Seat()
209+
210+ var keys []neferclient.KeyEvent
211+ var texts []string
212+ var focus []bool
213+ var changed []int
214+ h := neferclientmocks.NewMockHandler(t)
215+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
216+ keys = append(keys, *ev)
217+ texts = append(texts, string(ev.Text)) // the only place text is copied: the assertion of the test
218+ }).Return()
219+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
220+ h.EXPECT().SecretChanged(mock.Anything).Run(func(n int) { changed = append(changed, n) }).Return()
221+
222+ peer.keymap(kb, 1) // French AZERTY: evdev 16 is "a"
223+ srv.write(frame(kb, 5, append(appendU32(nil, 0), appendU32(nil, 0)...)))
224+ peer.keyboardEnter(kb, surf)
225+ srv.write(peer.key(kb, 16, 1))
226+ srv.write(peer.key(kb, 16, 0))
227+ require.NoError(t, c.Roundtrip())
228+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2 })
229+ require.Equal(t, []bool{true}, focus)
230+ require.Equal(t, "a", texts[0])
231+ require.True(t, keys[0].Pressed)
232+ require.Equal(t, s.ID(), keys[0].Surface)
233+ require.Equal(t, "", texts[1])
234+ require.False(t, keys[1].Pressed)
235+ require.Empty(t, changed)
236+
237+ // With a secret buffer: the text goes only into it.
238+ buf := neferclient.NewSecretBuffer(8)
239+ seat.SetSecret(buf)
240+ srv.write(peer.key(kb, 16, 1))
241+ srv.write(peer.key(kb, 16, 0))
242+ srv.write(peer.key(kb, 30, 1)) // "q" on AZERTY
243+ srv.write(peer.key(kb, 30, 0))
244+ srv.write(peer.key(kb, 14, 1)) // BackSpace
245+ srv.write(peer.key(kb, 14, 0))
246+ srv.write(peer.key(kb, 28, 1)) // Return: delivered, no text
247+ require.NoError(t, c.Roundtrip())
248+ dispatchUntil(t, c, h, func() bool { return len(keys) >= 2+7 })
249+ for _, text := range texts[2:] {
250+ require.Empty(t, text)
251+ }
252+ require.Equal(t, "a", string(buf.Bytes()))
253+ require.Equal(t, 1, buf.Len())
254+ require.Equal(t, []int{1, 2, 1}, changed)
255+
256+ srv.write(frame(kb, 2, appendU32(appendU32(nil, 5), surf))) // leave
257+ require.NoError(t, c.Roundtrip())
258+ dispatchUntil(t, c, h, func() bool { return len(focus) >= 2 })
259+ require.Equal(t, []bool{true, false}, focus)
260+ buf.Wipe()
261+}
262+
263+// TestSeatKeymapFDClosedOnClose: a keymap descriptor queued but never drained
264+// is closed by Close. The descriptor count after Close must not depend on
265+// whether a keymap was left in the queue.
266+func TestSeatKeymapFDClosedOnClose(t *testing.T) {
267+ run := func(withKeymap bool) (after int) {
268+ t.Run("run", func(t *testing.T) {
269+ c, _, peer, _ := seatSetup(t)
270+ _, kb := peer.ids()
271+ if withKeymap {
272+ before := openFDs(t)
273+ peer.keymap(kb, 1)
274+ require.NoError(t, c.Roundtrip()) // queued, not dispatched
275+ require.Greater(t, openFDs(t), before, "the queued event owns a descriptor")
276+ }
277+ require.NoError(t, c.Close())
278+ after = openFDs(t)
279+ })
280+ return after
281+ }
282+ without := run(false)
283+ require.Equal(t, without, run(true), "descriptor closed with the connection")
284+}
285+
286+// TestSeatKeymapFDClosedOnBadFormat: an unknown keymap format is reported and
287+// its descriptor closed; no_keymap (0) is closed silently.
288+func TestSeatKeymapFDClosedOnBadFormat(t *testing.T) {
289+ c, _, peer, _ := seatSetup(t)
290+ _, kb := peer.ids()
291+ before := openFDs(t)
292+
293+ peer.keymap(kb, 0) // no_keymap: no error, no leak
294+ require.NoError(t, c.Roundtrip())
295+ require.NoError(t, c.Dispatch(neferclientmocks.NewMockHandler(t))) // any Handler call fails
296+ require.Equal(t, before, openFDs(t))
297+
298+ peer.keymap(kb, 7) // unknown format
299+ require.NoError(t, c.Roundtrip())
300+ var errs int
301+ h := neferclientmocks.NewMockHandler(t)
302+ h.EXPECT().Error(mock.Anything).Run(func(error) { errs++ }).Return()
303+ dispatchUntil(t, c, h, func() bool { return errs == 1 })
304+ require.Equal(t, before, openFDs(t))
305+}
306+
307+func TestSeatCapabilityLossReleasesDevices(t *testing.T) {
308+ c, srv, peer, s := seatSetup(t)
309+ pointer, kb := peer.ids()
310+ b := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
311+ srv.write(frame(pointer, 0, appendU32(appendU32(b, fixed(1)), fixed(1))))
312+ peer.keyboardEnter(kb, s.SurfaceObjectID())
313+ require.NoError(t, c.Roundtrip())
314+
315+ var got []neferclient.PointerEvent
316+ var focus []bool
317+ h := neferclientmocks.NewMockHandler(t)
318+ h.EXPECT().Pointer(mock.Anything).Run(func(ev *neferclient.PointerEvent) { got = append(got, *ev) }).Return()
319+ h.EXPECT().KeyboardFocus(s.ID(), mock.Anything).Run(func(_ neferclient.SurfaceID, f bool) { focus = append(focus, f) }).Return()
320+ dispatchUntil(t, c, h, func() bool { return len(got) == 1 && len(focus) == 1 })
321+
322+ peer.capabilities(0)
323+ require.NoError(t, c.Roundtrip())
324+ dispatchUntil(t, c, h, func() bool { return len(got) == 2 && len(focus) == 2 })
325+ require.Equal(t, neferclient.PointerLeave, got[1].Kind, "a synthetic leave closes the focus")
326+ require.False(t, focus[1])
327+ require.NoError(t, c.Roundtrip())
328+ peer.mu.Lock()
329+ require.True(t, peer.pointerReleased)
330+ peer.mu.Unlock()
331+}
332+
333+// TestSeatRepeatThroughEpoll: the repeat timerfd is watched by the connection's
334+// epoll loop and the repeat arrives through Dispatch.
335+func TestSeatRepeatThroughEpoll(t *testing.T) {
336+ c, srv, peer, s := seatSetup(t)
337+ _, kb := peer.ids()
338+ peer.keymap(kb, 1)
339+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 100), 1))) // 100/s after 1 ms
340+ peer.keyboardEnter(kb, s.SurfaceObjectID())
341+ srv.write(peer.key(kb, 16, 1))
342+ require.NoError(t, c.Roundtrip())
343+ var repeats int
344+ var texts []string
345+ h := neferclientmocks.NewMockHandler(t)
346+ h.EXPECT().KeyboardFocus(mock.Anything, mock.Anything).Return()
347+ h.EXPECT().Key(mock.Anything).Run(func(ev *neferclient.KeyEvent) {
348+ if ev.Repeat {
349+ repeats++
350+ texts = append(texts, string(ev.Text))
351+ }
352+ }).Return()
353+ dispatchUntil(t, c, h, func() bool { return repeats >= 3 })
354+ require.Equal(t, "a", texts[0])
355+
356+ srv.write(peer.key(kb, 16, 0))
357+ require.NoError(t, c.Roundtrip())
358+ require.NoError(t, c.Dispatch(h))
359+ n := repeats
360+ time.Sleep(50 * time.Millisecond) // five repeat intervals at 100/s
361+ require.NoError(t, c.Dispatch(h))
362+ require.Equal(t, n, repeats, "release stops the repeat")
363+}
364+
365+// xkbAllocsPerPress bounds the allocations of one key press. They all come
366+// from purego-xkbcommon: its functions are registered through purego's
367+// reflection-based RegisterFunc, which allocates the boxed return value (and
368+// more) on every native call, and a press makes four (key_get_one_sym,
369+// compose feed, compose status, key_get_utf8). The library itself adds none;
370+// the zero-allocation guard below proves that. The bound pins the binding's
371+// cost so a regression above it shows.
372+const xkbAllocsPerPress = 20
373+
374+// xkbAllocsPerMask bounds a mask change: update_mask, serialize_mods and
375+// serialize_layout, boxed the same way.
376+const xkbAllocsPerMask = 12
377+
378+// TestAllocKey measures the steady-state key path over a real socketpair, on
379+// the real code path (no mocks): key frames in, reader decode, queue, Dispatch
380+// with xkb state and translation, and the Handler call.
381+//
382+// Releases make no native call, so a batch of them measures the library's own
383+// cost, which must be zero. Presses add the purego-xkbcommon cost (see
384+// xkbAllocsPerPress); that is measured for ordinary text and for the secret
385+// buffer.
386+func TestAllocKey(t *testing.T) {
387+ skipUnderRace(t)
388+ c, srv, peer, s := seatSetup(t)
389+ _, kb := peer.ids()
390+ peer.keymap(kb, 1)
391+ srv.write(frame(kb, 5, appendU32(appendU32(nil, 0), 0))) // repeat disabled
392+ peer.keyboardEnter(kb, s.SurfaceObjectID())
393+ require.NoError(t, c.Roundtrip())
394+ h := &countingHandler{}
395+ require.NoError(t, c.Dispatch(h))
396+ require.Zero(t, h.errs)
397+
398+ run := func(batch []byte, events int) func() {
399+ return func() {
400+ if _, err := srv.conn.Write(batch); err != nil {
401+ t.Fatal(err)
402+ }
403+ for c.QueueLen() < events {
404+ runtime.Gosched()
405+ }
406+ if err := c.Dispatch(h); err != nil {
407+ t.Fatal(err)
408+ }
409+ }
410+ }
411+ measure := func(name string, step func()) float64 {
412+ step()
413+ step()
414+ allocs := testing.AllocsPerRun(20, step)
415+ t.Logf("%s: allocs per Dispatch: %v", name, allocs)
416+ return allocs
417+ }
418+
419+ const pairs = 50
420+ var releases []byte // releases of keys that were never pressed: queue + decode + dispatch only
421+ for range 2 * pairs {
422+ releases = append(releases, peer.key(kb, 16, 0)...)
423+ }
424+ require.Zero(t, measure("pipeline, 100 releases", run(releases, 2*pairs)), "the library itself allocates nothing per key")
425+ require.Zero(t, h.keys, "unheld releases are not delivered")
426+
427+ var batch []byte
428+ for range pairs {
429+ batch = append(batch, peer.key(kb, 16, 1)...)
430+ batch = append(batch, peer.key(kb, 16, 0)...)
431+ }
432+ step := run(batch, 2*pairs)
433+ text := measure("text, 50 presses + releases", step)
434+ require.LessOrEqual(t, text, float64(pairs*xkbAllocsPerPress))
435+ require.Equal(t, "a", h.lastText)
436+
437+ // Modifier masks: an identical mask is a no-op and allocates nothing; a
438+ // changing one updates the xkb state (purego-xkbcommon calls, bounded).
439+ mods := func(depressed uint32) []byte {
440+ b := appendU32(appendU32(nil, 1), depressed)
441+ return frame(kb, 4, appendU32(appendU32(appendU32(b, 0), 0), 0))
442+ }
443+ var same, changing []byte
444+ for range 100 {
445+ same = append(same, mods(0)...)
446+ }
447+ for i := range 100 {
448+ changing = append(changing, mods(uint32(i%2))...)
449+ }
450+ require.Zero(t, measure("modifiers, 100 identical masks", run(same, 100)), "identical masks cost nothing")
451+ changes := measure("modifiers, 100 alternating masks", run(changing, 100))
452+ require.LessOrEqual(t, changes, float64(100*xkbAllocsPerMask))
453+
454+ buf := neferclient.NewSecretBuffer(4096)
455+ c.Seat().SetSecret(buf)
456+ secret := measure("secret, 50 presses + releases", step)
457+ require.LessOrEqual(t, secret, float64(pairs*xkbAllocsPerPress))
458+ require.Empty(t, h.lastText, "no text reaches the handler in secret mode")
459+ require.Positive(t, buf.Len())
460+ buf.Wipe()
461+ require.Zero(t, h.errs)
462+}
463+
464+// TestAllocPointer measures the pointer path over a real socketpair: a batch
465+// of motion, button and axis frames in, reader decode, queue, Dispatch and the
466+// Handler call. Pointer events need no xkb, so the whole path allocates
467+// nothing.
468+func TestAllocPointer(t *testing.T) {
469+ skipUnderRace(t)
470+ c, srv, peer, s := seatSetup(t)
471+ pointer, _ := peer.ids()
472+ enter := appendU32(appendU32(nil, 5), s.SurfaceObjectID())
473+ srv.write(frame(pointer, 0, appendU32(appendU32(enter, fixed(1)), fixed(1))))
474+ require.NoError(t, c.Roundtrip())
475+ h := &countingHandler{}
476+ require.NoError(t, c.Dispatch(h))
477+ require.Equal(t, 1, h.pointers)
478+
479+ const rounds = 40
480+ var batch []byte
481+ for i := range rounds {
482+ batch = append(batch, frame(pointer, 2, append(appendU32(appendU32(nil, 0), fixed(float64(i))), appendU32(nil, fixed(2))...))...)
483+ batch = append(batch, frame(pointer, 3, appendU32(appendU32(appendU32(appendU32(nil, 6), 0), 0x110), uint32(i%2)))...)
484+ batch = append(batch, frame(pointer, 4, appendU32(appendU32(appendU32(nil, 0), 0), fixed(2)))...)
485+ batch = append(batch, frame(pointer, 9, appendU32(appendU32(nil, 0), 120))...)
486+ }
487+ step := func() {
488+ if _, err := srv.conn.Write(batch); err != nil {
489+ t.Fatal(err)
490+ }
491+ for c.QueueLen() < 4*rounds {
492+ runtime.Gosched()
493+ }
494+ if err := c.Dispatch(h); err != nil {
495+ t.Fatal(err)
496+ }
497+ }
498+ step()
499+ step()
500+ before := h.pointers
501+ allocs := testing.AllocsPerRun(20, step)
502+ t.Logf("allocs per Dispatch of %d pointer events: %v", 4*rounds, allocs)
503+ require.Zero(t, allocs)
504+ require.Greater(t, h.pointers, before)
505+ require.Zero(t, h.errs)
506+}
A secret.go +92 -0
@@ -0,0 +1,92 @@
1+package neferclient
2+
3+import "unicode/utf8"
4+
5+// maxSecretCapacity bounds [NewSecretBuffer].
6+const maxSecretCapacity = 4096
7+
8+// SecretBuffer collects text typed while it is installed with
9+// [Seat.SetSecret], for example a password. Its storage is allocated once at
10+// full size, so it never reallocates and leaves no stale copies behind; the
11+// text is never turned into a string by the library.
12+//
13+// Only the owner goroutine (the one calling [Conn.Dispatch]) may use it.
14+type SecretBuffer struct {
15+ buf []byte // capacity*utf8.UTFMax bytes, fixed
16+ n int // bytes in use
17+ points int // code points in use
18+ max int // capacity in code points
19+}
20+
21+// NewSecretBuffer returns a buffer holding up to capacity code points.
22+// capacity is clamped to 1..4096.
23+func NewSecretBuffer(capacity int) *SecretBuffer {
24+ capacity = min(max(capacity, 1), maxSecretCapacity)
25+ return &SecretBuffer{buf: make([]byte, capacity*utf8.UTFMax), max: capacity}
26+}
27+
28+// Len returns the number of code points held.
29+func (b *SecretBuffer) Len() int {
30+ if b == nil {
31+ return 0
32+ }
33+ return b.points
34+}
35+
36+// Bytes returns the UTF-8 text held. The slice aliases the buffer: do not
37+// retain it, convert it to a string or log it; it changes on the next edit.
38+func (b *SecretBuffer) Bytes() []byte {
39+ if b == nil {
40+ return nil
41+ }
42+ return b.buf[:b.n:b.n]
43+}
44+
45+// Wipe zeroes the whole storage and empties the buffer.
46+func (b *SecretBuffer) Wipe() {
47+ if b == nil {
48+ return
49+ }
50+ clear(b.buf)
51+ b.n, b.points = 0, 0
52+}
53+
54+// appendText adds text, which must be valid UTF-8. It reports false and
55+// changes nothing when the text does not fit.
56+func (b *SecretBuffer) appendText(text []byte) bool {
57+ cp := utf8.RuneCount(text)
58+ if cp == 0 || b.points+cp > b.max {
59+ return false
60+ }
61+ b.n += copy(b.buf[b.n:], text)
62+ b.points += cp
63+ return true
64+}
65+
66+// backspace removes the last code point and wipes its bytes.
67+func (b *SecretBuffer) backspace() bool {
68+ if b.points == 0 {
69+ return false
70+ }
71+ _, size := utf8.DecodeLastRune(b.buf[:b.n])
72+ clear(b.buf[b.n-size : b.n])
73+ b.n -= size
74+ b.points--
75+ return true
76+}
77+
78+// classifySecretText reports whether text is printable input for a secret:
79+// non-empty, valid UTF-8 and free of C0 controls and DEL.
80+func classifySecretText(text []byte) bool {
81+ if len(text) == 0 || !utf8.Valid(text) {
82+ return false
83+ }
84+ for rest := text; len(rest) > 0; {
85+ r, size := utf8.DecodeRune(rest)
86+ if r < 0x20 || r == 0x7f {
87+ return false
88+ }
89+ rest = rest[size:]
90+ }
91+ return true
92+}
@@ -0,0 +1,92 @@
1+package neferclient
2+
3+import "unicode/utf8"
4+
5+// maxSecretCapacity bounds [NewSecretBuffer].
6+const maxSecretCapacity = 4096
7+
8+// SecretBuffer collects text typed while it is installed with
9+// [Seat.SetSecret], for example a password. Its storage is allocated once at
10+// full size, so it never reallocates and leaves no stale copies behind; the
11+// text is never turned into a string by the library.
12+//
13+// Only the owner goroutine (the one calling [Conn.Dispatch]) may use it.
14+type SecretBuffer struct {
15+ buf []byte // capacity*utf8.UTFMax bytes, fixed
16+ n int // bytes in use
17+ points int // code points in use
18+ max int // capacity in code points
19+}
20+
21+// NewSecretBuffer returns a buffer holding up to capacity code points.
22+// capacity is clamped to 1..4096.
23+func NewSecretBuffer(capacity int) *SecretBuffer {
24+ capacity = min(max(capacity, 1), maxSecretCapacity)
25+ return &SecretBuffer{buf: make([]byte, capacity*utf8.UTFMax), max: capacity}
26+}
27+
28+// Len returns the number of code points held.
29+func (b *SecretBuffer) Len() int {
30+ if b == nil {
31+ return 0
32+ }
33+ return b.points
34+}
35+
36+// Bytes returns the UTF-8 text held. The slice aliases the buffer: do not
37+// retain it, convert it to a string or log it; it changes on the next edit.
38+func (b *SecretBuffer) Bytes() []byte {
39+ if b == nil {
40+ return nil
41+ }
42+ return b.buf[:b.n:b.n]
43+}
44+
45+// Wipe zeroes the whole storage and empties the buffer.
46+func (b *SecretBuffer) Wipe() {
47+ if b == nil {
48+ return
49+ }
50+ clear(b.buf)
51+ b.n, b.points = 0, 0
52+}
53+
54+// appendText adds text, which must be valid UTF-8. It reports false and
55+// changes nothing when the text does not fit.
56+func (b *SecretBuffer) appendText(text []byte) bool {
57+ cp := utf8.RuneCount(text)
58+ if cp == 0 || b.points+cp > b.max {
59+ return false
60+ }
61+ b.n += copy(b.buf[b.n:], text)
62+ b.points += cp
63+ return true
64+}
65+
66+// backspace removes the last code point and wipes its bytes.
67+func (b *SecretBuffer) backspace() bool {
68+ if b.points == 0 {
69+ return false
70+ }
71+ _, size := utf8.DecodeLastRune(b.buf[:b.n])
72+ clear(b.buf[b.n-size : b.n])
73+ b.n -= size
74+ b.points--
75+ return true
76+}
77+
78+// classifySecretText reports whether text is printable input for a secret:
79+// non-empty, valid UTF-8 and free of C0 controls and DEL.
80+func classifySecretText(text []byte) bool {
81+ if len(text) == 0 || !utf8.Valid(text) {
82+ return false
83+ }
84+ for rest := text; len(rest) > 0; {
85+ r, size := utf8.DecodeRune(rest)
86+ if r < 0x20 || r == 0x7f {
87+ return false
88+ }
89+ rest = rest[size:]
90+ }
91+ return true
92+}
A secret_test.go +51 -0
@@ -0,0 +1,51 @@
1+package neferclient
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+)
8+
9+func TestSecretBufferEditing(t *testing.T) {
10+ b := NewSecretBuffer(4)
11+ storage := b.buf
12+ require.True(t, b.appendText([]byte("a")))
13+ require.True(t, b.appendText([]byte("é€")))
14+ require.Equal(t, 3, b.Len())
15+ require.Equal(t, []byte("aé€"), b.Bytes())
16+
17+ require.True(t, b.backspace()) // one code point, not one byte
18+ require.Equal(t, 2, b.Len())
19+ require.Equal(t, []byte("aé"), b.Bytes())
20+ require.Equal(t, make([]byte, 3), storage[3:6], "removed bytes are wiped")
21+
22+ require.False(t, b.appendText([]byte("xyz")), "does not fit: nothing changes")
23+ require.Equal(t, []byte("aé"), b.Bytes())
24+ require.True(t, b.appendText([]byte("xy")))
25+ require.False(t, b.appendText([]byte("z")), "full")
26+
27+ b.Wipe()
28+ require.Zero(t, b.Len())
29+ require.Empty(t, b.Bytes())
30+ require.Equal(t, make([]byte, len(storage)), storage, "whole storage zeroed")
31+ require.False(t, b.backspace())
32+}
33+
34+func TestSecretBufferCapacityClampAndNil(t *testing.T) {
35+ require.Equal(t, 1, NewSecretBuffer(-5).max)
36+ require.Equal(t, 1, NewSecretBuffer(0).max)
37+ require.Equal(t, maxSecretCapacity, NewSecretBuffer(1<<20).max)
38+ var b *SecretBuffer
39+ require.Zero(t, b.Len())
40+ require.Nil(t, b.Bytes())
41+ b.Wipe()
42+}
43+
44+func TestClassifySecretText(t *testing.T) {
45+ for _, ok := range []string{"a", "é", "€", " ", "日本"} {
46+ require.True(t, classifySecretText([]byte(ok)), ok)
47+ }
48+ for _, bad := range [][]byte{nil, {}, {'\r'}, {0x1b}, {0x7f}, {'a', '\n'}, {0xff}, {0xc3}} {
49+ require.False(t, classifySecretText(bad), "%q", bad)
50+ }
51+}
@@ -0,0 +1,51 @@
1+package neferclient
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/require"
7+)
8+
9+func TestSecretBufferEditing(t *testing.T) {
10+ b := NewSecretBuffer(4)
11+ storage := b.buf
12+ require.True(t, b.appendText([]byte("a")))
13+ require.True(t, b.appendText([]byte("é€")))
14+ require.Equal(t, 3, b.Len())
15+ require.Equal(t, []byte("aé€"), b.Bytes())
16+
17+ require.True(t, b.backspace()) // one code point, not one byte
18+ require.Equal(t, 2, b.Len())
19+ require.Equal(t, []byte("aé"), b.Bytes())
20+ require.Equal(t, make([]byte, 3), storage[3:6], "removed bytes are wiped")
21+
22+ require.False(t, b.appendText([]byte("xyz")), "does not fit: nothing changes")
23+ require.Equal(t, []byte("aé"), b.Bytes())
24+ require.True(t, b.appendText([]byte("xy")))
25+ require.False(t, b.appendText([]byte("z")), "full")
26+
27+ b.Wipe()
28+ require.Zero(t, b.Len())
29+ require.Empty(t, b.Bytes())
30+ require.Equal(t, make([]byte, len(storage)), storage, "whole storage zeroed")
31+ require.False(t, b.backspace())
32+}
33+
34+func TestSecretBufferCapacityClampAndNil(t *testing.T) {
35+ require.Equal(t, 1, NewSecretBuffer(-5).max)
36+ require.Equal(t, 1, NewSecretBuffer(0).max)
37+ require.Equal(t, maxSecretCapacity, NewSecretBuffer(1<<20).max)
38+ var b *SecretBuffer
39+ require.Zero(t, b.Len())
40+ require.Nil(t, b.Bytes())
41+ b.Wipe()
42+}
43+
44+func TestClassifySecretText(t *testing.T) {
45+ for _, ok := range []string{"a", "é", "€", " ", "日本"} {
46+ require.True(t, classifySecretText([]byte(ok)), ok)
47+ }
48+ for _, bad := range [][]byte{nil, {}, {'\r'}, {0x1b}, {0x7f}, {'a', '\n'}, {0xff}, {0xc3}} {
49+ require.False(t, classifySecretText(bad), "%q", bad)
50+ }
51+}
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