detached fc55404

feat(wayland): implement zwp_input_timestamps_v1 (#122)

* refactor(ports): carry input timestamps as time.Duration

Replace the TimeMsec/TimeUsec pairs on key and pointer input types with a single Time (CLOCK_MONOTONIC), like the swipe events. The wayland adapter derives the wire millisecond values with wireMsec and the relative pointer microseconds from it, so wire output is unchanged. Core edge-scroll timing keeps its 250 ms interval as a duration.

* perf(wayland): return seat device lists without allocating

The per-client pointer and keyboard lists now hold only live resources: OnDestroy removes a device copy-on-write, so clientPointers and clientKeyboards return the stored slice instead of filtering into a new one on every input event. Add an allocation test for both getters.

* feat(wayland): implement zwp_input_timestamps_v1

Advertise zwp_input_timestamps_manager_v1 version 1. A subscribed wl_keyboard or wl_pointer receives timestamp(sec_hi, sec_lo, nsec) right before each key, motion, button, axis and axis_stop event; unsubscribed devices and destroyed or inert objects get nothing. Subscriptions are kept copy-on-write per device resource and dropped with the device. The virtual keyboard forward and its release-on-destroy are stamped too; the input-method grab keeps its own interface.
commit
fc554045aaac4a0fd5bea58b4899dc621f90e2ac
author
bnema · · 2026-10-03 20:13 UTC
committer
GitHub · · 2026-10-03 20:13 UTC
parent
a59ec80
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
31 files · +523 -143
M README.md +1 -1
@@ -61,7 +61,7 @@ The terminal command comes from `terminal`, then `$TERMINAL`, then `foot`. `term
6161 - `wp_presentation`, `wp_fifo_v1`, `wp_commit_timing_v1` for frame pacing.
6262 - `wp_fractional_scale_v1` and `wp_viewporter`: clients render at the output scale, nothing is rescaled.
6363 - Damage tracking, occlusion of surfaces behind opaque ones, no work on idle outputs. Frame, cursor, overlay and VRR in one atomic commit.
64-- Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games.
64+- Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. `zwp_input_timestamps_v1` gives clients microsecond input timestamps from libinput.
6565 - `wp_drm_lease_v1` leases headset connectors to SteamVR and Monado.
6666
6767 ## Hardware support
@@ -61,7 +61,7 @@ The terminal command comes from `terminal`, then `$TERMINAL`, then `foot`. `term
61 - `wp_presentation`, `wp_fifo_v1`, `wp_commit_timing_v1` for frame pacing.61 - `wp_presentation`, `wp_fifo_v1`, `wp_commit_timing_v1` for frame pacing.
62 - `wp_fractional_scale_v1` and `wp_viewporter`: clients render at the output scale, nothing is rescaled.62 - `wp_fractional_scale_v1` and `wp_viewporter`: clients render at the output scale, nothing is rescaled.
63 - Damage tracking, occlusion of surfaces behind opaque ones, no work on idle outputs. Frame, cursor, overlay and VRR in one atomic commit.63 - Damage tracking, occlusion of surfaces behind opaque ones, no work on idle outputs. Frame, cursor, overlay and VRR in one atomic commit.
64-- Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games.64+- Input is read on its own thread and never waits on rendering; the cursor has a hardware plane. With CAP_SYS_NICE, input and output threads request real-time scheduling. Relative pointer, pointer constraints and keyboard shortcuts inhibit for games. `zwp_input_timestamps_v1` gives clients microsecond input timestamps from libinput.
65 - `wp_drm_lease_v1` leases headset connectors to SteamVR and Monado.65 - `wp_drm_lease_v1` leases headset connectors to SteamVR and Monado.
66 66
67 ## Hardware support67 ## Hardware support
M internal/adapters/headlessinput/input.go +6 -6
@@ -56,7 +56,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
5656 if security != nil {
5757 var deliver bool
5858 var err error
59- key, deliver, err = km.KeySecure(code, down, msec(monotonic()), state)
59+ key, deliver, err = km.KeySecure(code, down, monotonic(), state)
6060 if err != nil {
6161 return err
6262 }
@@ -64,7 +64,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
6464 return nil
6565 }
6666 } else {
67- key = km.Key(code, down, msec(monotonic()))
67+ key = km.Key(code, down, monotonic())
6868 }
6969 ev := stamp(key, state)
7070 select {
@@ -154,7 +154,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
154154 }
155155 return 0
156156 }
157- now := msec(monotonic())
157+ now := monotonic()
158158 if len(fields) > 0 && fields[0] == "move" {
159159 if len(fields) == 3 {
160160 x, ex := strconv.ParseFloat(fields[1], 64)
@@ -164,7 +164,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
164164 tx, ty := o.ToTarget(x, y)
165165 moveCursor(o.Info.Name, tx, ty)
166166 }
167- if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil {
167+ if err := sendPointer(ports.PointerMotion{X: x, Y: y, Time: now}); err != nil {
168168 return err
169169 }
170170 continue
@@ -205,12 +205,12 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
205205 continue
206206 }
207207 if fields[0] != "up" {
208- if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, TimeMsec: now}); err != nil {
208+ if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, Time: now}); err != nil {
209209 return err
210210 }
211211 }
212212 if fields[0] != "down" {
213- if err := sendPointer(ports.PointerButton{Button: code, TimeMsec: now}); err != nil {
213+ if err := sendPointer(ports.PointerButton{Button: code, Time: now}); err != nil {
214214 return err
215215 }
216216 }
@@ -56,7 +56,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
56 if security != nil {56 if security != nil {
57 var deliver bool57 var deliver bool
58 var err error58 var err error
59- key, deliver, err = km.KeySecure(code, down, msec(monotonic()), state)59+ key, deliver, err = km.KeySecure(code, down, monotonic(), state)
60 if err != nil {60 if err != nil {
61 return err61 return err
62 }62 }
@@ -64,7 +64,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
64 return nil64 return nil
65 }65 }
66 } else {66 } else {
67- key = km.Key(code, down, msec(monotonic()))67+ key = km.Key(code, down, monotonic())
68 }68 }
69 ev := stamp(key, state)69 ev := stamp(key, state)
70 select {70 select {
@@ -154,7 +154,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
154 }154 }
155 return 0155 return 0
156 }156 }
157- now := msec(monotonic())157+ now := monotonic()
158 if len(fields) > 0 && fields[0] == "move" {158 if len(fields) > 0 && fields[0] == "move" {
159 if len(fields) == 3 {159 if len(fields) == 3 {
160 x, ex := strconv.ParseFloat(fields[1], 64)160 x, ex := strconv.ParseFloat(fields[1], 64)
@@ -164,7 +164,7 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
164 tx, ty := o.ToTarget(x, y)164 tx, ty := o.ToTarget(x, y)
165 moveCursor(o.Info.Name, tx, ty)165 moveCursor(o.Info.Name, tx, ty)
166 }166 }
167- if err := sendPointer(ports.PointerMotion{X: x, Y: y, TimeMsec: now}); err != nil {167+ if err := sendPointer(ports.PointerMotion{X: x, Y: y, Time: now}); err != nil {
168 return err168 return err
169 }169 }
170 continue170 continue
@@ -205,12 +205,12 @@ func RunSecure(ctx context.Context, km *xkb.Keymap, keymaps <-chan *xkb.Keymap,
205 continue205 continue
206 }206 }
207 if fields[0] != "up" {207 if fields[0] != "up" {
208- if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, TimeMsec: now}); err != nil {208+ if err := sendPointer(ports.PointerButton{Button: code, Pressed: true, Time: now}); err != nil {
209 return err209 return err
210 }210 }
211 }211 }
212 if fields[0] != "down" {212 if fields[0] != "down" {
213- if err := sendPointer(ports.PointerButton{Button: code, TimeMsec: now}); err != nil {213+ if err := sendPointer(ports.PointerButton{Button: code, Time: now}); err != nil {
214 return err214 return err
215 }215 }
216 }216 }
M internal/adapters/headlessinput/swipe.go +0 -2
@@ -33,5 +33,3 @@ func monotonic() time.Duration {
3333 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
3434 return time.Duration(ts.Nano())
3535 }
36-
37-func msec(d time.Duration) uint32 { return uint32(d / time.Millisecond) }
@@ -33,5 +33,3 @@ func monotonic() time.Duration {
33 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)33 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
34 return time.Duration(ts.Nano())34 return time.Duration(ts.Nano())
35 }35 }
36-
37-func msec(d time.Duration) uint32 { return uint32(d / time.Millisecond) }
M internal/adapters/libinput/libinput.go +10 -14
@@ -511,9 +511,6 @@ func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputE
511511 // usec is a libinput timestamp as a duration.
512512 func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond }
513513
514-// msec is a libinput timestamp in wayland's millisecond clock.
515-func msec(v uint64) uint32 { return uint32(v / 1000) }
516-
517514 // translate converts one libinput event. Timestamps are the device's
518515 // (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on
519516 // when events happened, not when they were read.
@@ -569,13 +566,12 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
569566 case evKeyboardKey:
570567 k := keyboardEvent(ev)
571568 code, pressed := keyboardKey(k), keyboardState(k) == 1
572- return translateKeyboard(code, pressed, msec(keyboardUsec(k)), opts, state)
569+ return translateKeyboard(code, pressed, usec(keyboardUsec(k)), opts, state)
573570 case evPointerMotion:
574571 pe := pointerEvent(ev)
575572 m := p.move(pointerDX(pe), pointerDY(pe))
576573 m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)
577- m.TimeUsec = pointerUsec(pe)
578- m.TimeMsec = msec(m.TimeUsec)
574+ m.Time = usec(pointerUsec(pe))
579575 p.moved(opts.MoveCursor, true)
580576 if opts.LogMotion {
581577 log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
@@ -584,24 +580,24 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
584580 case evPointerAbs:
585581 // Absolute devices (tablets, VMs) map to the whole layout.
586582 pe := pointerEvent(ev)
587- now := msec(pointerUsec(pe))
583+ now := usec(pointerUsec(pe))
588584 b := p.bounds()
589585 if p.constraint.Mode == ports.ConstraintLock {
590- return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil
586+ return ports.PointerMotion{X: p.x, Y: p.y, Time: now}, nil
591587 }
592588 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
593589 p.moved(opts.MoveCursor, true)
594- return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil
590+ return ports.PointerMotion{X: x, Y: y, Time: now}, nil
595591 case evPointerButton:
596592 pe := pointerEvent(ev)
597593 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
598594 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
599- return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: msec(pointerUsec(pe))}, nil
595+ return ports.PointerButton{Button: b, Pressed: pressed, Time: usec(pointerUsec(pe))}, nil
600596 case evScrollWheel, evScrollFinger, evScrollCont:
601597 pe := pointerEvent(ev)
602598 // The three scroll events follow each other as the sources do.
603599 source := ports.AxisSource(eventType(ev) - evScrollWheel)
604- a := ports.PointerAxis{Source: source, TimeMsec: msec(pointerUsec(pe))}
600+ a := ports.PointerAxis{Source: source, Time: usec(pointerUsec(pe))}
605601 // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
606602 for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
607603 if pointerHasAxis(pe, uint32(i)) == 0 {
@@ -626,17 +622,17 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
626622
627623 // translateKeyboard applies the production epoch before native translation.
628624 // Quarantined events never reach logging, reserved hotkeys or core.
629-func translateKeyboard(code uint32, pressed bool, timeMsec uint32, opts Options, state ports.SecurityState) (ports.InputEvent, error) {
625+func translateKeyboard(code uint32, pressed bool, t time.Duration, opts Options, state ports.SecurityState) (ports.InputEvent, error) {
630626 var ke ports.KeyEvent
631627 if opts.Security != nil {
632628 var deliver bool
633629 var err error
634- ke, deliver, err = opts.Keymap.KeySecure(code, pressed, timeMsec, state)
630+ ke, deliver, err = opts.Keymap.KeySecure(code, pressed, t, state)
635631 if err != nil || !deliver {
636632 return nil, err
637633 }
638634 } else {
639- ke = opts.Keymap.Key(code, pressed, timeMsec)
635+ ke = opts.Keymap.Key(code, pressed, t)
640636 }
641637 return translateKey(ke, opts, state)
642638 }
@@ -511,9 +511,6 @@ func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputE
511 // usec is a libinput timestamp as a duration.511 // usec is a libinput timestamp as a duration.
512 func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond }512 func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond }
513 513
514-// msec is a libinput timestamp in wayland's millisecond clock.
515-func msec(v uint64) uint32 { return uint32(v / 1000) }
516-
517 // translate converts one libinput event. Timestamps are the device's514 // translate converts one libinput event. Timestamps are the device's
518 // (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on515 // (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on
519 // when events happened, not when they were read.516 // when events happened, not when they were read.
@@ -569,13 +566,12 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
569 case evKeyboardKey:566 case evKeyboardKey:
570 k := keyboardEvent(ev)567 k := keyboardEvent(ev)
571 code, pressed := keyboardKey(k), keyboardState(k) == 1568 code, pressed := keyboardKey(k), keyboardState(k) == 1
572- return translateKeyboard(code, pressed, msec(keyboardUsec(k)), opts, state)569+ return translateKeyboard(code, pressed, usec(keyboardUsec(k)), opts, state)
573 case evPointerMotion:570 case evPointerMotion:
574 pe := pointerEvent(ev)571 pe := pointerEvent(ev)
575 m := p.move(pointerDX(pe), pointerDY(pe))572 m := p.move(pointerDX(pe), pointerDY(pe))
576 m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)573 m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)
577- m.TimeUsec = pointerUsec(pe)574+ m.Time = usec(pointerUsec(pe))
578- m.TimeMsec = msec(m.TimeUsec)
579 p.moved(opts.MoveCursor, true)575 p.moved(opts.MoveCursor, true)
580 if opts.LogMotion {576 if opts.LogMotion {
581 log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")577 log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
@@ -584,24 +580,24 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
584 case evPointerAbs:580 case evPointerAbs:
585 // Absolute devices (tablets, VMs) map to the whole layout.581 // Absolute devices (tablets, VMs) map to the whole layout.
586 pe := pointerEvent(ev)582 pe := pointerEvent(ev)
587- now := msec(pointerUsec(pe))583+ now := usec(pointerUsec(pe))
588 b := p.bounds()584 b := p.bounds()
589 if p.constraint.Mode == ports.ConstraintLock {585 if p.constraint.Mode == ports.ConstraintLock {
590- return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil586+ return ports.PointerMotion{X: p.x, Y: p.y, Time: now}, nil
591 }587 }
592 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))588 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
593 p.moved(opts.MoveCursor, true)589 p.moved(opts.MoveCursor, true)
594- return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil590+ return ports.PointerMotion{X: x, Y: y, Time: now}, nil
595 case evPointerButton:591 case evPointerButton:
596 pe := pointerEvent(ev)592 pe := pointerEvent(ev)
597 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1593 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
598 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")594 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
599- return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: msec(pointerUsec(pe))}, nil595+ return ports.PointerButton{Button: b, Pressed: pressed, Time: usec(pointerUsec(pe))}, nil
600 case evScrollWheel, evScrollFinger, evScrollCont:596 case evScrollWheel, evScrollFinger, evScrollCont:
601 pe := pointerEvent(ev)597 pe := pointerEvent(ev)
602 // The three scroll events follow each other as the sources do.598 // The three scroll events follow each other as the sources do.
603 source := ports.AxisSource(eventType(ev) - evScrollWheel)599 source := ports.AxisSource(eventType(ev) - evScrollWheel)
604- a := ports.PointerAxis{Source: source, TimeMsec: msec(pointerUsec(pe))}600+ a := ports.PointerAxis{Source: source, Time: usec(pointerUsec(pe))}
605 // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.601 // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
606 for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {602 for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
607 if pointerHasAxis(pe, uint32(i)) == 0 {603 if pointerHasAxis(pe, uint32(i)) == 0 {
@@ -626,17 +622,17 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
626 622
627 // translateKeyboard applies the production epoch before native translation.623 // translateKeyboard applies the production epoch before native translation.
628 // Quarantined events never reach logging, reserved hotkeys or core.624 // Quarantined events never reach logging, reserved hotkeys or core.
629-func translateKeyboard(code uint32, pressed bool, timeMsec uint32, opts Options, state ports.SecurityState) (ports.InputEvent, error) {625+func translateKeyboard(code uint32, pressed bool, t time.Duration, opts Options, state ports.SecurityState) (ports.InputEvent, error) {
630 var ke ports.KeyEvent626 var ke ports.KeyEvent
631 if opts.Security != nil {627 if opts.Security != nil {
632 var deliver bool628 var deliver bool
633 var err error629 var err error
634- ke, deliver, err = opts.Keymap.KeySecure(code, pressed, timeMsec, state)630+ ke, deliver, err = opts.Keymap.KeySecure(code, pressed, t, state)
635 if err != nil || !deliver {631 if err != nil || !deliver {
636 return nil, err632 return nil, err
637 }633 }
638 } else {634 } else {
639- ke = opts.Keymap.Key(code, pressed, timeMsec)635+ ke = opts.Keymap.Key(code, pressed, t)
640 }636 }
641 return translateKey(ke, opts, state)637 return translateKey(ke, opts, state)
642 }638 }
M internal/adapters/libinput/libinput_test.go +3 -3
@@ -107,8 +107,8 @@ func TestAccelProfile(t *testing.T) {
107107 }
108108
109109 func TestTimestamps(t *testing.T) {
110- if usec(1500) != 1500*time.Microsecond || msec(1_234_567) != 1234 {
111- t.Fatal(usec(1500), msec(1_234_567))
110+ if usec(1500) != 1500*time.Microsecond {
111+ t.Fatal(usec(1500))
112112 }
113113 }
114114
@@ -301,7 +301,7 @@ func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) {
301301 produce := func(code uint32, down bool) ports.InputEvent {
302302 t.Helper()
303303 state := securitySnapshot(opts.Security)
304- ev, err := translateKeyboard(code, down, 123, opts, state)
304+ ev, err := translateKeyboard(code, down, 123*time.Millisecond, opts, state)
305305 if err != nil {
306306 t.Fatal(err)
307307 }
@@ -107,8 +107,8 @@ func TestAccelProfile(t *testing.T) {
107 }107 }
108 108
109 func TestTimestamps(t *testing.T) {109 func TestTimestamps(t *testing.T) {
110- if usec(1500) != 1500*time.Microsecond || msec(1_234_567) != 1234 {110+ if usec(1500) != 1500*time.Microsecond {
111- t.Fatal(usec(1500), msec(1_234_567))111+ t.Fatal(usec(1500))
112 }112 }
113 }113 }
114 114
@@ -301,7 +301,7 @@ func TestKeyboardProducerQuarantinesBeforeHotkeysAndLogging(t *testing.T) {
301 produce := func(code uint32, down bool) ports.InputEvent {301 produce := func(code uint32, down bool) ports.InputEvent {
302 t.Helper()302 t.Helper()
303 state := securitySnapshot(opts.Security)303 state := securitySnapshot(opts.Security)
304- ev, err := translateKeyboard(code, down, 123, opts, state)304+ ev, err := translateKeyboard(code, down, 123*time.Millisecond, opts, state)
305 if err != nil {305 if err != nil {
306 t.Fatal(err)306 t.Fatal(err)
307 }307 }
M internal/adapters/wayland/constraints.go +4 -1
@@ -1,6 +1,8 @@
11 package wayland
22
33 import (
4+ "time"
5+
46 "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
57 "github.com/bnema/go-wayland-bindings/server/pointerwarp"
68 "github.com/bnema/go-wayland-bindings/server/relativepointer"
@@ -82,9 +84,10 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool {
8284 return false
8385 }
8486 sent := false
87+ usec := uint64(c.Time / time.Microsecond)
8588 for _, rel := range s.relatives[w.xdg.resource.Client()] {
8689 if rel.Resource.Alive() {
87- rel.SendRelativeMotion(uint32(c.TimeUsec>>32), uint32(c.TimeUsec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))
90+ rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))
8891 sent = true
8992 }
9093 }
@@ -1,6 +1,8 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "time"
5+
4 "github.com/bnema/go-wayland-bindings/server/pointerconstraints"6 "github.com/bnema/go-wayland-bindings/server/pointerconstraints"
5 "github.com/bnema/go-wayland-bindings/server/pointerwarp"7 "github.com/bnema/go-wayland-bindings/server/pointerwarp"
6 "github.com/bnema/go-wayland-bindings/server/relativepointer"8 "github.com/bnema/go-wayland-bindings/server/relativepointer"
@@ -82,9 +84,10 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool {
82 return false84 return false
83 }85 }
84 sent := false86 sent := false
87+ usec := uint64(c.Time / time.Microsecond)
85 for _, rel := range s.relatives[w.xdg.resource.Client()] {88 for _, rel := range s.relatives[w.xdg.resource.Client()] {
86 if rel.Resource.Alive() {89 if rel.Resource.Alive() {
87- rel.SendRelativeMotion(uint32(c.TimeUsec>>32), uint32(c.TimeUsec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))90+ rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY))
88 sent = true91 sent = true
89 }92 }
90 }93 }
M internal/adapters/wayland/globals.go +11 -14
@@ -2,6 +2,7 @@ package wayland
22
33 import (
44 "os"
5+ "slices"
56
67 "github.com/bnema/go-wayland-bindings/server/wayland"
78 "github.com/bnema/neferwl/internal/ports"
@@ -80,6 +81,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
8081 }
8182 })
8283 },
84+ func() error { return registerInputTimestamps(d, s) },
8385 func() error { return registerClipboard(d, s) },
8486 func() error { return registerFixes(d) },
8587 func() error { return registerAlphaModifier(d, s) },
@@ -433,13 +435,11 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
433435 s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)
434436 p.OnDestroy = func() {
435437 delete(s.seat.enters, p.Resource)
436- list := s.seat.pointers[r.Client()]
437- for i, item := range list {
438- if item == p {
439- list = append(list[:i], list[i+1:]...)
440- break
441- }
442- }
438+ // Subscribed timestamps objects become inert with the device.
439+ delete(s.seat.pointerStamps, p.Resource)
440+ // Copy on write: callers may be ranging over the old list (see
441+ // clientPointers).
442+ list := slices.DeleteFunc(slices.Clone(s.seat.pointers[r.Client()]), func(x *wayland.Pointer) bool { return x == p })
443443 if len(list) == 0 {
444444 delete(s.seat.pointers, r.Client())
445445 } else {
@@ -462,13 +462,10 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
462462 }
463463 s := h.server
464464 k.OnDestroy = func() {
465- list := s.seat.keyboards[r.Client()]
466- for i, item := range list {
467- if item == k {
468- list = append(list[:i], list[i+1:]...)
469- break
470- }
471- }
465+ delete(s.seat.keyStamps, k.Resource)
466+ // Copy on write: callers may be ranging over the old list (see
467+ // clientKeyboards).
468+ list := slices.DeleteFunc(slices.Clone(s.seat.keyboards[r.Client()]), func(x *wayland.Keyboard) bool { return x == k })
472469 if len(list) == 0 {
473470 delete(s.seat.keyboards, r.Client())
474471 } else {
@@ -2,6 +2,7 @@ package wayland
2 2
3 import (3 import (
4 "os"4 "os"
5+ "slices"
5 6
6 "github.com/bnema/go-wayland-bindings/server/wayland"7 "github.com/bnema/go-wayland-bindings/server/wayland"
7 "github.com/bnema/neferwl/internal/ports"8 "github.com/bnema/neferwl/internal/ports"
@@ -80,6 +81,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
80 }81 }
81 })82 })
82 },83 },
84+ func() error { return registerInputTimestamps(d, s) },
83 func() error { return registerClipboard(d, s) },85 func() error { return registerClipboard(d, s) },
84 func() error { return registerFixes(d) },86 func() error { return registerFixes(d) },
85 func() error { return registerAlphaModifier(d, s) },87 func() error { return registerAlphaModifier(d, s) },
@@ -433,13 +435,11 @@ func (h seat) GetPointer(r *wayland.Seat, id uint32) {
433 s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)435 s.seat.pointers[r.Client()] = append(s.seat.pointers[r.Client()], p)
434 p.OnDestroy = func() {436 p.OnDestroy = func() {
435 delete(s.seat.enters, p.Resource)437 delete(s.seat.enters, p.Resource)
436- list := s.seat.pointers[r.Client()]438+ // Subscribed timestamps objects become inert with the device.
437- for i, item := range list {439+ delete(s.seat.pointerStamps, p.Resource)
438- if item == p {440+ // Copy on write: callers may be ranging over the old list (see
439- list = append(list[:i], list[i+1:]...)441+ // clientPointers).
440- break442+ list := slices.DeleteFunc(slices.Clone(s.seat.pointers[r.Client()]), func(x *wayland.Pointer) bool { return x == p })
441- }
442- }
443 if len(list) == 0 {443 if len(list) == 0 {
444 delete(s.seat.pointers, r.Client())444 delete(s.seat.pointers, r.Client())
445 } else {445 } else {
@@ -462,13 +462,10 @@ func (h seat) GetKeyboard(r *wayland.Seat, id uint32) {
462 }462 }
463 s := h.server463 s := h.server
464 k.OnDestroy = func() {464 k.OnDestroy = func() {
465- list := s.seat.keyboards[r.Client()]465+ delete(s.seat.keyStamps, k.Resource)
466- for i, item := range list {466+ // Copy on write: callers may be ranging over the old list (see
467- if item == k {467+ // clientKeyboards).
468- list = append(list[:i], list[i+1:]...)468+ list := slices.DeleteFunc(slices.Clone(s.seat.keyboards[r.Client()]), func(x *wayland.Keyboard) bool { return x == k })
469- break
470- }
471- }
472 if len(list) == 0 {469 if len(list) == 0 {
473 delete(s.seat.keyboards, r.Client())470 delete(s.seat.keyboards, r.Client())
474 } else {471 } else {
A internal/adapters/wayland/inputtimestamps.go +101 -0
@@ -0,0 +1,101 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "time"
6+
7+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
8+ "github.com/bnema/go-wayland-bindings/server/wayland"
9+ "github.com/bnema/purego-libwayland/server"
10+)
11+
12+// zwp_input_timestamps_v1: a client subscribes a wl_keyboard or wl_pointer
13+// and gets the device's high-resolution timestamp (libinput microseconds, in
14+// the protocol's nanosecond fields) right before each key, motion,
15+// button, axis and axis_stop event it sends. Subscriptions are per device
16+// resource and die with the device (the object becomes inert) or when the
17+// client destroys the timestamps object. There are no touch events, so touch
18+// subscriptions only create an inert object.
19+//
20+// The stamp lists are replaced, never mutated, like the seat device lists.
21+
22+func registerInputTimestamps(d *server.Display, s *Server) error {
23+ return inputtimestamps.NewZwpInputTimestampsManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
24+ _, _ = inputtimestamps.NewZwpInputTimestampsManagerV1(c, int32(v), id, timestampsManager{s})
25+ })
26+}
27+
28+type timestampsManager struct{ server *Server }
29+
30+func (timestampsManager) Destroy(*inputtimestamps.ZwpInputTimestampsManagerV1) {}
31+
32+func (m timestampsManager) GetKeyboardTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, kb *wayland.Keyboard) {
33+ var dev *server.Resource
34+ if kb != nil {
35+ dev = kb.Resource
36+ }
37+ m.subscribe(r, id, dev, m.server.seat.keyStamps)
38+}
39+
40+func (m timestampsManager) GetPointerTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, p *wayland.Pointer) {
41+ var dev *server.Resource
42+ if p != nil {
43+ dev = p.Resource
44+ }
45+ m.subscribe(r, id, dev, m.server.seat.pointerStamps)
46+}
47+
48+// GetTouchTimestamps creates the object only: the seat has no touch events.
49+func (timestampsManager) GetTouchTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, _ *wayland.Touch) {
50+ _, _ = inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{})
51+}
52+
53+// subscribe creates the timestamps object for dev and records it in stamps.
54+// A missing or dead device leaves the object inert.
55+func (m timestampsManager) subscribe(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, dev *server.Resource, stamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1) {
56+ ts, err := inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{})
57+ if err != nil || dev == nil || !dev.Alive() {
58+ return
59+ }
60+ stamps[dev] = append(slices.Clone(stamps[dev]), ts)
61+ ts.OnDestroy = func() {
62+ // The device may have died first and dropped its entry.
63+ list, ok := stamps[dev]
64+ if !ok {
65+ return
66+ }
67+ list = slices.DeleteFunc(slices.Clone(list), func(x *inputtimestamps.ZwpInputTimestampsV1) bool { return x == ts })
68+ if len(list) == 0 {
69+ delete(stamps, dev)
70+ } else {
71+ stamps[dev] = list
72+ }
73+ }
74+}
75+
76+type timestampsHandler struct{}
77+
78+func (timestampsHandler) Destroy(*inputtimestamps.ZwpInputTimestampsV1) {}
79+
80+// stampKeyboard sends t to the subscribers of k; call it right before a
81+// wl_keyboard.key. It allocates nothing.
82+func (s *Server) stampKeyboard(k *wayland.Keyboard, t time.Duration) {
83+ sendStamps(s.seat.keyStamps[k.Resource], t)
84+}
85+
86+// stampPointer sends t to the subscribers of p; call it right before a
87+// wl_pointer motion, button, axis or axis_stop.
88+func (s *Server) stampPointer(p *wayland.Pointer, t time.Duration) {
89+ sendStamps(s.seat.pointerStamps[p.Resource], t)
90+}
91+
92+func sendStamps(list []*inputtimestamps.ZwpInputTimestampsV1, t time.Duration) {
93+ if len(list) == 0 {
94+ return
95+ }
96+ sec := uint64(t / time.Second)
97+ ns := uint32(t % time.Second)
98+ for _, ts := range list {
99+ ts.SendTimestamp(uint32(sec>>32), uint32(sec), ns)
100+ }
101+}
@@ -0,0 +1,101 @@
1+package wayland
2+
3+import (
4+ "slices"
5+ "time"
6+
7+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
8+ "github.com/bnema/go-wayland-bindings/server/wayland"
9+ "github.com/bnema/purego-libwayland/server"
10+)
11+
12+// zwp_input_timestamps_v1: a client subscribes a wl_keyboard or wl_pointer
13+// and gets the device's high-resolution timestamp (libinput microseconds, in
14+// the protocol's nanosecond fields) right before each key, motion,
15+// button, axis and axis_stop event it sends. Subscriptions are per device
16+// resource and die with the device (the object becomes inert) or when the
17+// client destroys the timestamps object. There are no touch events, so touch
18+// subscriptions only create an inert object.
19+//
20+// The stamp lists are replaced, never mutated, like the seat device lists.
21+
22+func registerInputTimestamps(d *server.Display, s *Server) error {
23+ return inputtimestamps.NewZwpInputTimestampsManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
24+ _, _ = inputtimestamps.NewZwpInputTimestampsManagerV1(c, int32(v), id, timestampsManager{s})
25+ })
26+}
27+
28+type timestampsManager struct{ server *Server }
29+
30+func (timestampsManager) Destroy(*inputtimestamps.ZwpInputTimestampsManagerV1) {}
31+
32+func (m timestampsManager) GetKeyboardTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, kb *wayland.Keyboard) {
33+ var dev *server.Resource
34+ if kb != nil {
35+ dev = kb.Resource
36+ }
37+ m.subscribe(r, id, dev, m.server.seat.keyStamps)
38+}
39+
40+func (m timestampsManager) GetPointerTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, p *wayland.Pointer) {
41+ var dev *server.Resource
42+ if p != nil {
43+ dev = p.Resource
44+ }
45+ m.subscribe(r, id, dev, m.server.seat.pointerStamps)
46+}
47+
48+// GetTouchTimestamps creates the object only: the seat has no touch events.
49+func (timestampsManager) GetTouchTimestamps(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, _ *wayland.Touch) {
50+ _, _ = inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{})
51+}
52+
53+// subscribe creates the timestamps object for dev and records it in stamps.
54+// A missing or dead device leaves the object inert.
55+func (m timestampsManager) subscribe(r *inputtimestamps.ZwpInputTimestampsManagerV1, id uint32, dev *server.Resource, stamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1) {
56+ ts, err := inputtimestamps.NewZwpInputTimestampsV1(r.Client(), r.Version(), id, timestampsHandler{})
57+ if err != nil || dev == nil || !dev.Alive() {
58+ return
59+ }
60+ stamps[dev] = append(slices.Clone(stamps[dev]), ts)
61+ ts.OnDestroy = func() {
62+ // The device may have died first and dropped its entry.
63+ list, ok := stamps[dev]
64+ if !ok {
65+ return
66+ }
67+ list = slices.DeleteFunc(slices.Clone(list), func(x *inputtimestamps.ZwpInputTimestampsV1) bool { return x == ts })
68+ if len(list) == 0 {
69+ delete(stamps, dev)
70+ } else {
71+ stamps[dev] = list
72+ }
73+ }
74+}
75+
76+type timestampsHandler struct{}
77+
78+func (timestampsHandler) Destroy(*inputtimestamps.ZwpInputTimestampsV1) {}
79+
80+// stampKeyboard sends t to the subscribers of k; call it right before a
81+// wl_keyboard.key. It allocates nothing.
82+func (s *Server) stampKeyboard(k *wayland.Keyboard, t time.Duration) {
83+ sendStamps(s.seat.keyStamps[k.Resource], t)
84+}
85+
86+// stampPointer sends t to the subscribers of p; call it right before a
87+// wl_pointer motion, button, axis or axis_stop.
88+func (s *Server) stampPointer(p *wayland.Pointer, t time.Duration) {
89+ sendStamps(s.seat.pointerStamps[p.Resource], t)
90+}
91+
92+func sendStamps(list []*inputtimestamps.ZwpInputTimestampsV1, t time.Duration) {
93+ if len(list) == 0 {
94+ return
95+ }
96+ sec := uint64(t / time.Second)
97+ ns := uint32(t % time.Second)
98+ for _, ts := range list {
99+ ts.SendTimestamp(uint32(sec>>32), uint32(sec), ns)
100+ }
101+}
A internal/adapters/wayland/inputtimestamps_test.go +257 -0
@@ -0,0 +1,257 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "slices"
6+ "sync"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
11+ "github.com/bnema/go-wayland-bindings/server/wayland"
12+ "github.com/bnema/neferwl/internal/ports"
13+ "github.com/bnema/wlturbo"
14+)
15+
16+// stampRecorder is the ordered log shared by the proxies of one client, so a
17+// test sees whether a timestamp comes right before its event.
18+type stampRecorder struct {
19+ mu sync.Mutex
20+ lines []string
21+}
22+
23+func (r *stampRecorder) add(format string, args ...any) {
24+ r.mu.Lock()
25+ defer r.mu.Unlock()
26+ r.lines = append(r.lines, fmt.Sprintf(format, args...))
27+}
28+
29+func (r *stampRecorder) snapshot() []string {
30+ r.mu.Lock()
31+ defer r.mu.Unlock()
32+ return slices.Clone(r.lines)
33+}
34+
35+// wait round-trips until the log holds n lines.
36+func (r *stampRecorder) wait(t *testing.T, c *wlturbo.Display, n int) []string {
37+ t.Helper()
38+ deadline := time.Now().Add(2 * time.Second)
39+ for {
40+ if err := c.Roundtrip(); err != nil {
41+ t.Fatal(err)
42+ }
43+ if got := r.snapshot(); len(got) >= n {
44+ return got
45+ }
46+ if time.Now().After(deadline) {
47+ t.Fatalf("log has %v, want %d lines", r.snapshot(), n)
48+ }
49+ time.Sleep(5 * time.Millisecond)
50+ }
51+}
52+
53+// stampedKeyboard logs the key events of one wl_keyboard under its tag.
54+type stampedKeyboard struct {
55+ wlturbo.BaseProxy
56+ tag string
57+ log *stampRecorder
58+}
59+
60+func (p *stampedKeyboard) Dispatch(e *wlturbo.Event) {
61+ if e.Opcode == uint16(wayland.KeyboardEventKey) {
62+ _ = e.Uint32() // serial
63+ p.log.add("%s key %d", p.tag, e.Uint32())
64+ }
65+}
66+
67+// stampedPointer logs the motion, button and axis events of a wl_pointer.
68+type stampedPointer struct {
69+ wlturbo.BaseProxy
70+ log *stampRecorder
71+}
72+
73+func (p *stampedPointer) Dispatch(e *wlturbo.Event) {
74+ switch e.Opcode {
75+ case uint16(wayland.PointerEventMotion):
76+ p.log.add("motion %d", e.Uint32())
77+ case uint16(wayland.PointerEventButton):
78+ _ = e.Uint32() // serial
79+ p.log.add("button %d", e.Uint32())
80+ case uint16(wayland.PointerEventAxis):
81+ p.log.add("axis %d", e.Uint32())
82+ case uint16(wayland.PointerEventAxisStop):
83+ p.log.add("axis_stop %d", e.Uint32())
84+ }
85+}
86+
87+// stampProxy logs zwp_input_timestamps_v1.timestamp events.
88+type stampProxy struct {
89+ wlturbo.BaseProxy
90+ log *stampRecorder
91+}
92+
93+func (p *stampProxy) Dispatch(e *wlturbo.Event) {
94+ if e.Opcode == uint16(inputtimestamps.ZwpInputTimestampsV1EventTimestamp) {
95+ hi, lo, ns := e.Uint32(), e.Uint32(), e.Uint32()
96+ p.log.add("ts %d %d %d", hi, lo, ns)
97+ }
98+}
99+
100+func stampSubscription(t *testing.T, c *wlturbo.Display, log *stampRecorder, manager uint32, request uint32, device uint32) uint32 {
101+ t.Helper()
102+ id := c.AllocateID()
103+ p := &stampProxy{log: log}
104+ p.SetID(id)
105+ registerWireProxy(c, p)
106+ requestProtocol(t, c, manager, request, id, device)
107+ return id
108+}
109+
110+func TestInputTimestampsGlobal(t *testing.T) {
111+ s, _, _, dir := keyboardServer(t)
112+ c := protocolClient(t, s, dir)
113+ g, ok := c.Registry().FindGlobal("zwp_input_timestamps_manager_v1")
114+ if !ok || g.Version != 1 {
115+ t.Fatalf("zwp_input_timestamps_manager_v1: %+v, %v", g, ok)
116+ }
117+}
118+
119+func TestInputTimestampsKeyboard(t *testing.T) {
120+ s, events, commands, dir := keyboardServer(t)
121+ c := protocolClient(t, s, dir)
122+ log := &stampRecorder{}
123+ seat := bindProtocol(t, c, "wl_seat")
124+ registerProtocol(t, c, seat)
125+ var keyboards [2]uint32
126+ for i, tag := range []string{"subscribed", "plain"} {
127+ keyboards[i] = c.AllocateID()
128+ kp := &stampedKeyboard{tag: tag, log: log}
129+ kp.SetID(keyboards[i])
130+ registerWireProxy(c, kp)
131+ requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboards[i])
132+ }
133+ manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
134+ ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetKeyboardTimestamps, keyboards[0])
135+ w := toplevelMapper(t, c, events)()
136+ commands <- ports.FocusWindow{ID: w.ID}
137+
138+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 5*time.Second + 123456789}}
139+ got := log.wait(t, c, 3)
140+ want := []string{"ts 0 5 123456789", "subscribed key 5123", "plain key 5123"}
141+ if !slices.Equal(got, want) {
142+ t.Fatalf("log %v, want %v", got, want)
143+ }
144+
145+ // Seconds past 32 bits go to sec_hi.
146+ big := time.Duration(1<<32+7) * time.Second
147+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Time: big}}
148+ ms := uint64(big / time.Millisecond)
149+ got = log.wait(t, c, 6)
150+ want = append(want, "ts 1 7 0", fmt.Sprint("subscribed key ", uint32(ms)), fmt.Sprint("plain key ", uint32(ms)))
151+ if !slices.Equal(got, want) {
152+ t.Fatalf("log %v, want %v", got, want)
153+ }
154+
155+ // A destroyed timestamps object sends nothing more.
156+ requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
157+ if err := c.Roundtrip(); err != nil {
158+ t.Fatal(err)
159+ }
160+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 31, Pressed: true, Time: 9 * time.Second}}
161+ got = log.wait(t, c, 8)
162+ want = append(want, "subscribed key 9000", "plain key 9000")
163+ if !slices.Equal(got, want) {
164+ t.Fatalf("log %v, want %v", got, want)
165+ }
166+}
167+
168+func TestInputTimestampsPointer(t *testing.T) {
169+ s, events, commands, dir := keyboardServer(t)
170+ c := protocolClient(t, s, dir)
171+ log := &stampRecorder{}
172+ seat := bindVersion(t, c, "wl_seat", 8)
173+ registerProtocol(t, c, seat)
174+ pointer := c.AllocateID()
175+ pp := &stampedPointer{log: log}
176+ pp.SetID(pointer)
177+ registerWireProxy(c, pp)
178+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
179+ manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
180+ ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
181+ w := toplevelMapper(t, c, events)()
182+ commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
183+
184+ commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0, Time: 2*time.Second + time.Millisecond}
185+ commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 3 * time.Second}
186+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: 5}, Time: 4 * time.Second}}
187+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 5 * time.Second}}
188+ got := log.wait(t, c, 8)
189+ want := []string{
190+ "ts 0 2 1000000", "motion 2001",
191+ "ts 0 3 0", "button 3000",
192+ "ts 0 4 0", "axis 4000",
193+ "ts 0 5 0", "axis_stop 5000",
194+ }
195+ if !slices.Equal(got, want) {
196+ t.Fatalf("log %v, want %v", got, want)
197+ }
198+
199+ // The stamped input path allocates nothing, with a live subscriber.
200+ var allocs float64
201+ s.display.Do(func() {
202+ allocs = testing.AllocsPerRun(100, func() {
203+ s.applyInput(ports.PointerMotionTo{ID: w.ID, Time: 6 * time.Second})
204+ })
205+ })
206+ if allocs != 0 {
207+ t.Fatalf("stamped motion allocates %v", allocs)
208+ }
209+
210+ // Releasing the pointer makes the object inert and drops its entry;
211+ // destroying it afterwards is still fine.
212+ requestProtocol(t, c, pointer, wayland.PointerRequestRelease)
213+ if err := c.Roundtrip(); err != nil {
214+ t.Fatal(err)
215+ }
216+ if n := stampEntries(s); n != 0 {
217+ t.Fatalf("%d stamp entries after the pointer died", n)
218+ }
219+ requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
220+ if err := c.Roundtrip(); err != nil {
221+ t.Fatal(err)
222+ }
223+}
224+
225+// Destroying the timestamps object before its device drops the entry too.
226+func TestInputTimestampsDestroyBeforeDevice(t *testing.T) {
227+ s, _, _, dir := keyboardServer(t)
228+ c := protocolClient(t, s, dir)
229+ log := &stampRecorder{}
230+ seat := bindProtocol(t, c, "wl_seat")
231+ registerProtocol(t, c, seat)
232+ pointer := c.AllocateID()
233+ registerProtocol(t, c, pointer)
234+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
235+ manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
236+ ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
237+ if err := c.Roundtrip(); err != nil {
238+ t.Fatal(err)
239+ }
240+ if n := stampEntries(s); n != 1 {
241+ t.Fatalf("%d stamp entries, want 1", n)
242+ }
243+ requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
244+ if err := c.Roundtrip(); err != nil {
245+ t.Fatal(err)
246+ }
247+ if n := stampEntries(s); n != 0 {
248+ t.Fatalf("%d stamp entries after destroy", n)
249+ }
250+}
251+
252+// stampEntries counts the devices with subscriptions, on the display goroutine.
253+func stampEntries(s *Server) int {
254+ var n int
255+ s.display.Do(func() { n = len(s.seat.keyStamps) + len(s.seat.pointerStamps) })
256+ return n
257+}
@@ -0,0 +1,257 @@
1+package wayland
2+
3+import (
4+ "fmt"
5+ "slices"
6+ "sync"
7+ "testing"
8+ "time"
9+
10+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
11+ "github.com/bnema/go-wayland-bindings/server/wayland"
12+ "github.com/bnema/neferwl/internal/ports"
13+ "github.com/bnema/wlturbo"
14+)
15+
16+// stampRecorder is the ordered log shared by the proxies of one client, so a
17+// test sees whether a timestamp comes right before its event.
18+type stampRecorder struct {
19+ mu sync.Mutex
20+ lines []string
21+}
22+
23+func (r *stampRecorder) add(format string, args ...any) {
24+ r.mu.Lock()
25+ defer r.mu.Unlock()
26+ r.lines = append(r.lines, fmt.Sprintf(format, args...))
27+}
28+
29+func (r *stampRecorder) snapshot() []string {
30+ r.mu.Lock()
31+ defer r.mu.Unlock()
32+ return slices.Clone(r.lines)
33+}
34+
35+// wait round-trips until the log holds n lines.
36+func (r *stampRecorder) wait(t *testing.T, c *wlturbo.Display, n int) []string {
37+ t.Helper()
38+ deadline := time.Now().Add(2 * time.Second)
39+ for {
40+ if err := c.Roundtrip(); err != nil {
41+ t.Fatal(err)
42+ }
43+ if got := r.snapshot(); len(got) >= n {
44+ return got
45+ }
46+ if time.Now().After(deadline) {
47+ t.Fatalf("log has %v, want %d lines", r.snapshot(), n)
48+ }
49+ time.Sleep(5 * time.Millisecond)
50+ }
51+}
52+
53+// stampedKeyboard logs the key events of one wl_keyboard under its tag.
54+type stampedKeyboard struct {
55+ wlturbo.BaseProxy
56+ tag string
57+ log *stampRecorder
58+}
59+
60+func (p *stampedKeyboard) Dispatch(e *wlturbo.Event) {
61+ if e.Opcode == uint16(wayland.KeyboardEventKey) {
62+ _ = e.Uint32() // serial
63+ p.log.add("%s key %d", p.tag, e.Uint32())
64+ }
65+}
66+
67+// stampedPointer logs the motion, button and axis events of a wl_pointer.
68+type stampedPointer struct {
69+ wlturbo.BaseProxy
70+ log *stampRecorder
71+}
72+
73+func (p *stampedPointer) Dispatch(e *wlturbo.Event) {
74+ switch e.Opcode {
75+ case uint16(wayland.PointerEventMotion):
76+ p.log.add("motion %d", e.Uint32())
77+ case uint16(wayland.PointerEventButton):
78+ _ = e.Uint32() // serial
79+ p.log.add("button %d", e.Uint32())
80+ case uint16(wayland.PointerEventAxis):
81+ p.log.add("axis %d", e.Uint32())
82+ case uint16(wayland.PointerEventAxisStop):
83+ p.log.add("axis_stop %d", e.Uint32())
84+ }
85+}
86+
87+// stampProxy logs zwp_input_timestamps_v1.timestamp events.
88+type stampProxy struct {
89+ wlturbo.BaseProxy
90+ log *stampRecorder
91+}
92+
93+func (p *stampProxy) Dispatch(e *wlturbo.Event) {
94+ if e.Opcode == uint16(inputtimestamps.ZwpInputTimestampsV1EventTimestamp) {
95+ hi, lo, ns := e.Uint32(), e.Uint32(), e.Uint32()
96+ p.log.add("ts %d %d %d", hi, lo, ns)
97+ }
98+}
99+
100+func stampSubscription(t *testing.T, c *wlturbo.Display, log *stampRecorder, manager uint32, request uint32, device uint32) uint32 {
101+ t.Helper()
102+ id := c.AllocateID()
103+ p := &stampProxy{log: log}
104+ p.SetID(id)
105+ registerWireProxy(c, p)
106+ requestProtocol(t, c, manager, request, id, device)
107+ return id
108+}
109+
110+func TestInputTimestampsGlobal(t *testing.T) {
111+ s, _, _, dir := keyboardServer(t)
112+ c := protocolClient(t, s, dir)
113+ g, ok := c.Registry().FindGlobal("zwp_input_timestamps_manager_v1")
114+ if !ok || g.Version != 1 {
115+ t.Fatalf("zwp_input_timestamps_manager_v1: %+v, %v", g, ok)
116+ }
117+}
118+
119+func TestInputTimestampsKeyboard(t *testing.T) {
120+ s, events, commands, dir := keyboardServer(t)
121+ c := protocolClient(t, s, dir)
122+ log := &stampRecorder{}
123+ seat := bindProtocol(t, c, "wl_seat")
124+ registerProtocol(t, c, seat)
125+ var keyboards [2]uint32
126+ for i, tag := range []string{"subscribed", "plain"} {
127+ keyboards[i] = c.AllocateID()
128+ kp := &stampedKeyboard{tag: tag, log: log}
129+ kp.SetID(keyboards[i])
130+ registerWireProxy(c, kp)
131+ requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboards[i])
132+ }
133+ manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
134+ ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetKeyboardTimestamps, keyboards[0])
135+ w := toplevelMapper(t, c, events)()
136+ commands <- ports.FocusWindow{ID: w.ID}
137+
138+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 5*time.Second + 123456789}}
139+ got := log.wait(t, c, 3)
140+ want := []string{"ts 0 5 123456789", "subscribed key 5123", "plain key 5123"}
141+ if !slices.Equal(got, want) {
142+ t.Fatalf("log %v, want %v", got, want)
143+ }
144+
145+ // Seconds past 32 bits go to sec_hi.
146+ big := time.Duration(1<<32+7) * time.Second
147+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Time: big}}
148+ ms := uint64(big / time.Millisecond)
149+ got = log.wait(t, c, 6)
150+ want = append(want, "ts 1 7 0", fmt.Sprint("subscribed key ", uint32(ms)), fmt.Sprint("plain key ", uint32(ms)))
151+ if !slices.Equal(got, want) {
152+ t.Fatalf("log %v, want %v", got, want)
153+ }
154+
155+ // A destroyed timestamps object sends nothing more.
156+ requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
157+ if err := c.Roundtrip(); err != nil {
158+ t.Fatal(err)
159+ }
160+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 31, Pressed: true, Time: 9 * time.Second}}
161+ got = log.wait(t, c, 8)
162+ want = append(want, "subscribed key 9000", "plain key 9000")
163+ if !slices.Equal(got, want) {
164+ t.Fatalf("log %v, want %v", got, want)
165+ }
166+}
167+
168+func TestInputTimestampsPointer(t *testing.T) {
169+ s, events, commands, dir := keyboardServer(t)
170+ c := protocolClient(t, s, dir)
171+ log := &stampRecorder{}
172+ seat := bindVersion(t, c, "wl_seat", 8)
173+ registerProtocol(t, c, seat)
174+ pointer := c.AllocateID()
175+ pp := &stampedPointer{log: log}
176+ pp.SetID(pointer)
177+ registerWireProxy(c, pp)
178+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
179+ manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
180+ ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
181+ w := toplevelMapper(t, c, events)()
182+ commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
183+
184+ commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0, Time: 2*time.Second + time.Millisecond}
185+ commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 3 * time.Second}
186+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: 5}, Time: 4 * time.Second}}
187+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 5 * time.Second}}
188+ got := log.wait(t, c, 8)
189+ want := []string{
190+ "ts 0 2 1000000", "motion 2001",
191+ "ts 0 3 0", "button 3000",
192+ "ts 0 4 0", "axis 4000",
193+ "ts 0 5 0", "axis_stop 5000",
194+ }
195+ if !slices.Equal(got, want) {
196+ t.Fatalf("log %v, want %v", got, want)
197+ }
198+
199+ // The stamped input path allocates nothing, with a live subscriber.
200+ var allocs float64
201+ s.display.Do(func() {
202+ allocs = testing.AllocsPerRun(100, func() {
203+ s.applyInput(ports.PointerMotionTo{ID: w.ID, Time: 6 * time.Second})
204+ })
205+ })
206+ if allocs != 0 {
207+ t.Fatalf("stamped motion allocates %v", allocs)
208+ }
209+
210+ // Releasing the pointer makes the object inert and drops its entry;
211+ // destroying it afterwards is still fine.
212+ requestProtocol(t, c, pointer, wayland.PointerRequestRelease)
213+ if err := c.Roundtrip(); err != nil {
214+ t.Fatal(err)
215+ }
216+ if n := stampEntries(s); n != 0 {
217+ t.Fatalf("%d stamp entries after the pointer died", n)
218+ }
219+ requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
220+ if err := c.Roundtrip(); err != nil {
221+ t.Fatal(err)
222+ }
223+}
224+
225+// Destroying the timestamps object before its device drops the entry too.
226+func TestInputTimestampsDestroyBeforeDevice(t *testing.T) {
227+ s, _, _, dir := keyboardServer(t)
228+ c := protocolClient(t, s, dir)
229+ log := &stampRecorder{}
230+ seat := bindProtocol(t, c, "wl_seat")
231+ registerProtocol(t, c, seat)
232+ pointer := c.AllocateID()
233+ registerProtocol(t, c, pointer)
234+ requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
235+ manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
236+ ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
237+ if err := c.Roundtrip(); err != nil {
238+ t.Fatal(err)
239+ }
240+ if n := stampEntries(s); n != 1 {
241+ t.Fatalf("%d stamp entries, want 1", n)
242+ }
243+ requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
244+ if err := c.Roundtrip(); err != nil {
245+ t.Fatal(err)
246+ }
247+ if n := stampEntries(s); n != 0 {
248+ t.Fatalf("%d stamp entries after destroy", n)
249+ }
250+}
251+
252+// stampEntries counts the devices with subscriptions, on the display goroutine.
253+func stampEntries(s *Server) int {
254+ var n int
255+ s.display.Do(func() { n = len(s.seat.keyStamps) + len(s.seat.pointerStamps) })
256+ return n
257+}
M internal/adapters/wayland/keyboard_test.go +2 -2
@@ -232,8 +232,8 @@ func TestFootKeyboard(t *testing.T) {
232232 w := mapped(t, events, 10*time.Second)
233233 commands <- ports.FocusWindow{ID: w.ID}
234234 for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {
235- commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, TimeMsec: 1}}
236- commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, TimeMsec: 2}}
235+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, Time: 1 * time.Millisecond}}
236+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Time: 2 * time.Millisecond}}
237237 }
238238 enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)
239239 key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)
@@ -232,8 +232,8 @@ func TestFootKeyboard(t *testing.T) {
232 w := mapped(t, events, 10*time.Second)232 w := mapped(t, events, 10*time.Second)
233 commands <- ports.FocusWindow{ID: w.ID}233 commands <- ports.FocusWindow{ID: w.ID}
234 for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {234 for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {
235- commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, TimeMsec: 1}}235+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, Time: 1 * time.Millisecond}}
236- commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, TimeMsec: 2}}236+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Time: 2 * time.Millisecond}}
237 }237 }
238 enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)238 enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)
239 key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)239 key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)
M internal/adapters/wayland/move_request_test.go +1 -1
@@ -78,7 +78,7 @@ func TestToplevelMoveRequest(t *testing.T) {
7878 }
7979 w := mapped(t, events, 2*time.Second)
8080 commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1}
81- commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}
81+ commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond}
8282 var serial uint32
8383 deadline := time.After(2 * time.Second)
8484 for serial == 0 {
@@ -78,7 +78,7 @@ func TestToplevelMoveRequest(t *testing.T) {
78 }78 }
79 w := mapped(t, events, 2*time.Second)79 w := mapped(t, events, 2*time.Second)
80 commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1}80 commands <- ports.PointerFocus{ID: w.ID, X: 1, Y: 1}
81- commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}81+ commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond}
82 var serial uint3282 var serial uint32
83 deadline := time.After(2 * time.Second)83 deadline := time.After(2 * time.Second)
84 for serial == 0 {84 for serial == 0 {
M internal/adapters/wayland/protocol_test.go +6 -6
@@ -510,13 +510,13 @@ func TestPointerProtocol(t *testing.T) {
510510 }
511511 w := mapped(t, events, 2*time.Second)
512512 commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}
513- commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}
513+ commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond}
514514 // Middle, side and extra buttons pass through as evdev codes.
515515 for _, b := range []uint32{0x112, 0x113, 0x114} {
516- commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, TimeMsec: 1}
516+ commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, Time: 1 * time.Millisecond}
517517 }
518- commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, TimeMsec: 2}}
519- commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, TimeMsec: 3}}
518+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, Time: 2 * time.Millisecond}}
519+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 3 * time.Millisecond}}
520520 deadline := time.After(2 * time.Second)
521521 dispatched := make(chan error, 1)
522522 go func() {
@@ -607,10 +607,10 @@ func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) {
607607 }
608608 commands <- ports.FocusWindow{ID: a}
609609 enter(nil)
610- commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}}
610+ commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}}
611611 commands <- ports.FocusWindow{ID: b}
612612 enter([]byte{30, 0, 0, 0})
613- commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, TimeMsec: 2}}
613+ commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, Time: 2 * time.Millisecond}}
614614 commands <- ports.FocusWindow{ID: a}
615615 enter(nil)
616616 }
@@ -510,13 +510,13 @@ func TestPointerProtocol(t *testing.T) {
510 }510 }
511 w := mapped(t, events, 2*time.Second)511 w := mapped(t, events, 2*time.Second)
512 commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}512 commands <- ports.PointerFocus{ID: w.ID, X: 10, Y: 20}
513- commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, TimeMsec: 1}513+ commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 1 * time.Millisecond}
514 // Middle, side and extra buttons pass through as evdev codes.514 // Middle, side and extra buttons pass through as evdev codes.
515 for _, b := range []uint32{0x112, 0x113, 0x114} {515 for _, b := range []uint32{0x112, 0x113, 0x114} {
516- commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, TimeMsec: 1}516+ commands <- ports.PointerButtonTo{ID: w.ID, Button: b, Pressed: true, Time: 1 * time.Millisecond}
517 }517 }
518- commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, TimeMsec: 2}}518+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}, Time: 2 * time.Millisecond}}
519- commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, TimeMsec: 3}}519+ commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 3 * time.Millisecond}}
520 deadline := time.After(2 * time.Second)520 deadline := time.After(2 * time.Second)
521 dispatched := make(chan error, 1)521 dispatched := make(chan error, 1)
522 go func() {522 go func() {
@@ -607,10 +607,10 @@ func TestHeldKeyOnKeyboardFocusTransfer(t *testing.T) {
607 }607 }
608 commands <- ports.FocusWindow{ID: a}608 commands <- ports.FocusWindow{ID: a}
609 enter(nil)609 enter(nil)
610- commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}}610+ commands <- ports.ForwardKey{ID: a, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}}
611 commands <- ports.FocusWindow{ID: b}611 commands <- ports.FocusWindow{ID: b}
612 enter([]byte{30, 0, 0, 0})612 enter([]byte{30, 0, 0, 0})
613- commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, TimeMsec: 2}}613+ commands <- ports.ForwardKey{ID: b, Key: ports.KeyEvent{Keycode: 30, Time: 2 * time.Millisecond}}
614 commands <- ports.FocusWindow{ID: a}614 commands <- ports.FocusWindow{ID: a}
615 enter(nil)615 enter(nil)
616 }616 }
M internal/adapters/wayland/seat.go +43 -29
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "time"
77
8+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
89 "github.com/bnema/go-wayland-bindings/server/wayland"
910 "github.com/bnema/neferwl/internal/ports"
1011 "github.com/bnema/purego-libwayland/server"
@@ -43,6 +44,9 @@ type seatState struct {
4344 // pressAt and focusAt date the last press and keyboard focus change,
4445 // for xdg-activation tokens.
4546 pressAt, focusAt time.Time
47+ // keyStamps and pointerStamps hold the zwp_input_timestamps_v1 objects
48+ // subscribed to each device; the lists are replaced, never mutated.
49+ keyStamps, pointerStamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1
4650 }
4751
4852 // applyInput delivers a core input command to the focused client.
@@ -76,7 +80,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
7680 if l := s.lockSurfaces[c.ID]; l != nil && s.lockInputTarget(c.ID) && c.ID == s.seat.pointerFocus {
7781 s.seat.pointerX, s.seat.pointerY = c.X, c.Y
7882 for _, p := range s.clientPointers(l.resource.Client()) {
79- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
83+ s.stampPointer(p, c.Time)
84+ p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
8085 pointerFrame(p)
8186 }
8287 return
@@ -84,7 +89,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
8489 if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus {
8590 s.seat.pointerX, s.seat.pointerY = c.X, c.Y
8691 for _, p := range s.layerPointers(l) {
87- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
92+ s.stampPointer(p, c.Time)
93+ p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
8894 pointerFrame(p)
8995 }
9096 return
@@ -106,7 +112,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
106112 s.updateConstraint()
107113 }
108114 for _, p := range s.windowPointers(w) {
109- p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))
115+ s.stampPointer(p, c.Time)
116+ p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y))
110117 pointerFrame(p)
111118 }
112119 }
@@ -120,7 +127,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
120127 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
121128 }
122129 for _, p := range pointers {
123- p.SendButton(s.serial, c.TimeMsec, c.Button, state)
130+ s.stampPointer(p, c.Time)
131+ p.SendButton(s.serial, wireMsec(c.Time), c.Button, state)
124132 pointerFrame(p)
125133 }
126134 }
@@ -132,7 +140,7 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
132140 steps, values := s.seat.wheelSteps(c.Axis)
133141 _, pointers, _ := s.pointerTarget(c.ID)
134142 for _, p := range pointers {
135- sendAxis(p, c.Axis, steps, values)
143+ s.sendAxis(p, c.Axis, steps, values)
136144 }
137145 case ports.SetKeymap:
138146 s.setKeymap(c)
@@ -169,7 +177,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
169177 }
170178 }
171179 for _, k := range keyboards {
172- k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
180+ s.stampKeyboard(k, c.Key.Time)
181+ k.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state)
173182 }
174183 if c.Key.State != s.seat.modState {
175184 s.seat.modState = c.Key.State
@@ -188,14 +197,15 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
188197 return s.clientKeyboards(w.xdg.resource.Client())
189198 }
190199
200+// clientKeyboards returns c's wl_keyboards without allocating. The list holds
201+// only live resources (OnDestroy removes a keyboard when it dies) and is
202+// replaced, never mutated, so callers may iterate it while sending.
203+//
204+// This relies on purego-libwayland: destroyed() sets gone and runs OnDestroy
205+// synchronously on the display goroutine, and PostEvent never re-enters Go, so
206+// a list ranged over while sending is never changed under the caller.
191207 func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
192- var alive []*wayland.Keyboard
193- for _, k := range s.seat.keyboards[c] {
194- if k.Resource.Alive() {
195- alive = append(alive, k)
196- }
197- }
198- return alive
208+ return s.seat.keyboards[c]
199209 }
200210
201211 // focusTarget resolves a window or layer ID to its wl_surface and keyboards.
@@ -395,7 +405,7 @@ func (st *seatState) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]
395405 // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
396406 // wheel axis only with a step, carrying values (all the value since the
397407 // last step), so it never counts smooth and discrete scroll.
398-func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
408+func (s *Server) sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
399409 v := p.Version()
400410 axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
401411 for i := range axes {
@@ -409,21 +419,22 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2
409419 if v >= 5 {
410420 p.SendAxisSource(uint32(a.Source))
411421 }
412- for axis, s := range axes {
413- if !s.Set {
422+ for axis, ax := range axes {
423+ if !ax.Set {
414424 continue
415425 }
416- if s.Stop {
426+ if ax.Stop {
417427 if v >= 5 {
418- p.SendAxisStop(a.TimeMsec, uint32(axis))
428+ s.stampPointer(p, a.Time)
429+ p.SendAxisStop(wireMsec(a.Time), uint32(axis))
419430 }
420431 continue
421432 }
422- value := s.Value
423- if a.Source == ports.AxisWheel && s.V120 != 0 {
433+ value := ax.Value
434+ if a.Source == ports.AxisWheel && ax.V120 != 0 {
424435 switch {
425436 case v >= 8:
426- p.SendAxisValue120(uint32(axis), s.V120)
437+ p.SendAxisValue120(uint32(axis), ax.V120)
427438 default:
428439 value = values[axis]
429440 if v >= 5 {
@@ -431,11 +442,16 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2
431442 }
432443 }
433444 }
434- p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))
445+ s.stampPointer(p, a.Time)
446+ p.SendAxis(wireMsec(a.Time), uint32(axis), server.FixedFromFloat(value))
435447 }
436448 pointerFrame(p)
437449 }
438450
451+// wireMsec is a device timestamp as the 32-bit millisecond time of wl_pointer
452+// and wl_keyboard events (wrapping, like the protocol).
453+func wireMsec(t time.Duration) uint32 { return uint32(t / time.Millisecond) }
454+
439455 func pointerFrame(p *wayland.Pointer) {
440456 if p.Version() >= 5 {
441457 p.SendFrame()
@@ -455,14 +471,12 @@ func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
455471 return s.clientPointers(l.resource.Client())
456472 }
457473
474+// clientPointers returns c's wl_pointers without allocating. The list holds
475+// only live resources (OnDestroy removes a pointer when it dies) and is
476+// replaced, never mutated, so callers may iterate it while sending. See
477+// clientKeyboards for the libwayland invariant this relies on.
458478 func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
459- var result []*wayland.Pointer
460- for _, p := range s.seat.pointers[c] {
461- if p.Resource.Alive() {
462- result = append(result, p)
463- }
464- }
465- return result
479+ return s.seat.pointers[c]
466480 }
467481 func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
468482 if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) {
@@ -5,6 +5,7 @@ import (
5 "fmt"5 "fmt"
6 "time"6 "time"
7 7
8+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
8 "github.com/bnema/go-wayland-bindings/server/wayland"9 "github.com/bnema/go-wayland-bindings/server/wayland"
9 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
10 "github.com/bnema/purego-libwayland/server"11 "github.com/bnema/purego-libwayland/server"
@@ -43,6 +44,9 @@ type seatState struct {
43 // pressAt and focusAt date the last press and keyboard focus change,44 // pressAt and focusAt date the last press and keyboard focus change,
44 // for xdg-activation tokens.45 // for xdg-activation tokens.
45 pressAt, focusAt time.Time46 pressAt, focusAt time.Time
47+ // keyStamps and pointerStamps hold the zwp_input_timestamps_v1 objects
48+ // subscribed to each device; the lists are replaced, never mutated.
49+ keyStamps, pointerStamps map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1
46 }50 }
47 51
48 // applyInput delivers a core input command to the focused client.52 // applyInput delivers a core input command to the focused client.
@@ -76,7 +80,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
76 if l := s.lockSurfaces[c.ID]; l != nil && s.lockInputTarget(c.ID) && c.ID == s.seat.pointerFocus {80 if l := s.lockSurfaces[c.ID]; l != nil && s.lockInputTarget(c.ID) && c.ID == s.seat.pointerFocus {
77 s.seat.pointerX, s.seat.pointerY = c.X, c.Y81 s.seat.pointerX, s.seat.pointerY = c.X, c.Y
78 for _, p := range s.clientPointers(l.resource.Client()) {82 for _, p := range s.clientPointers(l.resource.Client()) {
79- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))83+ s.stampPointer(p, c.Time)
84+ p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
80 pointerFrame(p)85 pointerFrame(p)
81 }86 }
82 return87 return
@@ -84,7 +89,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
84 if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus {89 if l := s.layers[c.ID]; l != nil && c.ID == s.seat.pointerFocus {
85 s.seat.pointerX, s.seat.pointerY = c.X, c.Y90 s.seat.pointerX, s.seat.pointerY = c.X, c.Y
86 for _, p := range s.layerPointers(l) {91 for _, p := range s.layerPointers(l) {
87- p.SendMotion(c.TimeMsec, server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))92+ s.stampPointer(p, c.Time)
93+ p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(c.X), server.FixedFromFloat(c.Y))
88 pointerFrame(p)94 pointerFrame(p)
89 }95 }
90 return96 return
@@ -106,7 +112,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
106 s.updateConstraint()112 s.updateConstraint()
107 }113 }
108 for _, p := range s.windowPointers(w) {114 for _, p := range s.windowPointers(w) {
109- p.SendMotion(c.TimeMsec, server.FixedFromFloat(x), server.FixedFromFloat(y))115+ s.stampPointer(p, c.Time)
116+ p.SendMotion(wireMsec(c.Time), server.FixedFromFloat(x), server.FixedFromFloat(y))
110 pointerFrame(p)117 pointerFrame(p)
111 }118 }
112 }119 }
@@ -120,7 +127,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
120 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()127 s.seat.press, s.seat.pressClient, s.seat.pressAt = s.serial, client, time.Now()
121 }128 }
122 for _, p := range pointers {129 for _, p := range pointers {
123- p.SendButton(s.serial, c.TimeMsec, c.Button, state)130+ s.stampPointer(p, c.Time)
131+ p.SendButton(s.serial, wireMsec(c.Time), c.Button, state)
124 pointerFrame(p)132 pointerFrame(p)
125 }133 }
126 }134 }
@@ -132,7 +140,7 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
132 steps, values := s.seat.wheelSteps(c.Axis)140 steps, values := s.seat.wheelSteps(c.Axis)
133 _, pointers, _ := s.pointerTarget(c.ID)141 _, pointers, _ := s.pointerTarget(c.ID)
134 for _, p := range pointers {142 for _, p := range pointers {
135- sendAxis(p, c.Axis, steps, values)143+ s.sendAxis(p, c.Axis, steps, values)
136 }144 }
137 case ports.SetKeymap:145 case ports.SetKeymap:
138 s.setKeymap(c)146 s.setKeymap(c)
@@ -169,7 +177,8 @@ func (s *Server) applyInput(cmd ports.ClientCommand) {
169 }177 }
170 }178 }
171 for _, k := range keyboards {179 for _, k := range keyboards {
172- k.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)180+ s.stampKeyboard(k, c.Key.Time)
181+ k.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state)
173 }182 }
174 if c.Key.State != s.seat.modState {183 if c.Key.State != s.seat.modState {
175 s.seat.modState = c.Key.State184 s.seat.modState = c.Key.State
@@ -188,14 +197,15 @@ func (s *Server) windowKeyboards(w *window) []*wayland.Keyboard {
188 return s.clientKeyboards(w.xdg.resource.Client())197 return s.clientKeyboards(w.xdg.resource.Client())
189 }198 }
190 199
200+// clientKeyboards returns c's wl_keyboards without allocating. The list holds
201+// only live resources (OnDestroy removes a keyboard when it dies) and is
202+// replaced, never mutated, so callers may iterate it while sending.
203+//
204+// This relies on purego-libwayland: destroyed() sets gone and runs OnDestroy
205+// synchronously on the display goroutine, and PostEvent never re-enters Go, so
206+// a list ranged over while sending is never changed under the caller.
191 func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {207 func (s *Server) clientKeyboards(c server.Client) []*wayland.Keyboard {
192- var alive []*wayland.Keyboard208+ return s.seat.keyboards[c]
193- for _, k := range s.seat.keyboards[c] {
194- if k.Resource.Alive() {
195- alive = append(alive, k)
196- }
197- }
198- return alive
199 }209 }
200 210
201 // focusTarget resolves a window or layer ID to its wl_surface and keyboards.211 // focusTarget resolves a window or layer ID to its wl_surface and keyboards.
@@ -395,7 +405,7 @@ func (st *seatState) wheelSteps(a ports.PointerAxis) (steps [2]int32, values [2]
395 // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a405 // steps are whole wheel detents for axis_discrete. A pre-v8 client gets a
396 // wheel axis only with a step, carrying values (all the value since the406 // wheel axis only with a step, carrying values (all the value since the
397 // last step), so it never counts smooth and discrete scroll.407 // last step), so it never counts smooth and discrete scroll.
398-func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {408+func (s *Server) sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2]float64) {
399 v := p.Version()409 v := p.Version()
400 axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}410 axes := [2]ports.ScrollAxis{a.Vertical, a.Horizontal}
401 for i := range axes {411 for i := range axes {
@@ -409,21 +419,22 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2
409 if v >= 5 {419 if v >= 5 {
410 p.SendAxisSource(uint32(a.Source))420 p.SendAxisSource(uint32(a.Source))
411 }421 }
412- for axis, s := range axes {422+ for axis, ax := range axes {
413- if !s.Set {423+ if !ax.Set {
414 continue424 continue
415 }425 }
416- if s.Stop {426+ if ax.Stop {
417 if v >= 5 {427 if v >= 5 {
418- p.SendAxisStop(a.TimeMsec, uint32(axis))428+ s.stampPointer(p, a.Time)
429+ p.SendAxisStop(wireMsec(a.Time), uint32(axis))
419 }430 }
420 continue431 continue
421 }432 }
422- value := s.Value433+ value := ax.Value
423- if a.Source == ports.AxisWheel && s.V120 != 0 {434+ if a.Source == ports.AxisWheel && ax.V120 != 0 {
424 switch {435 switch {
425 case v >= 8:436 case v >= 8:
426- p.SendAxisValue120(uint32(axis), s.V120)437+ p.SendAxisValue120(uint32(axis), ax.V120)
427 default:438 default:
428 value = values[axis]439 value = values[axis]
429 if v >= 5 {440 if v >= 5 {
@@ -431,11 +442,16 @@ func sendAxis(p *wayland.Pointer, a ports.PointerAxis, steps [2]int32, values [2
431 }442 }
432 }443 }
433 }444 }
434- p.SendAxis(a.TimeMsec, uint32(axis), server.FixedFromFloat(value))445+ s.stampPointer(p, a.Time)
446+ p.SendAxis(wireMsec(a.Time), uint32(axis), server.FixedFromFloat(value))
435 }447 }
436 pointerFrame(p)448 pointerFrame(p)
437 }449 }
438 450
451+// wireMsec is a device timestamp as the 32-bit millisecond time of wl_pointer
452+// and wl_keyboard events (wrapping, like the protocol).
453+func wireMsec(t time.Duration) uint32 { return uint32(t / time.Millisecond) }
454+
439 func pointerFrame(p *wayland.Pointer) {455 func pointerFrame(p *wayland.Pointer) {
440 if p.Version() >= 5 {456 if p.Version() >= 5 {
441 p.SendFrame()457 p.SendFrame()
@@ -455,14 +471,12 @@ func (s *Server) layerPointers(l *layerSurface) []*wayland.Pointer {
455 return s.clientPointers(l.resource.Client())471 return s.clientPointers(l.resource.Client())
456 }472 }
457 473
474+// clientPointers returns c's wl_pointers without allocating. The list holds
475+// only live resources (OnDestroy removes a pointer when it dies) and is
476+// replaced, never mutated, so callers may iterate it while sending. See
477+// clientKeyboards for the libwayland invariant this relies on.
458 func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {478 func (s *Server) clientPointers(c server.Client) []*wayland.Pointer {
459- var result []*wayland.Pointer479+ return s.seat.pointers[c]
460- for _, p := range s.seat.pointers[c] {
461- if p.Resource.Alive() {
462- result = append(result, p)
463- }
464- }
465- return result
466 }480 }
467 func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {481 func (s *Server) changePointerFocus(id ports.WindowID, x, y float64) {
468 if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) {482 if s.security != nil && s.security.Snapshot().Protected && id != 0 && !s.lockInputTarget(id) {
M internal/adapters/wayland/server.go +2 -1
@@ -11,6 +11,7 @@ import (
1111 "github.com/bnema/go-wayland-bindings/server/relativepointer"
1212
1313 "github.com/bnema/go-wayland-bindings/server/fractionalscale"
14+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
1415 "github.com/bnema/go-wayland-bindings/server/wayland"
1516 "github.com/bnema/neferwl/internal/adapters/captureallow"
1617 "github.com/bnema/neferwl/internal/adapters/clock"
@@ -331,7 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
331332 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
332333 constraints: map[*surface]*constraint{},
333334 positioners: map[*server.Resource]*positioner{},
334- seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
335+ seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
335336 }
336337 s.leaseDevices = map[string]*leaseDevice{}
337338 s.pendingLeases = map[uint64]*leaseObject{}
@@ -11,6 +11,7 @@ import (
11 "github.com/bnema/go-wayland-bindings/server/relativepointer"11 "github.com/bnema/go-wayland-bindings/server/relativepointer"
12 12
13 "github.com/bnema/go-wayland-bindings/server/fractionalscale"13 "github.com/bnema/go-wayland-bindings/server/fractionalscale"
14+ "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
14 "github.com/bnema/go-wayland-bindings/server/wayland"15 "github.com/bnema/go-wayland-bindings/server/wayland"
15 "github.com/bnema/neferwl/internal/adapters/captureallow"16 "github.com/bnema/neferwl/internal/adapters/captureallow"
16 "github.com/bnema/neferwl/internal/adapters/clock"17 "github.com/bnema/neferwl/internal/adapters/clock"
@@ -331,7 +332,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
331 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},332 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
332 constraints: map[*surface]*constraint{},333 constraints: map[*surface]*constraint{},
333 positioners: map[*server.Resource]*positioner{},334 positioners: map[*server.Resource]*positioner{},
334- seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},335+ seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
335 }336 }
336 s.leaseDevices = map[string]*leaseDevice{}337 s.leaseDevices = map[string]*leaseDevice{}
337 s.pendingLeases = map[uint64]*leaseObject{}338 s.pendingLeases = map[uint64]*leaseObject{}
M internal/adapters/wayland/session_lock_input_epoch_test.go +1 -1
@@ -158,7 +158,7 @@ func TestSessionLockInputEpochHeldPasswordClearedBeforeDesktopEnter(t *testing.T
158158 if *locked != 1 {
159159 t.Fatal("lock not confirmed")
160160 }
161- password := ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}}
161+ password := ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}}
162162 s.display.Do(func() {
163163 s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: id}})
164164 s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: id, Key: password}})
@@ -158,7 +158,7 @@ func TestSessionLockInputEpochHeldPasswordClearedBeforeDesktopEnter(t *testing.T
158 if *locked != 1 {158 if *locked != 1 {
159 t.Fatal("lock not confirmed")159 t.Fatal("lock not confirmed")
160 }160 }
161- password := ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}}161+ password := ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond, State: ports.ModState{Depressed: 65, Latched: 2, Locked: 16, Group: 1}}
162 s.display.Do(func() {162 s.display.Do(func() {
163 s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: id}})163 s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: id}})
164 s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: id, Key: password}})164 s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: id, Key: password}})
M internal/adapters/wayland/session_lock_lifecycle_test.go +1 -1
@@ -335,7 +335,7 @@ func TestSessionLockLifecycleIMEGrabCannotReceiveLockerKey(t *testing.T) {
335335 s.display.Do(func() {
336336 l := s.surfaceOf(findTestSurface(s, surf.ID())).lock
337337 s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: l.id}})
338- s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}}})
338+ s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}}})
339339 })
340340 settle(t, locker, ime.c)
341341 for _, event := range grab.take() {
@@ -335,7 +335,7 @@ func TestSessionLockLifecycleIMEGrabCannotReceiveLockerKey(t *testing.T) {
335 s.display.Do(func() {335 s.display.Do(func() {
336 l := s.surfaceOf(findTestSurface(s, surf.ID())).lock336 l := s.surfaceOf(findTestSurface(s, surf.ID())).lock
337 s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: l.id}})337 s.apply(ports.SecurityCommand{State: state, Command: ports.FocusWindow{ID: l.id}})
338- s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 1}}})338+ s.apply(ports.SecurityCommand{State: state, Command: ports.ForwardKey{ID: l.id, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 1 * time.Millisecond}}})
339 })339 })
340 settle(t, locker, ime.c)340 settle(t, locker, ime.c)
341 for _, event := range grab.take() {341 for _, event := range grab.take() {
M internal/adapters/wayland/textinput.go +1 -1
@@ -516,7 +516,7 @@ func (s *Server) grabKey(c ports.ForwardKey) bool {
516516 s.seat.grabKeys[code] = true
517517 }
518518 s.serial++
519- g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)
519+ g.res.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state)
520520 if c.Key.State != s.seat.modState {
521521 s.seat.modState = c.Key.State
522522 m := s.seat.modState
@@ -516,7 +516,7 @@ func (s *Server) grabKey(c ports.ForwardKey) bool {
516 s.seat.grabKeys[code] = true516 s.seat.grabKeys[code] = true
517 }517 }
518 s.serial++518 s.serial++
519- g.res.SendKey(s.serial, c.Key.TimeMsec, c.Key.Keycode, state)519+ g.res.SendKey(s.serial, wireMsec(c.Key.Time), c.Key.Keycode, state)
520 if c.Key.State != s.seat.modState {520 if c.Key.State != s.seat.modState {
521 s.seat.modState = c.Key.State521 s.seat.modState = c.Key.State
522 m := s.seat.modState522 m := s.seat.modState
M internal/adapters/wayland/textinput_test.go +1 -1
@@ -309,7 +309,7 @@ func TestInputMethodKeyboardGrab(t *testing.T) {
309309 ime := newIMEApp(t, s, dir)
310310 app.keys.opcodes = nil
311311 key := func(code uint32, pressed bool) {
312- commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, TimeMsec: 7}}
312+ commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, Time: 7 * time.Millisecond}}
313313 }
314314 k := uint16(wayland.KeyboardEventKey)
315315 key(31, true) // held by the client before the grab
@@ -309,7 +309,7 @@ func TestInputMethodKeyboardGrab(t *testing.T) {
309 ime := newIMEApp(t, s, dir)309 ime := newIMEApp(t, s, dir)
310 app.keys.opcodes = nil310 app.keys.opcodes = nil
311 key := func(code uint32, pressed bool) {311 key := func(code uint32, pressed bool) {
312- commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, TimeMsec: 7}}312+ commands <- ports.ForwardKey{ID: app.window, Key: ports.KeyEvent{Keycode: code, Pressed: pressed, Time: 7 * time.Millisecond}}
313 }313 }
314 k := uint16(wayland.KeyboardEventKey)314 k := uint16(wayland.KeyboardEventKey)
315 key(31, true) // held by the client before the grab315 key(31, true) // held by the client before the grab
M internal/adapters/wayland/virtualkeyboard.go +9 -5
@@ -136,7 +136,7 @@ func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*
136136 return keyboards
137137 }
138138
139-func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) {
139+func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, msec, key, state uint32) {
140140 if !k.admitted() || state != 1 && k.dropReleases && !k.pressed[key] {
141141 return
142142 }
@@ -149,8 +149,11 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key
149149 } else {
150150 delete(k.pressed, key)
151151 }
152+ // The client's millisecond time is all the timestamp there is.
153+ stamp := time.Duration(msec) * time.Millisecond
152154 for _, kb := range keyboards {
153- kb.SendKey(k.server.serial, time, key, state)
155+ k.server.stampKeyboard(kb, stamp)
156+ kb.SendKey(k.server.serial, msec, key, state)
154157 }
155158 }
156159
@@ -198,12 +201,13 @@ func (k *virtualKeyboard) release() {
198201 _, keyboards := s.focusTarget(s.seat.focused)
199202 var ts unix.Timespec
200203 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
201- // Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock).
202- now := uint32(ts.Nano() / int64(time.Millisecond))
204+ // Input events carry CLOCK_MONOTONIC (libinput's clock).
205+ now := time.Duration(ts.Nano())
203206 for key := range k.pressed {
204207 s.serial++
205208 for _, kb := range keyboards {
206- kb.SendKey(s.serial, now, key, 0)
209+ s.stampKeyboard(kb, now)
210+ kb.SendKey(s.serial, wireMsec(now), key, 0)
207211 }
208212 }
209213 }
@@ -136,7 +136,7 @@ func (k *virtualKeyboard) keyboards(r *virtualkeyboard.ZwpVirtualKeyboardV1) []*
136 return keyboards136 return keyboards
137 }137 }
138 138
139-func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key, state uint32) {139+func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, msec, key, state uint32) {
140 if !k.admitted() || state != 1 && k.dropReleases && !k.pressed[key] {140 if !k.admitted() || state != 1 && k.dropReleases && !k.pressed[key] {
141 return141 return
142 }142 }
@@ -149,8 +149,11 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key
149 } else {149 } else {
150 delete(k.pressed, key)150 delete(k.pressed, key)
151 }151 }
152+ // The client's millisecond time is all the timestamp there is.
153+ stamp := time.Duration(msec) * time.Millisecond
152 for _, kb := range keyboards {154 for _, kb := range keyboards {
153- kb.SendKey(k.server.serial, time, key, state)155+ k.server.stampKeyboard(kb, stamp)
156+ kb.SendKey(k.server.serial, msec, key, state)
154 }157 }
155 }158 }
156 159
@@ -198,12 +201,13 @@ func (k *virtualKeyboard) release() {
198 _, keyboards := s.focusTarget(s.seat.focused)201 _, keyboards := s.focusTarget(s.seat.focused)
199 var ts unix.Timespec202 var ts unix.Timespec
200 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)203 _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
201- // Input events carry CLOCK_MONOTONIC milliseconds (libinput's clock).204+ // Input events carry CLOCK_MONOTONIC (libinput's clock).
202- now := uint32(ts.Nano() / int64(time.Millisecond))205+ now := time.Duration(ts.Nano())
203 for key := range k.pressed {206 for key := range k.pressed {
204 s.serial++207 s.serial++
205 for _, kb := range keyboards {208 for _, kb := range keyboards {
206- kb.SendKey(s.serial, now, key, 0)209+ s.stampKeyboard(kb, now)
210+ kb.SendKey(s.serial, wireMsec(now), key, 0)
207 }211 }
208 }212 }
209 }213 }
M internal/adapters/wayland/virtualkeyboard_test.go +1 -1
@@ -78,7 +78,7 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) {
7878 }
7979
8080 proxy.opcodes, proxy.keymaps = nil, nil
81- commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}}
81+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}}
8282 deadline := time.Now().Add(2 * time.Second)
8383 for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) {
8484 if err := target.Roundtrip(); err != nil {
@@ -78,7 +78,7 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) {
78 }78 }
79 79
80 proxy.opcodes, proxy.keymaps = nil, nil80 proxy.opcodes, proxy.keymaps = nil, nil
81- commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}}81+ commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 3 * time.Millisecond}}
82 deadline := time.Now().Add(2 * time.Second)82 deadline := time.Now().Add(2 * time.Second)
83 for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) {83 for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) {
84 if err := target.Roundtrip(); err != nil {84 if err := target.Roundtrip(); err != nil {
M internal/adapters/wayland/wire_client_test.go +3 -0
@@ -27,6 +27,7 @@ import (
2727 wire_fractionalscale "github.com/bnema/go-wayland-bindings/server/fractionalscale"
2828 wire_idleinhibit "github.com/bnema/go-wayland-bindings/server/idleinhibit"
2929 wire_inputmethod "github.com/bnema/go-wayland-bindings/server/inputmethod"
30+ wire_inputtimestamps "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
3031 wire_keyboardshortcutsinhibit "github.com/bnema/go-wayland-bindings/server/keyboardshortcutsinhibit"
3132 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
3233 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
@@ -225,6 +226,8 @@ var wireSchemas = map[string]wireSchema{
225226 "zwp_input_method_manager_v2": {wire_inputmethod.ZwpInputMethodManagerV2Interface, &client_inputmethod.InputMethodManager{}, map[uint16]bool{1: true}, nil},
226227 "zwp_input_method_v2": {wire_inputmethod.ZwpInputMethodV2Interface, &client_inputmethod.InputMethod{}, map[uint16]bool{6: true}, nil},
227228 "zwp_input_popup_surface_v2": {wire_inputmethod.ZwpInputPopupSurfaceV2Interface, &client_inputmethod.InputPopupSurface{}, map[uint16]bool{0: true}, nil},
229+ "zwp_input_timestamps_manager_v1": {wire_inputtimestamps.ZwpInputTimestampsManagerV1Interface, nil, map[uint16]bool{0: true}, nil},
230+ "zwp_input_timestamps_v1": {wire_inputtimestamps.ZwpInputTimestampsV1Interface, nil, map[uint16]bool{0: true}, nil},
228231 "zwp_keyboard_shortcuts_inhibit_manager_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitManager{}, map[uint16]bool{0: true}, nil},
229232 "zwp_keyboard_shortcuts_inhibitor_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitor{}, map[uint16]bool{0: true}, nil},
230233 "zwp_linux_buffer_params_v1": {wire_linuxdmabuf.ZwpLinuxBufferParamsV1Interface, &client_linuxdmabuf.LinuxBufferParams{}, map[uint16]bool{0: true}, nil},
@@ -27,6 +27,7 @@ import (
27 wire_fractionalscale "github.com/bnema/go-wayland-bindings/server/fractionalscale"27 wire_fractionalscale "github.com/bnema/go-wayland-bindings/server/fractionalscale"
28 wire_idleinhibit "github.com/bnema/go-wayland-bindings/server/idleinhibit"28 wire_idleinhibit "github.com/bnema/go-wayland-bindings/server/idleinhibit"
29 wire_inputmethod "github.com/bnema/go-wayland-bindings/server/inputmethod"29 wire_inputmethod "github.com/bnema/go-wayland-bindings/server/inputmethod"
30+ wire_inputtimestamps "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
30 wire_keyboardshortcutsinhibit "github.com/bnema/go-wayland-bindings/server/keyboardshortcutsinhibit"31 wire_keyboardshortcutsinhibit "github.com/bnema/go-wayland-bindings/server/keyboardshortcutsinhibit"
31 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"32 wire_linuxdmabuf "github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
32 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"33 wire_linuxdrmsyncobj "github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
@@ -225,6 +226,8 @@ var wireSchemas = map[string]wireSchema{
225 "zwp_input_method_manager_v2": {wire_inputmethod.ZwpInputMethodManagerV2Interface, &client_inputmethod.InputMethodManager{}, map[uint16]bool{1: true}, nil},226 "zwp_input_method_manager_v2": {wire_inputmethod.ZwpInputMethodManagerV2Interface, &client_inputmethod.InputMethodManager{}, map[uint16]bool{1: true}, nil},
226 "zwp_input_method_v2": {wire_inputmethod.ZwpInputMethodV2Interface, &client_inputmethod.InputMethod{}, map[uint16]bool{6: true}, nil},227 "zwp_input_method_v2": {wire_inputmethod.ZwpInputMethodV2Interface, &client_inputmethod.InputMethod{}, map[uint16]bool{6: true}, nil},
227 "zwp_input_popup_surface_v2": {wire_inputmethod.ZwpInputPopupSurfaceV2Interface, &client_inputmethod.InputPopupSurface{}, map[uint16]bool{0: true}, nil},228 "zwp_input_popup_surface_v2": {wire_inputmethod.ZwpInputPopupSurfaceV2Interface, &client_inputmethod.InputPopupSurface{}, map[uint16]bool{0: true}, nil},
229+ "zwp_input_timestamps_manager_v1": {wire_inputtimestamps.ZwpInputTimestampsManagerV1Interface, nil, map[uint16]bool{0: true}, nil},
230+ "zwp_input_timestamps_v1": {wire_inputtimestamps.ZwpInputTimestampsV1Interface, nil, map[uint16]bool{0: true}, nil},
228 "zwp_keyboard_shortcuts_inhibit_manager_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitManager{}, map[uint16]bool{0: true}, nil},231 "zwp_keyboard_shortcuts_inhibit_manager_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitManagerV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitManager{}, map[uint16]bool{0: true}, nil},
229 "zwp_keyboard_shortcuts_inhibitor_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitor{}, map[uint16]bool{0: true}, nil},232 "zwp_keyboard_shortcuts_inhibitor_v1": {wire_keyboardshortcutsinhibit.ZwpKeyboardShortcutsInhibitorV1Interface, &client_shortcutsinhibit.KeyboardShortcutsInhibitor{}, map[uint16]bool{0: true}, nil},
230 "zwp_linux_buffer_params_v1": {wire_linuxdmabuf.ZwpLinuxBufferParamsV1Interface, &client_linuxdmabuf.LinuxBufferParams{}, map[uint16]bool{0: true}, nil},233 "zwp_linux_buffer_params_v1": {wire_linuxdmabuf.ZwpLinuxBufferParamsV1Interface, &client_linuxdmabuf.LinuxBufferParams{}, map[uint16]bool{0: true}, nil},
M internal/adapters/xkb/xkb.go +5 -4
@@ -4,6 +4,7 @@ package xkb
44 import (
55 "fmt"
66 "runtime"
7+ "time"
78 "unsafe"
89
910 "github.com/bnema/neferwl/internal/ports"
@@ -130,7 +131,7 @@ func (k *Keymap) String() string {
130131 return string(unsafe.Slice((*byte)(ptr), n))
131132 }
132133
133-func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyEvent {
134+func (k *Keymap) Key(evdevCode uint32, pressed bool, t time.Duration) ports.KeyEvent {
134135 code := evdevCode + 8
135136 sym := k.getSym(k.state, code)
136137 var buf [64]byte
@@ -144,7 +145,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE
144145 direction = 1
145146 }
146147 k.updateKey(k.state, code, direction)
147- event := ports.KeyEvent{Keysym: name, Pressed: pressed, TimeMsec: timeMsec, Keycode: evdevCode}
148+ event := ports.KeyEvent{Keysym: name, Pressed: pressed, Time: t, Keycode: evdevCode}
148149 if pressed {
149150 layout := k.serializeLayout(k.state, layoutEffective)
150151 event.Base = k.levelName(code, layout, 0)
@@ -170,7 +171,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE
170171 // new security epoch. Keys physically held across a transition are suppressed
171172 // (including repeats and their eventual release), never replayed into XKB.
172173 // On reset failure no event is translated against the old native state.
173-func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch ports.SecurityState) (ports.KeyEvent, bool, error) {
174+func (k *Keymap) KeySecure(code uint32, pressed bool, t time.Duration, epoch ports.SecurityState) (ports.KeyEvent, bool, error) {
174175 if !k.epochSet || k.epoch != epoch {
175176 next := k.stateNew(k.mapPtr)
176177 if next == 0 {
@@ -208,7 +209,7 @@ func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch por
208209 if blocked {
209210 return ports.KeyEvent{}, false, nil
210211 }
211- return k.Key(code, pressed, timeMsec), true, nil
212+ return k.Key(code, pressed, t), true, nil
212213 }
213214
214215 // QuarantineFrom transfers physical held-key tracking when a producer replaces
@@ -4,6 +4,7 @@ package xkb
4 import (4 import (
5 "fmt"5 "fmt"
6 "runtime"6 "runtime"
7+ "time"
7 "unsafe"8 "unsafe"
8 9
9 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
@@ -130,7 +131,7 @@ func (k *Keymap) String() string {
130 return string(unsafe.Slice((*byte)(ptr), n))131 return string(unsafe.Slice((*byte)(ptr), n))
131 }132 }
132 133
133-func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyEvent {134+func (k *Keymap) Key(evdevCode uint32, pressed bool, t time.Duration) ports.KeyEvent {
134 code := evdevCode + 8135 code := evdevCode + 8
135 sym := k.getSym(k.state, code)136 sym := k.getSym(k.state, code)
136 var buf [64]byte137 var buf [64]byte
@@ -144,7 +145,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE
144 direction = 1145 direction = 1
145 }146 }
146 k.updateKey(k.state, code, direction)147 k.updateKey(k.state, code, direction)
147- event := ports.KeyEvent{Keysym: name, Pressed: pressed, TimeMsec: timeMsec, Keycode: evdevCode}148+ event := ports.KeyEvent{Keysym: name, Pressed: pressed, Time: t, Keycode: evdevCode}
148 if pressed {149 if pressed {
149 layout := k.serializeLayout(k.state, layoutEffective)150 layout := k.serializeLayout(k.state, layoutEffective)
150 event.Base = k.levelName(code, layout, 0)151 event.Base = k.levelName(code, layout, 0)
@@ -170,7 +171,7 @@ func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyE
170 // new security epoch. Keys physically held across a transition are suppressed171 // new security epoch. Keys physically held across a transition are suppressed
171 // (including repeats and their eventual release), never replayed into XKB.172 // (including repeats and their eventual release), never replayed into XKB.
172 // On reset failure no event is translated against the old native state.173 // On reset failure no event is translated against the old native state.
173-func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch ports.SecurityState) (ports.KeyEvent, bool, error) {174+func (k *Keymap) KeySecure(code uint32, pressed bool, t time.Duration, epoch ports.SecurityState) (ports.KeyEvent, bool, error) {
174 if !k.epochSet || k.epoch != epoch {175 if !k.epochSet || k.epoch != epoch {
175 next := k.stateNew(k.mapPtr)176 next := k.stateNew(k.mapPtr)
176 if next == 0 {177 if next == 0 {
@@ -208,7 +209,7 @@ func (k *Keymap) KeySecure(code uint32, pressed bool, timeMsec uint32, epoch por
208 if blocked {209 if blocked {
209 return ports.KeyEvent{}, false, nil210 return ports.KeyEvent{}, false, nil
210 }211 }
211- return k.Key(code, pressed, timeMsec), true, nil212+ return k.Key(code, pressed, t), true, nil
212 }213 }
213 214
214 // QuarantineFrom transfers physical held-key tracking when a producer replaces215 // QuarantineFrom transfers physical held-key tracking when a producer replaces
M internal/adapters/xkb/xkb_test.go +2 -1
@@ -3,6 +3,7 @@ package xkb
33 import (
44 "strings"
55 "testing"
6+ "time"
67
78 "github.com/bnema/neferwl/internal/ports"
89 )
@@ -25,7 +26,7 @@ func TestKeymap(t *testing.T) {
2526 if !strings.Contains(k.String(), "xkb_keymap") {
2627 t.Fatal("missing keymap text")
2728 }
28- if ev := k.Key(30, true, 123); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.TimeMsec != 123 {
29+ if ev := k.Key(30, true, 123*time.Millisecond); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.Time != 123*time.Millisecond {
2930 t.Fatalf("A: %+v", ev)
3031 }
3132 k.Key(30, false, 0)
@@ -3,6 +3,7 @@ package xkb
3 import (3 import (
4 "strings"4 "strings"
5 "testing"5 "testing"
6+ "time"
6 7
7 "github.com/bnema/neferwl/internal/ports"8 "github.com/bnema/neferwl/internal/ports"
8 )9 )
@@ -25,7 +26,7 @@ func TestKeymap(t *testing.T) {
25 if !strings.Contains(k.String(), "xkb_keymap") {26 if !strings.Contains(k.String(), "xkb_keymap") {
26 t.Fatal("missing keymap text")27 t.Fatal("missing keymap text")
27 }28 }
28- if ev := k.Key(30, true, 123); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.TimeMsec != 123 {29+ if ev := k.Key(30, true, 123*time.Millisecond); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.Time != 123*time.Millisecond {
29 t.Fatalf("A: %+v", ev)30 t.Fatalf("A: %+v", ev)
30 }31 }
31 k.Key(30, false, 0)32 k.Key(30, false, 0)
M internal/core/core.go +3 -3
@@ -130,9 +130,9 @@ type Core struct {
130130 swipe *swipeGesture
131131 frameC <-chan time.Time
132132 frameStop func() bool
133- // motionMsec is the time of the last pointer motion; pointerAt is the
133+ // motionTime is the time of the last pointer motion; pointerAt is the
134134 // last position sent in the pointer's window.
135- motionMsec uint32
135+ motionTime time.Duration
136136 pointerAt [2]float64
137137 // capt is the capture state (capture.go, capindicator.go).
138138 capt captureState
@@ -703,7 +703,7 @@ func (c *Core) rehit(ctx context.Context) error {
703703 // The same window moved under the cursor: it moves in the window. No
704704 // relative motion: the device did not move. The input clock is the
705705 // device's; the last motion's time is the closest core knows.
706- return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: c.motionMsec})
706+ return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, Time: c.motionTime})
707707 }
708708
709709 // updateInhibit makes the shortcuts inhibitor of the keyboard focus the
@@ -130,9 +130,9 @@ type Core struct {
130 swipe *swipeGesture130 swipe *swipeGesture
131 frameC <-chan time.Time131 frameC <-chan time.Time
132 frameStop func() bool132 frameStop func() bool
133- // motionMsec is the time of the last pointer motion; pointerAt is the133+ // motionTime is the time of the last pointer motion; pointerAt is the
134 // last position sent in the pointer's window.134 // last position sent in the pointer's window.
135- motionMsec uint32135+ motionTime time.Duration
136 pointerAt [2]float64136 pointerAt [2]float64
137 // capt is the capture state (capture.go, capindicator.go).137 // capt is the capture state (capture.go, capindicator.go).
138 capt captureState138 capt captureState
@@ -703,7 +703,7 @@ func (c *Core) rehit(ctx context.Context) error {
703 // The same window moved under the cursor: it moves in the window. No703 // The same window moved under the cursor: it moves in the window. No
704 // relative motion: the device did not move. The input clock is the704 // relative motion: the device did not move. The input clock is the
705 // device's; the last motion's time is the closest core knows.705 // device's; the last motion's time is the closest core knows.
706- return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, TimeMsec: c.motionMsec})706+ return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, Time: c.motionTime})
707 }707 }
708 708
709 // updateInhibit makes the shortcuts inhibitor of the keyboard focus the709 // updateInhibit makes the shortcuts inhibitor of the keyboard focus the
M internal/core/core_test.go +3 -3
@@ -245,11 +245,11 @@ func TestPointerFocusAndGrab(t *testing.T) {
245245 for len(commands) > 0 {
246246 <-commands
247247 }
248- input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), TimeMsec: 1}
248+ input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), Time: 1 * time.Millisecond}
249249 if v := command(t, commands); v != (ports.PointerFocus{ID: 1, X: 2, Y: 3}) {
250250 t.Fatal(v)
251251 }
252- if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) {
252+ if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, Time: 1 * time.Millisecond}) {
253253 t.Fatal(v)
254254 }
255255 input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}
@@ -324,7 +324,7 @@ func TestBorderInset(t *testing.T) {
324324 if !found {
325325 t.Fatalf("no %v for outer %v", want, r)
326326 }
327- input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), TimeMsec: 1}
327+ input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), Time: 1 * time.Millisecond}
328328 if v := command(t, commands); v != (ports.PointerFocus{ID: 2, X: 2, Y: 5}) {
329329 t.Fatal(v)
330330 }
@@ -245,11 +245,11 @@ func TestPointerFocusAndGrab(t *testing.T) {
245 for len(commands) > 0 {245 for len(commands) > 0 {
246 <-commands246 <-commands
247 }247 }
248- input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), TimeMsec: 1}248+ input <- ports.PointerMotion{X: float64(first.X + 2), Y: float64(first.Y + 3), Time: 1 * time.Millisecond}
249 if v := command(t, commands); v != (ports.PointerFocus{ID: 1, X: 2, Y: 3}) {249 if v := command(t, commands); v != (ports.PointerFocus{ID: 1, X: 2, Y: 3}) {
250 t.Fatal(v)250 t.Fatal(v)
251 }251 }
252- if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, TimeMsec: 1}) {252+ if v := command(t, commands); v != (ports.PointerMotionTo{ID: 1, X: 2, Y: 3, Time: 1 * time.Millisecond}) {
253 t.Fatal(v)253 t.Fatal(v)
254 }254 }
255 input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}255 input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 15, V120: 120}}
@@ -324,7 +324,7 @@ func TestBorderInset(t *testing.T) {
324 if !found {324 if !found {
325 t.Fatalf("no %v for outer %v", want, r)325 t.Fatalf("no %v for outer %v", want, r)
326 }326 }
327- input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), TimeMsec: 1}327+ input <- ports.PointerMotion{X: float64(r.X + 2), Y: float64(r.Y + 5), Time: 1 * time.Millisecond}
328 if v := command(t, commands); v != (ports.PointerFocus{ID: 2, X: 2, Y: 5}) {328 if v := command(t, commands); v != (ports.PointerFocus{ID: 2, X: 2, Y: 5}) {
329 t.Fatal(v)329 t.Fatal(v)
330 }330 }
M internal/core/drag.go +10 -9
@@ -3,6 +3,7 @@ package core
33 import (
44 "context"
55 "slices"
6+ "time"
67
78 "github.com/bnema/neferwl/internal/ports"
89 )
@@ -30,9 +31,9 @@ const (
3031 gapPad = 12
3132 // edgeScroll is the distance from the usable edge, logical px, that
3233 // scrolls a scroll-overflow workspace during a tile drag, at most
33- // once per edgeScrollMsec.
34- edgeScroll = 32
35- edgeScrollMsec = 250
34+ // once per edgeScrollInterval.
35+ edgeScroll = 32
36+ edgeScrollInterval = 250 * time.Millisecond
3637 )
3738
3839 type dropKind int
@@ -69,7 +70,7 @@ type dragState struct {
6970 float bool
7071 target dropTarget
7172 // scrolled is the time of the last edge scroll.
72- scrolled uint32
73+ scrolled time.Duration
7374 }
7475
7576 // draggable returns the screen and workspace of a window a drag may
@@ -173,7 +174,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize
173174
174175 // dragMotion follows the cursor: a float moves or resizes, a tile shows
175176 // where it would land.
176-func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error {
177+func (c *Core) dragMotion(ctx context.Context, at time.Duration) error {
177178 d := c.drag
178179 _, w, ok := c.draggable(d.id)
179180 if !ok {
@@ -198,7 +199,7 @@ func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error {
198199 }
199200 return c.publish(ctx)
200201 }
201- scrolled := c.edgeScroll(timeMsec)
202+ scrolled := c.edgeScroll(at)
202203 t := c.dropAt(d.id, c.cursorX, c.cursorY)
203204 if !scrolled && t.kind == d.target.kind && t.anchor == d.target.anchor && t.ws == d.target.ws && slices.Equal(t.hints, d.target.hints) {
204205 return nil
@@ -513,9 +514,9 @@ func (c *Core) drop(id WindowID, t dropTarget) {
513514 // edgeScroll scrolls a scroll-overflow workspace by one column when a
514515 // tile drag moves the pointer against its usable edge. It reports whether
515516 // the view moved.
516-func (c *Core) edgeScroll(timeMsec uint32) bool {
517+func (c *Core) edgeScroll(t time.Duration) bool {
517518 i := c.screenAt(c.cursorX, c.cursorY)
518- if i < 0 || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 {
519+ if i < 0 || t-c.drag.scrolled < edgeScrollInterval && c.drag.scrolled != 0 {
519520 return false
520521 }
521522 sc := c.screens[i]
@@ -536,7 +537,7 @@ func (c *Core) edgeScroll(timeMsec uint32) bool {
536537 if !w.scrollBy(dir) {
537538 return false
538539 }
539- c.drag.scrolled = max(timeMsec, 1)
540+ c.drag.scrolled = max(t, 1)
540541 return true
541542 }
542543
@@ -3,6 +3,7 @@ package core
3 import (3 import (
4 "context"4 "context"
5 "slices"5 "slices"
6+ "time"
6 7
7 "github.com/bnema/neferwl/internal/ports"8 "github.com/bnema/neferwl/internal/ports"
8 )9 )
@@ -30,9 +31,9 @@ const (
30 gapPad = 1231 gapPad = 12
31 // edgeScroll is the distance from the usable edge, logical px, that32 // edgeScroll is the distance from the usable edge, logical px, that
32 // scrolls a scroll-overflow workspace during a tile drag, at most33 // scrolls a scroll-overflow workspace during a tile drag, at most
33- // once per edgeScrollMsec.34+ // once per edgeScrollInterval.
34- edgeScroll = 3235+ edgeScroll = 32
35- edgeScrollMsec = 25036+ edgeScrollInterval = 250 * time.Millisecond
36 )37 )
37 38
38 type dropKind int39 type dropKind int
@@ -69,7 +70,7 @@ type dragState struct {
69 float bool70 float bool
70 target dropTarget71 target dropTarget
71 // scrolled is the time of the last edge scroll.72 // scrolled is the time of the last edge scroll.
72- scrolled uint3273+ scrolled time.Duration
73 }74 }
74 75
75 // draggable returns the screen and workspace of a window a drag may76 // draggable returns the screen and workspace of a window a drag may
@@ -173,7 +174,7 @@ func (c *Core) beginDrag(ctx context.Context, id WindowID, button uint32, resize
173 174
174 // dragMotion follows the cursor: a float moves or resizes, a tile shows175 // dragMotion follows the cursor: a float moves or resizes, a tile shows
175 // where it would land.176 // where it would land.
176-func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error {177+func (c *Core) dragMotion(ctx context.Context, at time.Duration) error {
177 d := c.drag178 d := c.drag
178 _, w, ok := c.draggable(d.id)179 _, w, ok := c.draggable(d.id)
179 if !ok {180 if !ok {
@@ -198,7 +199,7 @@ func (c *Core) dragMotion(ctx context.Context, timeMsec uint32) error {
198 }199 }
199 return c.publish(ctx)200 return c.publish(ctx)
200 }201 }
201- scrolled := c.edgeScroll(timeMsec)202+ scrolled := c.edgeScroll(at)
202 t := c.dropAt(d.id, c.cursorX, c.cursorY)203 t := c.dropAt(d.id, c.cursorX, c.cursorY)
203 if !scrolled && t.kind == d.target.kind && t.anchor == d.target.anchor && t.ws == d.target.ws && slices.Equal(t.hints, d.target.hints) {204 if !scrolled && t.kind == d.target.kind && t.anchor == d.target.anchor && t.ws == d.target.ws && slices.Equal(t.hints, d.target.hints) {
204 return nil205 return nil
@@ -513,9 +514,9 @@ func (c *Core) drop(id WindowID, t dropTarget) {
513 // edgeScroll scrolls a scroll-overflow workspace by one column when a514 // edgeScroll scrolls a scroll-overflow workspace by one column when a
514 // tile drag moves the pointer against its usable edge. It reports whether515 // tile drag moves the pointer against its usable edge. It reports whether
515 // the view moved.516 // the view moved.
516-func (c *Core) edgeScroll(timeMsec uint32) bool {517+func (c *Core) edgeScroll(t time.Duration) bool {
517 i := c.screenAt(c.cursorX, c.cursorY)518 i := c.screenAt(c.cursorX, c.cursorY)
518- if i < 0 || timeMsec-c.drag.scrolled < edgeScrollMsec && c.drag.scrolled != 0 {519+ if i < 0 || t-c.drag.scrolled < edgeScrollInterval && c.drag.scrolled != 0 {
519 return false520 return false
520 }521 }
521 sc := c.screens[i]522 sc := c.screens[i]
@@ -536,7 +537,7 @@ func (c *Core) edgeScroll(timeMsec uint32) bool {
536 if !w.scrollBy(dir) {537 if !w.scrollBy(dir) {
537 return false538 return false
538 }539 }
539- c.drag.scrolled = max(timeMsec, 1)540+ c.drag.scrolled = max(t, 1)
540 return true541 return true
541 }542 }
542 543
M internal/core/drag_test.go +1 -1
@@ -168,7 +168,7 @@ func columnsOf(set []ports.Scene, output string) [][]ports.WindowID {
168168 var dragOut = ports.OutputInfo{Name: "A", Width: 1000, Height: 600}
169169
170170 func (r *dragRig) moveTo(x, y float64, msec uint32) {
171- r.in(ports.PointerMotion{X: x, Y: y, TimeMsec: msec})
171+ r.in(ports.PointerMotion{X: x, Y: y, Time: time.Duration(msec) * time.Millisecond})
172172 }
173173
174174 func (r *dragRig) press(t *testing.T, button uint32, mods ports.Mods) {
@@ -168,7 +168,7 @@ func columnsOf(set []ports.Scene, output string) [][]ports.WindowID {
168 var dragOut = ports.OutputInfo{Name: "A", Width: 1000, Height: 600}168 var dragOut = ports.OutputInfo{Name: "A", Width: 1000, Height: 600}
169 169
170 func (r *dragRig) moveTo(x, y float64, msec uint32) {170 func (r *dragRig) moveTo(x, y float64, msec uint32) {
171- r.in(ports.PointerMotion{X: x, Y: y, TimeMsec: msec})171+ r.in(ports.PointerMotion{X: x, Y: y, Time: time.Duration(msec) * time.Millisecond})
172 }172 }
173 173
174 func (r *dragRig) press(t *testing.T, button uint32, mods ports.Mods) {174 func (r *dragRig) press(t *testing.T, button uint32, mods ports.Mods) {
M internal/core/input.go +4 -4
@@ -18,7 +18,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
1818 }
1919 switch v := ev.(type) {
2020 case ports.PointerMotion:
21- c.motionMsec = v.TimeMsec
21+ c.motionTime = v.Time
2222 // A locked pointer stays still; relative motion still flows.
2323 if c.constraint.Mode != ports.ConstraintLock {
2424 c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
@@ -40,7 +40,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
4040 }
4141 if c.drag != nil {
4242 // The dragged window and the others get no pointer events.
43- return c.dragMotion(ctx, v.TimeMsec)
43+ return c.dragMotion(ctx, v.Time)
4444 }
4545 id, x, y := c.hit(c.cursorX, c.cursorY)
4646 // Layout changes are intentionally re-hit-tested only on motion.
@@ -52,7 +52,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
5252 }
5353 c.pointerAt = [2]float64{x, y}
5454 if id != 0 {
55- if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec}); err != nil {
55+ if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}); err != nil {
5656 return err
5757 }
5858 }
@@ -107,7 +107,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
107107 delete(c.buttons, v.Button)
108108 }
109109 if id != 0 {
110- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {
110+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
111111 return err
112112 }
113113 // A click on an on-demand layer gives it the keyboard.
@@ -18,7 +18,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
18 }18 }
19 switch v := ev.(type) {19 switch v := ev.(type) {
20 case ports.PointerMotion:20 case ports.PointerMotion:
21- c.motionMsec = v.TimeMsec21+ c.motionTime = v.Time
22 // A locked pointer stays still; relative motion still flows.22 // A locked pointer stays still; relative motion still flows.
23 if c.constraint.Mode != ports.ConstraintLock {23 if c.constraint.Mode != ports.ConstraintLock {
24 c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))24 c.cursorX, c.cursorY = c.constraint.Clamp(c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y))
@@ -40,7 +40,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
40 }40 }
41 if c.drag != nil {41 if c.drag != nil {
42 // The dragged window and the others get no pointer events.42 // The dragged window and the others get no pointer events.
43- return c.dragMotion(ctx, v.TimeMsec)43+ return c.dragMotion(ctx, v.Time)
44 }44 }
45 id, x, y := c.hit(c.cursorX, c.cursorY)45 id, x, y := c.hit(c.cursorX, c.cursorY)
46 // Layout changes are intentionally re-hit-tested only on motion.46 // Layout changes are intentionally re-hit-tested only on motion.
@@ -52,7 +52,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
52 }52 }
53 c.pointerAt = [2]float64{x, y}53 c.pointerAt = [2]float64{x, y}
54 if id != 0 {54 if id != 0 {
55- if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec}); err != nil {55+ if err := c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time}); err != nil {
56 return err56 return err
57 }57 }
58 }58 }
@@ -107,7 +107,7 @@ func (c *Core) handleInput(ctx context.Context, ev ports.InputEvent) error {
107 delete(c.buttons, v.Button)107 delete(c.buttons, v.Button)
108 }108 }
109 if id != 0 {109 if id != 0 {
110- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {110+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
111 return err111 return err
112 }112 }
113 // A click on an on-demand layer gives it the keyboard.113 // A click on an on-demand layer gives it the keyboard.
M internal/core/security.go +4 -4
@@ -25,7 +25,7 @@ func (c *Core) syncSecurity() bool {
2525 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}
2626 c.pointer, c.grab, c.pointerOutput = 0, 0, ""
2727 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0
28- c.pointerAt, c.motionMsec = [2]float64{}, 0
28+ c.pointerAt, c.motionTime = [2]float64{}, 0
2929 c.keyboard = keyboard{}
3030 c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{}
3131 if c.ch.Constraints != nil {
@@ -249,7 +249,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
249249 return c.command(ctx, ports.ForwardKey{ID: id, Key: v})
250250 }
251251 case ports.PointerMotion:
252- c.motionMsec = v.TimeMsec
252+ c.motionTime = v.Time
253253 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)
254254 id, x, y := c.lockHit(c.cursorX, c.cursorY)
255255 if id != c.pointer {
@@ -260,7 +260,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
260260 }
261261 c.pointerAt = [2]float64{x, y}
262262 if id != 0 {
263- return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec})
263+ return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time})
264264 }
265265 case ports.PointerButton:
266266 id := c.pointer
@@ -288,7 +288,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
288288 }
289289 }
290290 }
291- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {
291+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
292292 return err
293293 }
294294 }
@@ -25,7 +25,7 @@ func (c *Core) syncSecurity() bool {
25 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}25 c.pressed, c.buttons, c.inputKeys = map[string]bool{}, map[uint32]bool{}, map[string]bool{}
26 c.pointer, c.grab, c.pointerOutput = 0, 0, ""26 c.pointer, c.grab, c.pointerOutput = 0, 0, ""
27 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 027 c.drag, c.swallow, c.mods, c.lastButton = nil, map[uint32]bool{}, 0, 0
28- c.pointerAt, c.motionMsec = [2]float64{}, 028+ c.pointerAt, c.motionTime = [2]float64{}, 0
29 c.keyboard = keyboard{}29 c.keyboard = keyboard{}
30 c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{}30 c.constrained, c.constraint = ports.PointerConstrained{}, ports.PointerConstraint{}
31 if c.ch.Constraints != nil {31 if c.ch.Constraints != nil {
@@ -249,7 +249,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
249 return c.command(ctx, ports.ForwardKey{ID: id, Key: v})249 return c.command(ctx, ports.ForwardKey{ID: id, Key: v})
250 }250 }
251 case ports.PointerMotion:251 case ports.PointerMotion:
252- c.motionMsec = v.TimeMsec252+ c.motionTime = v.Time
253 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)253 c.cursorX, c.cursorY = c.clampPointer(c.cursorX, c.cursorY, v.X, v.Y)
254 id, x, y := c.lockHit(c.cursorX, c.cursorY)254 id, x, y := c.lockHit(c.cursorX, c.cursorY)
255 if id != c.pointer {255 if id != c.pointer {
@@ -260,7 +260,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
260 }260 }
261 c.pointerAt = [2]float64{x, y}261 c.pointerAt = [2]float64{x, y}
262 if id != 0 {262 if id != 0 {
263- return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, TimeMsec: v.TimeMsec, TimeUsec: v.TimeUsec})263+ return c.command(ctx, ports.PointerMotionTo{ID: id, X: x, Y: y, DX: v.DX, DY: v.DY, UnaccelDX: v.UnaccelDX, UnaccelDY: v.UnaccelDY, Time: v.Time})
264 }264 }
265 case ports.PointerButton:265 case ports.PointerButton:
266 id := c.pointer266 id := c.pointer
@@ -288,7 +288,7 @@ func (c *Core) protectedInput(ctx context.Context, ev ports.InputEvent) error {
288 }288 }
289 }289 }
290 }290 }
291- if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, TimeMsec: v.TimeMsec}); err != nil {291+ if err := c.command(ctx, ports.PointerButtonTo{ID: id, Button: v.Button, Pressed: v.Pressed, Time: v.Time}); err != nil {
292 return err292 return err
293 }293 }
294 }294 }
M internal/ports/client_commands.go +8 -7
@@ -161,24 +161,25 @@ type PointerFocus struct {
161161 func (PointerFocus) clientCommand() {}
162162
163163 // PointerMotionTo carries core → wayland motion for the pointer focus, in
164-// window coordinates. The deltas and TimeUsec feed zwp_relative_pointer_v1;
164+// window coordinates. The deltas and Time feed zwp_relative_pointer_v1;
165165 // While the window locks the pointer, wayland sends only relative motion.
166166 type PointerMotionTo struct {
167167 ID WindowID
168168 X, Y float64
169169 DX, DY float64
170170 UnaccelDX, UnaccelDY float64
171- TimeMsec uint32
172- TimeUsec uint64
171+ // Time is the device timestamp (CLOCK_MONOTONIC).
172+ Time time.Duration
173173 }
174174
175175 func (PointerMotionTo) clientCommand() {}
176176
177177 type PointerButtonTo struct {
178- ID WindowID
179- Button uint32
180- Pressed bool
181- TimeMsec uint32
178+ ID WindowID
179+ Button uint32
180+ Pressed bool
181+ // Time is the device timestamp (CLOCK_MONOTONIC).
182+ Time time.Duration
182183 }
183184
184185 func (PointerButtonTo) clientCommand() {}
@@ -161,24 +161,25 @@ type PointerFocus struct {
161 func (PointerFocus) clientCommand() {}161 func (PointerFocus) clientCommand() {}
162 162
163 // PointerMotionTo carries core → wayland motion for the pointer focus, in163 // PointerMotionTo carries core → wayland motion for the pointer focus, in
164-// window coordinates. The deltas and TimeUsec feed zwp_relative_pointer_v1;164+// window coordinates. The deltas and Time feed zwp_relative_pointer_v1;
165 // While the window locks the pointer, wayland sends only relative motion.165 // While the window locks the pointer, wayland sends only relative motion.
166 type PointerMotionTo struct {166 type PointerMotionTo struct {
167 ID WindowID167 ID WindowID
168 X, Y float64168 X, Y float64
169 DX, DY float64169 DX, DY float64
170 UnaccelDX, UnaccelDY float64170 UnaccelDX, UnaccelDY float64
171- TimeMsec uint32171+ // Time is the device timestamp (CLOCK_MONOTONIC).
172- TimeUsec uint64172+ Time time.Duration
173 }173 }
174 174
175 func (PointerMotionTo) clientCommand() {}175 func (PointerMotionTo) clientCommand() {}
176 176
177 type PointerButtonTo struct {177 type PointerButtonTo struct {
178- ID WindowID178+ ID WindowID
179- Button uint32179+ Button uint32
180- Pressed bool180+ Pressed bool
181- TimeMsec uint32181+ // Time is the device timestamp (CLOCK_MONOTONIC).
182+ Time time.Duration
182 }183 }
183 184
184 func (PointerButtonTo) clientCommand() {}185 func (PointerButtonTo) clientCommand() {}
M internal/ports/input_events.go +19 -17
@@ -17,17 +17,18 @@ type InputEvent interface{ inputEvent() }
1717
1818 // KeyEvent carries input → core key transitions; Keysym is an xkb keysym name.
1919 // Keycode is the evdev code (xkb keycode - 8) and State the xkb modifier state
20-// after this transition; wayland forwards both to clients unchanged.
20+// after this transition; wayland forwards both to clients unchanged. Time is
21+// the device timestamp (CLOCK_MONOTONIC), like SwipeBegin.Time.
2122 type KeyEvent struct {
2223 Keysym string
2324 // Base is the key's unshifted keysym in the active layout, so Cmd+Shift+1
2425 // still matches a cmd+shift+1 bind although it prints exclam.
25- Base string
26- Mods Mods
27- Pressed bool
28- TimeMsec uint32
29- Keycode uint32
30- State ModState
26+ Base string
27+ Mods Mods
28+ Pressed bool
29+ Time time.Duration
30+ Keycode uint32
31+ State ModState
3132 }
3233
3334 // ModState is the serialized xkb modifier state sent in wl_keyboard.modifiers.
@@ -37,23 +38,23 @@ func (KeyEvent) inputEvent() {}
3738
3839 // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement).
3940 // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the
40-// raw device deltas; all four are 0 for absolute devices. TimeUsec is the
41-// device timestamp in microseconds.
41+// raw device deltas; all four are 0 for absolute devices. Time is the device
42+// timestamp (CLOCK_MONOTONIC).
4243 type PointerMotion struct {
4344 X, Y float64
4445 DX, DY float64
4546 UnaccelDX, UnaccelDY float64
46- TimeMsec uint32
47- TimeUsec uint64
47+ Time time.Duration
4848 }
4949
5050 func (PointerMotion) inputEvent() {}
5151
52-// PointerButton uses evdev button codes (BTN_LEFT is 0x110).
52+// PointerButton uses evdev button codes (BTN_LEFT is 0x110). Time is the
53+// device timestamp (CLOCK_MONOTONIC).
5354 type PointerButton struct {
54- Button uint32
55- Pressed bool
56- TimeMsec uint32
55+ Button uint32
56+ Pressed bool
57+ Time time.Duration
5758 }
5859
5960 func (PointerButton) inputEvent() {}
@@ -77,11 +78,12 @@ type ScrollAxis struct {
7778 Stop bool
7879 }
7980
80-// PointerAxis is one scroll frame: Vertical, then Horizontal.
81+// PointerAxis is one scroll frame: Vertical, then Horizontal. Time is the
82+// device timestamp (CLOCK_MONOTONIC).
8183 type PointerAxis struct {
8284 Source AxisSource
8385 Vertical, Horizontal ScrollAxis
84- TimeMsec uint32
86+ Time time.Duration
8587 }
8688
8789 func (PointerAxis) inputEvent() {}
@@ -17,17 +17,18 @@ type InputEvent interface{ inputEvent() }
17 17
18 // KeyEvent carries input → core key transitions; Keysym is an xkb keysym name.18 // KeyEvent carries input → core key transitions; Keysym is an xkb keysym name.
19 // Keycode is the evdev code (xkb keycode - 8) and State the xkb modifier state19 // Keycode is the evdev code (xkb keycode - 8) and State the xkb modifier state
20-// after this transition; wayland forwards both to clients unchanged.20+// after this transition; wayland forwards both to clients unchanged. Time is
21+// the device timestamp (CLOCK_MONOTONIC), like SwipeBegin.Time.
21 type KeyEvent struct {22 type KeyEvent struct {
22 Keysym string23 Keysym string
23 // Base is the key's unshifted keysym in the active layout, so Cmd+Shift+124 // Base is the key's unshifted keysym in the active layout, so Cmd+Shift+1
24 // still matches a cmd+shift+1 bind although it prints exclam.25 // still matches a cmd+shift+1 bind although it prints exclam.
25- Base string26+ Base string
26- Mods Mods27+ Mods Mods
27- Pressed bool28+ Pressed bool
28- TimeMsec uint3229+ Time time.Duration
29- Keycode uint3230+ Keycode uint32
30- State ModState31+ State ModState
31 }32 }
32 33
33 // ModState is the serialized xkb modifier state sent in wl_keyboard.modifiers.34 // ModState is the serialized xkb modifier state sent in wl_keyboard.modifiers.
@@ -37,23 +38,23 @@ func (KeyEvent) inputEvent() {}
37 38
38 // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement).39 // PointerMotion uses global layout coordinates in logical pixels (see OutputPlacement).
39 // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the40 // DX and DY are the accelerated logical deltas and UnaccelDX, UnaccelDY the
40-// raw device deltas; all four are 0 for absolute devices. TimeUsec is the41+// raw device deltas; all four are 0 for absolute devices. Time is the device
41-// device timestamp in microseconds.42+// timestamp (CLOCK_MONOTONIC).
42 type PointerMotion struct {43 type PointerMotion struct {
43 X, Y float6444 X, Y float64
44 DX, DY float6445 DX, DY float64
45 UnaccelDX, UnaccelDY float6446 UnaccelDX, UnaccelDY float64
46- TimeMsec uint3247+ Time time.Duration
47- TimeUsec uint64
48 }48 }
49 49
50 func (PointerMotion) inputEvent() {}50 func (PointerMotion) inputEvent() {}
51 51
52-// PointerButton uses evdev button codes (BTN_LEFT is 0x110).52+// PointerButton uses evdev button codes (BTN_LEFT is 0x110). Time is the
53+// device timestamp (CLOCK_MONOTONIC).
53 type PointerButton struct {54 type PointerButton struct {
54- Button uint3255+ Button uint32
55- Pressed bool56+ Pressed bool
56- TimeMsec uint3257+ Time time.Duration
57 }58 }
58 59
59 func (PointerButton) inputEvent() {}60 func (PointerButton) inputEvent() {}
@@ -77,11 +78,12 @@ type ScrollAxis struct {
77 Stop bool78 Stop bool
78 }79 }
79 80
80-// PointerAxis is one scroll frame: Vertical, then Horizontal.81+// PointerAxis is one scroll frame: Vertical, then Horizontal. Time is the
82+// device timestamp (CLOCK_MONOTONIC).
81 type PointerAxis struct {83 type PointerAxis struct {
82 Source AxisSource84 Source AxisSource
83 Vertical, Horizontal ScrollAxis85 Vertical, Horizontal ScrollAxis
84- TimeMsec uint3286+ Time time.Duration
85 }87 }
86 88
87 func (PointerAxis) inputEvent() {}89 func (PointerAxis) inputEvent() {}

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 25

Tags 10

↑↓ moveenter openesc close