1// Package libinput reads keyboards and pointers through libinput's udev backend.
2package libinput
3
4import (
5 "context"
6 "errors"
7 "fmt"
8 "strings"
9 "sync"
10 "time"
11 "unsafe"
12
13 "github.com/bnema/neferwl/internal/adapters/xkb"
14 "github.com/bnema/neferwl/internal/ports"
15 "github.com/bnema/purego"
16 "github.com/bnema/zerowrap"
17 "golang.org/x/sys/unix"
18)
19
20// ErrEmergencyQuit is returned when Ctrl+Alt+Backspace is pressed.
21var ErrEmergencyQuit = errors.New("emergency quit key")
22
23const (
24 evDeviceAdded = 1
25 evDeviceRemoved = 2
26 evKeyboardKey = 300
27 evPointerMotion = 400
28 evPointerAbs = 401
29 evPointerButton = 402
30 evScrollWheel = 404
31 evScrollFinger = 405
32 evScrollCont = 406
33)
34
35var (
36 loadOnce sync.Once
37 loadErr error
38
39 udevNew func() uintptr
40 udevUnref func(uintptr) uintptr
41 createContext func(iface unsafe.Pointer, data uintptr, udev uintptr) uintptr
42 assignSeat func(li uintptr, seat string) int32
43 unref func(li uintptr) uintptr
44 getFD func(li uintptr) int32
45 dispatch func(li uintptr) int32
46 getEvent func(li uintptr) uintptr
47 eventType func(ev uintptr) int32
48 eventDestroy func(ev uintptr)
49 eventDevice func(ev uintptr) uintptr
50 deviceName func(dev uintptr) string
51 suspend func(li uintptr)
52 resume func(li uintptr) int32
53 keyboardEvent func(ev uintptr) uintptr
54 keyboardKey func(kev uintptr) uint32
55 keyboardState func(kev uintptr) int32
56 pointerEvent func(ev uintptr) uintptr
57 pointerDX func(pev uintptr) float64
58 pointerDY func(pev uintptr) float64
59 pointerRawDX func(pev uintptr) float64
60 pointerRawDY func(pev uintptr) float64
61 pointerUsec func(pev uintptr) uint64
62 pointerAbsX func(pev uintptr, width uint32) float64
63 pointerAbsY func(pev uintptr, height uint32) float64
64 pointerButton func(pev uintptr) uint32
65 pointerBtnState func(pev uintptr) int32
66 pointerHasAxis func(pev uintptr, axis uint32) int32
67 scrollValue func(pev uintptr, axis uint32) float64
68 scrollV120 func(pev uintptr, axis uint32) float64
69 iface [2]uintptr
70 active ports.Seat
71)
72
73func load() error {
74 loadOnce.Do(func() {
75 udev, err := purego.Dlopen("libudev.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
76 if err != nil {
77 loadErr = fmt.Errorf("open libudev.so.1: %w", err)
78 return
79 }
80 lib, err := purego.Dlopen("libinput.so.10", purego.RTLD_NOW|purego.RTLD_LOCAL)
81 if err != nil {
82 loadErr = fmt.Errorf("open libinput.so.10: %w", err)
83 return
84 }
85 defer func() {
86 if r := recover(); r != nil {
87 loadErr = fmt.Errorf("libinput: %v", r)
88 }
89 }()
90 purego.RegisterLibFunc(&udevNew, udev, "udev_new")
91 purego.RegisterLibFunc(&udevUnref, udev, "udev_unref")
92 reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, "libinput_"+name) }
93 reg(&createContext, "udev_create_context")
94 reg(&assignSeat, "udev_assign_seat")
95 reg(&unref, "unref")
96 reg(&getFD, "get_fd")
97 reg(&dispatch, "dispatch")
98 reg(&getEvent, "get_event")
99 reg(&eventType, "event_get_type")
100 reg(&eventDestroy, "event_destroy")
101 reg(&eventDevice, "event_get_device")
102 reg(&deviceName, "device_get_name")
103 reg(&suspend, "suspend")
104 reg(&resume, "resume")
105 reg(&keyboardEvent, "event_get_keyboard_event")
106 reg(&keyboardKey, "event_keyboard_get_key")
107 reg(&keyboardState, "event_keyboard_get_key_state")
108 reg(&pointerEvent, "event_get_pointer_event")
109 reg(&pointerDX, "event_pointer_get_dx")
110 reg(&pointerDY, "event_pointer_get_dy")
111 reg(&pointerRawDX, "event_pointer_get_dx_unaccelerated")
112 reg(&pointerRawDY, "event_pointer_get_dy_unaccelerated")
113 reg(&pointerUsec, "event_pointer_get_time_usec")
114 reg(&pointerAbsX, "event_pointer_get_absolute_x_transformed")
115 reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed")
116 reg(&pointerButton, "event_pointer_get_button")
117 reg(&pointerBtnState, "event_pointer_get_button_state")
118 reg(&pointerHasAxis, "event_pointer_has_axis")
119 reg(&scrollValue, "event_pointer_get_scroll_value")
120 reg(&scrollV120, "event_pointer_get_scroll_value_v120")
121 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {
122 fd, err := active.OpenDevice(goString(path))
123 if err != nil {
124 return uintptr(uint32(^uint32(unix.EACCES) + 1)) // -EACCES as C int
125 }
126 return uintptr(fd)
127 })
128 iface[1] = purego.NewCallback(func(fd, _ uintptr) { active.CloseDevice(int(int32(fd))) })
129 })
130 return loadErr
131}
132
133func goString(p *byte) string {
134 if p == nil {
135 return ""
136 }
137 n := 0
138 for *(*byte)(unsafe.Add(unsafe.Pointer(p), n)) != 0 {
139 n++
140 }
141 return string(unsafe.Slice(p, n))
142}
143
144// Options configures the input adapter.
145type Options struct {
146 Seat ports.Seat
147 SeatName string
148 Keymap *xkb.Keymap
149 // Keymaps replaces Keymap live; Run takes ownership and closes the old one.
150 Keymaps <-chan *xkb.Keymap
151 // Layout is the initial output layout; Layouts replaces it live.
152 Layout ports.Layout
153 Layouts <-chan ports.Layout
154 // Constraints holds the pointer lock or confinement of the focused
155 // window, in global logical coordinates.
156 Constraints <-chan ports.PointerConstraint
157 Active <-chan bool
158 // MoveCursor, when set, places the hardware cursor as soon as motion is
159 // read, before core sees the event: the output under the pointer and
160 // the physical position on it.
161 MoveCursor func(output string, x, y float64)
162 Log zerowrap.Logger
163 // LogMotion logs every pointer motion (--debug=input-motion); motions
164 // are otherwise only counted in the input stats.
165 LogMotion bool
166}
167
168// statsEvery is how often Run logs input throughput.
169const statsEvery = 10 * time.Second
170
171// Run owns libinput and the keymap, converting device events into input events.
172func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error {
173 defer func() { opts.Keymap.Close() }()
174 if err := load(); err != nil {
175 return err
176 }
177 active = opts.Seat
178 udev := udevNew()
179 if udev == 0 {
180 return errors.New("udev_new failed")
181 }
182 defer udevUnref(udev)
183 li := createContext(unsafe.Pointer(&iface), 0, udev)
184 if li == 0 {
185 return errors.New("libinput_udev_create_context failed")
186 }
187 defer unref(li)
188 name := opts.SeatName
189 if name == "" {
190 name = "seat0"
191 }
192 if assignSeat(li, name) != 0 {
193 return fmt.Errorf("libinput_udev_assign_seat %s failed", name)
194 }
195 p := newPointer(opts.Layout)
196 p.moved(opts.MoveCursor)
197 fd := getFD(li)
198 fwd := newForwarder(opts.Log)
199 fwdCtx, stopFwd := context.WithCancel(ctx)
200 var fwdDone sync.WaitGroup
201 fwdDone.Go(func() { fwd.run(fwdCtx, input) })
202 defer func() { stopFwd(); fwdDone.Wait() }()
203 lastStats := time.Now()
204 for ctx.Err() == nil {
205 if time.Since(lastStats) >= statsEvery {
206 lastStats = time.Now()
207 if s := fwd.take(); s.Motions > 0 || s.Sent > 0 {
208 opts.Log.Info().Int("motions", s.Motions).Int("coalesced", s.Coalesced).Int("sent", s.Sent).Dur("max_block_ms", s.MaxBlock).Msg("input stats")
209 }
210 }
211 select {
212 case on := <-opts.Active:
213 if on {
214 resume(li)
215 } else {
216 suspend(li)
217 }
218 opts.Log.Info().Bool("active", on).Msg("input")
219 case km := <-opts.Keymaps:
220 opts.Keymap.Close()
221 opts.Keymap = km
222 opts.Log.Info().Msg("keymap replaced")
223 case l := <-opts.Layouts:
224 p.setLayout(l)
225 p.moved(opts.MoveCursor)
226 case c := <-opts.Constraints:
227 p.constrain(c)
228 p.moved(opts.MoveCursor)
229 default:
230 }
231 fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}}
232 n, err := unix.Poll(fds, 100)
233 if err != nil && !errors.Is(err, unix.EINTR) {
234 return fmt.Errorf("libinput poll: %w", err)
235 }
236 if n <= 0 {
237 continue
238 }
239 if dispatch(li) != 0 {
240 opts.Log.Warn().Msg("libinput_dispatch failed")
241 }
242 for ev := getEvent(li); ev != 0; ev = getEvent(li) {
243 out, err := translate(ev, opts, p)
244 eventDestroy(ev)
245 if err != nil {
246 return err
247 }
248 if out != nil {
249 fwd.push(out)
250 }
251 }
252 }
253 return nil
254}
255
256func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) {
257 now := uint32(time.Now().UnixMilli())
258 log := opts.Log
259 switch eventType(ev) {
260 case evDeviceAdded:
261 log.Info().Str("device", deviceName(eventDevice(ev))).Msg("input device added")
262 case evDeviceRemoved:
263 log.Info().Str("device", deviceName(eventDevice(ev))).Msg("input device removed")
264 case evKeyboardKey:
265 k := keyboardEvent(ev)
266 code, pressed := keyboardKey(k), keyboardState(k) == 1
267 ke := opts.Keymap.Key(code, pressed, now)
268 log.Debug().Uint32("code", code).Str("keysym", ke.Keysym).Bool("pressed", pressed).Uint8("mods", uint8(ke.Mods)).Msg("key")
269 switch action, vt := hotkey(ke); action {
270 case hotkeyQuit:
271 log.Warn().Str("reason", "emergency-key").Msg("quit")
272 return nil, ErrEmergencyQuit
273 case hotkeyVT:
274 opts.Seat.SwitchVT(vt)
275 return nil, nil
276 }
277 return ke, nil
278 case evPointerMotion:
279 pe := pointerEvent(ev)
280 m := p.move(pointerDX(pe), pointerDY(pe))
281 m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)
282 m.TimeMsec, m.TimeUsec = now, pointerUsec(pe)
283 p.moved(opts.MoveCursor)
284 if opts.LogMotion {
285 log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
286 }
287 return m, nil
288 case evPointerAbs:
289 // Absolute devices (tablets, VMs) map to the whole layout.
290 pe := pointerEvent(ev)
291 b := p.bounds()
292 if p.constraint.Mode == ports.ConstraintLock {
293 return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil
294 }
295 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
296 p.moved(opts.MoveCursor)
297 return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil
298 case evPointerButton:
299 pe := pointerEvent(ev)
300 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
301 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
302 return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: now}, nil
303 case evScrollWheel, evScrollFinger, evScrollCont:
304 pe := pointerEvent(ev)
305 // The three scroll events follow each other as the sources do.
306 source := ports.AxisSource(eventType(ev) - evScrollWheel)
307 a := ports.PointerAxis{Source: source, TimeMsec: now}
308 // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
309 for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
310 if pointerHasAxis(pe, uint32(i)) == 0 {
311 continue
312 }
313 s.Set, s.Value = true, scrollValue(pe, uint32(i))
314 if source == ports.AxisWheel {
315 s.V120 = int32(scrollV120(pe, uint32(i)))
316 } else {
317 s.Stop = s.Value == 0
318 }
319 }
320 log.Debug().Float64("v", a.Vertical.Value).Float64("h", a.Horizontal.Value).Int32("v120", a.Vertical.V120).Int32("hv120", a.Horizontal.V120).Uint8("source", uint8(a.Source)).Msg("scroll")
321 return a, nil
322 }
323 return nil, nil
324}
325
326type hotkeyAction int
327
328const (
329 hotkeyNone hotkeyAction = iota
330 hotkeyQuit
331 hotkeyVT
332)
333
334// hotkey detects compositor-reserved keys on press: Ctrl+Alt+Backspace and Ctrl+Alt+F1..F12.
335func hotkey(ke ports.KeyEvent) (hotkeyAction, int) {
336 if !ke.Pressed {
337 return hotkeyNone, 0
338 }
339 if vt, ok := strings.CutPrefix(ke.Keysym, "XF86Switch_VT_"); ok {
340 var n int
341 if _, err := fmt.Sscan(vt, &n); err == nil && n >= 1 && n <= 12 {
342 return hotkeyVT, n
343 }
344 }
345 if ke.Mods&(ports.ModCtrl|ports.ModAlt) != ports.ModCtrl|ports.ModAlt {
346 return hotkeyNone, 0
347 }
348 switch {
349 case ke.Keysym == "BackSpace" || ke.Keycode == 14:
350 return hotkeyQuit, 0
351 case ke.Keycode >= 59 && ke.Keycode <= 68: // F1..F10
352 return hotkeyVT, int(ke.Keycode) - 58
353 case ke.Keycode == 87 || ke.Keycode == 88: // F11, F12
354 return hotkeyVT, int(ke.Keycode) - 76
355 }
356 return hotkeyNone, 0
357}
358
359// pointer tracks the cursor in global logical coordinates. Relative motion
360// is divided by the scale of the output under the pointer, so it moves the
361// same number of physical pixels on every output.
362type pointer struct {
363 x, y float64
364 layout ports.Layout
365 touched bool // false until the user moves it: it follows the primary output
366 constraint ports.PointerConstraint
367}
368
369// newPointer starts at the centre of the primary output, else the first.
370func newPointer(l ports.Layout) *pointer {
371 p := &pointer{}
372 p.setLayout(l)
373 return p
374}
375
376// setLayout keeps the pointer where it is, or clamps it onto an output.
377// Until it is first moved it stays centred on the primary output, which
378// may be plugged in after the others.
379func (p *pointer) setLayout(l ports.Layout) {
380 p.layout = l
381 if !p.touched && len(l) > 0 {
382 o := l[0]
383 for _, v := range l {
384 if v.Primary {
385 o = v
386 }
387 }
388 p.x, p.y = float64(o.X)+float64(o.Width)/2, float64(o.Y)+float64(o.Height)/2
389 return
390 }
391 p.x, p.y = l.Clamp(p.x, p.y, p.x, p.y)
392}
393
394func (p *pointer) scale() float64 {
395 if o, ok := p.layout.At(p.x, p.y); ok && o.Scale > 0 {
396 return o.Scale
397 }
398 return 1
399}
400
401// move applies a relative motion and returns it with the logical deltas.
402// A locked pointer stays still; a confined one stays in its rectangle.
403func (p *pointer) move(dx, dy float64) ports.PointerMotion {
404 s := p.scale()
405 dx, dy = dx/s, dy/s
406 if p.constraint.Mode != ports.ConstraintLock {
407 p.set(p.x+dx, p.y+dy)
408 }
409 return ports.PointerMotion{X: p.x, Y: p.y, DX: dx, DY: dy}
410}
411
412func (p *pointer) set(x, y float64) (float64, float64) {
413 p.touched = true
414 p.x, p.y = p.constraint.Clamp(p.layout.Clamp(p.x, p.y, x, y))
415 return p.x, p.y
416}
417
418// constrain applies a new constraint. Entering or leaving a lock resyncs
419// the pointer to core's cursor, which stood still meanwhile; a confined
420// pointer moves inside.
421func (p *pointer) constrain(c ports.PointerConstraint) {
422 if c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
423 p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
424 }
425 p.constraint = c
426 p.x, p.y = c.Clamp(p.x, p.y)
427}
428
429// bounds is the rectangle holding every output.
430func (p *pointer) bounds() ports.Rect {
431 var r ports.Rect
432 for _, o := range p.layout {
433 r.W = max(r.W, o.X+o.Width)
434 r.H = max(r.H, o.Y+o.Height)
435 }
436 return r
437}
438
439// moved reports the physical position on the output under the pointer.
440func (p *pointer) moved(move func(output string, x, y float64)) {
441 if o, ok := p.layout.At(p.x, p.y); ok && move != nil {
442 move(o.Info.Name, (p.x-float64(o.X))*o.Scale, (p.y-float64(o.Y))*o.Scale)
443 }
444}