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 evSwipeBegin = 800
34 evSwipeUpdate = 801
35 evSwipeEnd = 802
36)
37
38var (
39 loadOnce sync.Once
40 loadErr error
41
42 udevNew func() uintptr
43 udevUnref func(uintptr) uintptr
44 createContext func(iface unsafe.Pointer, data uintptr, udev uintptr) uintptr
45 assignSeat func(li uintptr, seat string) int32
46 unref func(li uintptr) uintptr
47 getFD func(li uintptr) int32
48 dispatch func(li uintptr) int32
49 getEvent func(li uintptr) uintptr
50 eventType func(ev uintptr) int32
51 eventDestroy func(ev uintptr)
52 eventDevice func(ev uintptr) uintptr
53 deviceName func(dev uintptr) string
54 suspend func(li uintptr)
55 resume func(li uintptr) int32
56 keyboardEvent func(ev uintptr) uintptr
57 keyboardKey func(kev uintptr) uint32
58 keyboardState func(kev uintptr) int32
59 keyboardUsec func(kev uintptr) uint64
60 pointerEvent func(ev uintptr) uintptr
61 pointerDX func(pev uintptr) float64
62 pointerDY func(pev uintptr) float64
63 pointerRawDX func(pev uintptr) float64
64 pointerRawDY func(pev uintptr) float64
65 pointerUsec func(pev uintptr) uint64
66 pointerAbsX func(pev uintptr, width uint32) float64
67 pointerAbsY func(pev uintptr, height uint32) float64
68 pointerButton func(pev uintptr) uint32
69 pointerBtnState func(pev uintptr) int32
70 pointerHasAxis func(pev uintptr, axis uint32) int32
71 scrollValue func(pev uintptr, axis uint32) float64
72 scrollV120 func(pev uintptr, axis uint32) float64
73 gestureEvent func(ev uintptr) uintptr
74 gestureFingers func(gev uintptr) int32
75 gestureCanceled func(gev uintptr) int32
76 gestureDX func(gev uintptr) float64
77 gestureDY func(gev uintptr) float64
78 gestureUsec func(gev uintptr) uint64
79 deviceRef func(dev uintptr) uintptr
80 deviceUnref func(dev uintptr) uintptr
81 hasNatural func(dev uintptr) int32
82 setNatural func(dev uintptr, on int32) int32
83 tapFingers func(dev uintptr) int32
84 setTap func(dev uintptr, on int32) int32
85 setTapMap func(dev uintptr, m int32) int32
86 hasCapability func(dev uintptr, capability int32) int32
87 accelAvailable func(dev uintptr) int32
88 setAccelSpeed func(dev uintptr, speed float64) int32
89 accelProfiles func(dev uintptr) uint32
90 setAccelProfile func(dev uintptr, profile uint32) int32
91 iface [2]uintptr
92 active ports.Seat
93)
94
95func load() error {
96 loadOnce.Do(func() {
97 udev, err := purego.Dlopen("libudev.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
98 if err != nil {
99 loadErr = fmt.Errorf("open libudev.so.1: %w", err)
100 return
101 }
102 lib, err := purego.Dlopen("libinput.so.10", purego.RTLD_NOW|purego.RTLD_LOCAL)
103 if err != nil {
104 loadErr = fmt.Errorf("open libinput.so.10: %w", err)
105 return
106 }
107 defer func() {
108 if r := recover(); r != nil {
109 loadErr = fmt.Errorf("libinput: %v", r)
110 }
111 }()
112 purego.RegisterLibFunc(&udevNew, udev, "udev_new")
113 purego.RegisterLibFunc(&udevUnref, udev, "udev_unref")
114 reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, "libinput_"+name) }
115 reg(&createContext, "udev_create_context")
116 reg(&assignSeat, "udev_assign_seat")
117 reg(&unref, "unref")
118 reg(&getFD, "get_fd")
119 reg(&dispatch, "dispatch")
120 reg(&getEvent, "get_event")
121 reg(&eventType, "event_get_type")
122 reg(&eventDestroy, "event_destroy")
123 reg(&eventDevice, "event_get_device")
124 reg(&deviceName, "device_get_name")
125 reg(&suspend, "suspend")
126 reg(&resume, "resume")
127 reg(&keyboardEvent, "event_get_keyboard_event")
128 reg(&keyboardKey, "event_keyboard_get_key")
129 reg(&keyboardState, "event_keyboard_get_key_state")
130 reg(&keyboardUsec, "event_keyboard_get_time_usec")
131 reg(&pointerEvent, "event_get_pointer_event")
132 reg(&pointerDX, "event_pointer_get_dx")
133 reg(&pointerDY, "event_pointer_get_dy")
134 reg(&pointerRawDX, "event_pointer_get_dx_unaccelerated")
135 reg(&pointerRawDY, "event_pointer_get_dy_unaccelerated")
136 reg(&pointerUsec, "event_pointer_get_time_usec")
137 reg(&pointerAbsX, "event_pointer_get_absolute_x_transformed")
138 reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed")
139 reg(&pointerButton, "event_pointer_get_button")
140 reg(&pointerBtnState, "event_pointer_get_button_state")
141 reg(&pointerHasAxis, "event_pointer_has_axis")
142 reg(&scrollValue, "event_pointer_get_scroll_value")
143 reg(&scrollV120, "event_pointer_get_scroll_value_v120")
144 reg(&gestureEvent, "event_get_gesture_event")
145 reg(&gestureFingers, "event_gesture_get_finger_count")
146 reg(&gestureCanceled, "event_gesture_get_cancelled")
147 reg(&gestureDX, "event_gesture_get_dx_unaccelerated")
148 reg(&gestureDY, "event_gesture_get_dy_unaccelerated")
149 reg(&gestureUsec, "event_gesture_get_time_usec")
150 reg(&deviceRef, "device_ref")
151 reg(&deviceUnref, "device_unref")
152 reg(&hasNatural, "device_config_scroll_has_natural_scroll")
153 reg(&setNatural, "device_config_scroll_set_natural_scroll_enabled")
154 reg(&tapFingers, "device_config_tap_get_finger_count")
155 reg(&setTap, "device_config_tap_set_enabled")
156 reg(&setTapMap, "device_config_tap_set_button_map")
157 reg(&hasCapability, "device_has_capability")
158 reg(&accelAvailable, "device_config_accel_is_available")
159 reg(&setAccelSpeed, "device_config_accel_set_speed")
160 reg(&accelProfiles, "device_config_accel_get_profiles")
161 reg(&setAccelProfile, "device_config_accel_set_profile")
162 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {
163 fd, err := active.OpenDevice(goString(path))
164 if err != nil {
165 return uintptr(uint32(^uint32(unix.EACCES) + 1)) // -EACCES as C int
166 }
167 return uintptr(fd)
168 })
169 iface[1] = purego.NewCallback(func(fd, _ uintptr) { active.CloseDevice(int(int32(fd))) })
170 })
171 return loadErr
172}
173
174func goString(p *byte) string {
175 if p == nil {
176 return ""
177 }
178 n := 0
179 for *(*byte)(unsafe.Add(unsafe.Pointer(p), n)) != 0 {
180 n++
181 }
182 return string(unsafe.Slice(p, n))
183}
184
185// Options configures the input adapter.
186type Options struct {
187 // Security gates credential logging and emergency quit and stamps input
188 // before forwarding. Nil preserves raw unlocked behavior; core remains
189 // responsible for input admission.
190 Security ports.SessionSecurity
191 Seat ports.Seat
192 SeatName string
193 Keymap *xkb.Keymap
194 // Keymaps replaces Keymap live; Run takes ownership and closes the old one.
195 Keymaps <-chan *xkb.Keymap
196 // Layout is the initial output layout; Layouts replaces it live.
197 Layout ports.Layout
198 Layouts <-chan ports.Layout
199 // Constraints holds the pointer lock or confinement of the focused
200 // window, in global logical coordinates.
201 Constraints <-chan ports.PointerConstraint
202 // Touchpad is the initial touchpad config; Touchpads replaces it live.
203 Touchpad ports.TouchpadConfig
204 Touchpads <-chan ports.TouchpadConfig
205 Active <-chan bool
206 // MoveCursor, when set, places the hardware cursor as soon as motion is
207 // read, before core sees the event: the output under the pointer and
208 // the physical position on it. motion is false when the pointer is only
209 // placed again after a layout or constraint change.
210 MoveCursor func(output string, x, y float64, motion bool)
211 Log zerowrap.Logger
212 // LogMotion logs every pointer motion (--debug=input-motion); motions
213 // are otherwise only counted in the input stats.
214 LogMotion bool
215 // LogKeys logs every key (--debug=input-keys): typed text, off by
216 // default.
217 LogKeys bool
218}
219
220// statsEvery is how often Run logs input throughput.
221const statsEvery = 10 * time.Second
222
223// Run owns libinput and the keymap, converting device events into input events.
224func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error {
225 defer func() { opts.Keymap.Close() }()
226 if err := load(); err != nil {
227 return err
228 }
229 active = opts.Seat
230 udev := udevNew()
231 if udev == 0 {
232 return errors.New("udev_new failed")
233 }
234 defer udevUnref(udev)
235 li := createContext(unsafe.Pointer(&iface), 0, udev)
236 if li == 0 {
237 return errors.New("libinput_udev_create_context failed")
238 }
239 defer unref(li)
240 name := opts.SeatName
241 if name == "" {
242 name = "seat0"
243 }
244 if assignSeat(li, name) != 0 {
245 return fmt.Errorf("libinput_udev_assign_seat %s failed", name)
246 }
247 p := newPointer(opts.Layout)
248 p.moved(opts.MoveCursor, false)
249 in := &inputState{touchpad: opts.Touchpad, devices: map[uintptr]bool{}}
250 defer in.release()
251 fd := getFD(li)
252 fwd := newForwarder(opts.Log)
253 fwdCtx, stopFwd := context.WithCancel(ctx)
254 var fwdDone sync.WaitGroup
255 fwdDone.Go(func() { fwd.run(fwdCtx, input) })
256 defer func() { stopFwd(); fwdDone.Wait() }()
257 lastStats := time.Now()
258 for ctx.Err() == nil {
259 if time.Since(lastStats) >= statsEvery {
260 lastStats = time.Now()
261 if s := fwd.take(); s.Motions > 0 || s.Sent > 0 {
262 opts.Log.Info().Int("motions", s.Motions).Int("coalesced", s.Coalesced).Int("sent", s.Sent).Dur("max_block_ms", s.MaxBlock).Msg("input stats")
263 }
264 }
265 select {
266 case on := <-opts.Active:
267 if on {
268 resume(li)
269 } else {
270 suspend(li)
271 }
272 opts.Log.Info().Bool("active", on).Msg("input")
273 case km := <-opts.Keymaps:
274 if opts.Security != nil {
275 km.QuarantineFrom(opts.Keymap)
276 }
277 opts.Keymap.Close()
278 opts.Keymap = km
279 opts.Log.Info().Msg("keymap replaced")
280 case l := <-opts.Layouts:
281 p.setLayout(l)
282 p.moved(opts.MoveCursor, false)
283 case c := <-opts.Constraints:
284 p.constrain(c)
285 p.moved(opts.MoveCursor, false)
286 case t := <-opts.Touchpads:
287 in.touchpad = t
288 for dev := range in.devices {
289 in.configure(dev, opts.Log)
290 }
291 opts.Log.Info().Bool("natural_scroll", t.NaturalScroll).Bool("tap", t.Tap).Float64("accel_speed", t.AccelSpeed).Str("accel_profile", t.AccelProfile).Float64("scroll_factor", t.ScrollFactor).Msg("touchpad reloaded")
292 default:
293 }
294 fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}}
295 n, err := unix.Poll(fds, 100)
296 if err != nil && !errors.Is(err, unix.EINTR) {
297 return fmt.Errorf("libinput poll: %w", err)
298 }
299 if n <= 0 {
300 continue
301 }
302 if dispatch(li) != 0 {
303 opts.Log.Warn().Msg("libinput_dispatch failed")
304 }
305 for ev := getEvent(li); ev != 0; ev = getEvent(li) {
306 out, err := translate(ev, opts, p, in)
307 eventDestroy(ev)
308 if err != nil {
309 return err
310 }
311 if out != nil {
312 fwd.push(out)
313 }
314 }
315 }
316 return nil
317}
318
319// inputState is the device state Run owns: touchpad config, the devices
320// it configures (referenced until removed) and the swipe in progress on
321// each touchpad.
322type inputState struct {
323 touchpad ports.TouchpadConfig
324 devices map[uintptr]bool
325 swipes swipes
326}
327
328// configurable reports whether the touchpad config applies to dev: it has
329// natural scroll or tapping, or is a touchpad.
330func configurable(dev uintptr) bool {
331 return hasNatural(dev) != 0 || tapFingers(dev) > 0 || touchpad(dev)
332}
333
334// capGesture is LIBINPUT_DEVICE_CAP_GESTURE: libinput gives it to
335// touchpads, with or without tapping.
336const capGesture = 5
337
338func touchpad(dev uintptr) bool { return hasCapability(dev, capGesture) != 0 }
339
340// tapButtonMap is LIBINPUT_CONFIG_TAP_MAP_LRM: one finger taps left, two
341// right, three middle.
342const tapButtonMap = 0
343
344func (s *inputState) configure(dev uintptr, log zerowrap.Logger) {
345 if hasNatural(dev) != 0 && setNatural(dev, flag(s.touchpad.NaturalScroll)) != 0 {
346 log.Warn().Str("device", deviceName(dev)).Msg("natural scroll rejected")
347 }
348 if tapFingers(dev) > 0 {
349 if setTap(dev, flag(s.touchpad.Tap)) != 0 {
350 log.Warn().Str("device", deviceName(dev)).Msg("tap to click rejected")
351 }
352 if setTapMap(dev, tapButtonMap) != 0 {
353 log.Warn().Str("device", deviceName(dev)).Msg("tap button map rejected")
354 }
355 }
356 // Pointer speed is a touchpad setting: mice keep libinput's own.
357 if touchpad(dev) && accelAvailable(dev) != 0 {
358 if setAccelSpeed(dev, s.touchpad.AccelSpeed) != 0 {
359 log.Warn().Str("device", deviceName(dev)).Msg("accel speed rejected")
360 }
361 if p := accelProfile(s.touchpad.AccelProfile); accelProfiles(dev)&p != 0 && setAccelProfile(dev, p) != 0 {
362 log.Warn().Str("device", deviceName(dev)).Msg("accel profile rejected")
363 }
364 }
365}
366
367// accelProfile is the libinput accel profile bit: LIBINPUT_CONFIG_ACCEL_
368// PROFILE_FLAT (1) or _ADAPTIVE (2).
369func accelProfile(name string) uint32 {
370 if name == ports.AccelFlat {
371 return 1
372 }
373 return 2
374}
375
376func flag(on bool) int32 {
377 if on {
378 return 1
379 }
380 return 0
381}
382
383func (s *inputState) release() {
384 for dev := range s.devices {
385 deviceUnref(dev)
386 }
387 clear(s.devices)
388}
389
390// swipes streams one three- or four-finger swipe at a time to core, which
391// follows
392// the fingers. owner is the touchpad of the swipe in progress (0: none);
393// another touchpad's swipe is ignored until it ends, so core never sees
394// two streams interleaved.
395type swipes struct{ owner uintptr }
396
397func (s *swipes) begin(dev uintptr, fingers int, at time.Duration) ports.InputEvent {
398 if (fingers != 3 && fingers != 4) || s.owner != 0 {
399 return nil
400 }
401 s.owner = dev
402 return ports.SwipeBegin{Fingers: fingers, Time: at}
403}
404
405func (s *swipes) update(dev uintptr, dx, dy float64, at time.Duration) ports.InputEvent {
406 if dev == 0 || s.owner != dev {
407 return nil
408 }
409 return ports.SwipeUpdate{DX: dx, DY: dy, Time: at}
410}
411
412func (s *swipes) end(dev uintptr, cancelled bool, at time.Duration) ports.InputEvent {
413 if dev == 0 || s.owner != dev {
414 return nil
415 }
416 s.owner = 0
417 return ports.SwipeEnd{Cancelled: cancelled, Time: at}
418}
419
420// usec is a libinput timestamp as a duration.
421func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond }
422
423// msec is a libinput timestamp in wayland's millisecond clock.
424func msec(v uint64) uint32 { return uint32(v / 1000) }
425
426// translate converts one libinput event. Timestamps are the device's
427// (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on
428// when events happened, not when they were read.
429// It snapshots admission before translating and queueing any input kind.
430func translate(ev uintptr, opts Options, p *pointer, in *inputState) (ports.InputEvent, error) {
431 state := securitySnapshot(opts.Security)
432 out, err := translateEvent(ev, opts, p, in, state)
433 return secureInput(out, opts.Security, state), err
434}
435
436func securitySnapshot(security ports.SessionSecurity) ports.SecurityState {
437 if security != nil {
438 return security.Snapshot()
439 }
440 return ports.SecurityState{}
441}
442
443func secureInput(ev ports.InputEvent, security ports.SessionSecurity, state ports.SecurityState) ports.InputEvent {
444 if ev == nil || security == nil {
445 return ev
446 }
447 return ports.SecurityInput{State: state, Event: ev}
448}
449
450func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state ports.SecurityState) (ports.InputEvent, error) {
451 log := opts.Log
452 switch eventType(ev) {
453 case evDeviceAdded:
454 dev := eventDevice(ev)
455 log.Info().Str("device", deviceName(dev)).Msg("input device added")
456 if configurable(dev) && !in.devices[dev] {
457 in.devices[deviceRef(dev)] = true
458 in.configure(dev, log)
459 }
460 case evDeviceRemoved:
461 dev := eventDevice(ev)
462 log.Info().Str("device", deviceName(dev)).Msg("input device removed")
463 if in.devices[dev] {
464 delete(in.devices, dev)
465 deviceUnref(dev)
466 }
467 // Core waits for the end of a swipe the device began.
468 return in.swipes.end(dev, true, 0), nil
469 case evSwipeBegin:
470 ge := gestureEvent(ev)
471 return in.swipes.begin(eventDevice(ev), int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
472 case evSwipeUpdate:
473 ge := gestureEvent(ev)
474 return in.swipes.update(eventDevice(ev), gestureDX(ge), gestureDY(ge), usec(gestureUsec(ge))), nil
475 case evSwipeEnd:
476 ge := gestureEvent(ev)
477 return in.swipes.end(eventDevice(ev), gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
478 case evKeyboardKey:
479 k := keyboardEvent(ev)
480 code, pressed := keyboardKey(k), keyboardState(k) == 1
481 return translateKeyboard(code, pressed, msec(keyboardUsec(k)), opts, state)
482 case evPointerMotion:
483 pe := pointerEvent(ev)
484 m := p.move(pointerDX(pe), pointerDY(pe))
485 m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)
486 m.TimeUsec = pointerUsec(pe)
487 m.TimeMsec = msec(m.TimeUsec)
488 p.moved(opts.MoveCursor, true)
489 if opts.LogMotion {
490 log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
491 }
492 return m, nil
493 case evPointerAbs:
494 // Absolute devices (tablets, VMs) map to the whole layout.
495 pe := pointerEvent(ev)
496 now := msec(pointerUsec(pe))
497 b := p.bounds()
498 if p.constraint.Mode == ports.ConstraintLock {
499 return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil
500 }
501 x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
502 p.moved(opts.MoveCursor, true)
503 return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil
504 case evPointerButton:
505 pe := pointerEvent(ev)
506 b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
507 log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
508 return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: msec(pointerUsec(pe))}, nil
509 case evScrollWheel, evScrollFinger, evScrollCont:
510 pe := pointerEvent(ev)
511 // The three scroll events follow each other as the sources do.
512 source := ports.AxisSource(eventType(ev) - evScrollWheel)
513 a := ports.PointerAxis{Source: source, TimeMsec: msec(pointerUsec(pe))}
514 // libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
515 for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
516 if pointerHasAxis(pe, uint32(i)) == 0 {
517 continue
518 }
519 s.Set, s.Value = true, scrollValue(pe, uint32(i))
520 if source == ports.AxisFinger {
521 // Two-finger scroll is the touchpad's only scroll source.
522 s.Value *= in.touchpad.ScrollFactor
523 }
524 if source == ports.AxisWheel {
525 s.V120 = int32(scrollV120(pe, uint32(i)))
526 } else {
527 s.Stop = s.Value == 0
528 }
529 }
530 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")
531 return a, nil
532 }
533 return nil, nil
534}
535
536// translateKeyboard applies the production epoch before native translation.
537// Quarantined events never reach logging, reserved hotkeys or core.
538func translateKeyboard(code uint32, pressed bool, timeMsec uint32, opts Options, state ports.SecurityState) (ports.InputEvent, error) {
539 var ke ports.KeyEvent
540 if opts.Security != nil {
541 var deliver bool
542 var err error
543 ke, deliver, err = opts.Keymap.KeySecure(code, pressed, timeMsec, state)
544 if err != nil || !deliver {
545 return nil, err
546 }
547 } else {
548 ke = opts.Keymap.Key(code, pressed, timeMsec)
549 }
550 return translateKey(ke, opts, state)
551}
552
553// translateKey applies the keyboard policy without native calls. One coherent
554// snapshot governs both logging and reserved hotkeys; protected keys still go
555// to core for the locker. VT switching is allowed and does not release the lock.
556func translateKey(ke ports.KeyEvent, opts Options, security ports.SecurityState) (ports.InputEvent, error) {
557 if opts.LogKeys && !security.Protected {
558 opts.Log.Debug().Str("component", "input").Uint32("code", ke.Keycode).Str("keysym", ke.Keysym).Bool("pressed", ke.Pressed).Uint8("mods", uint8(ke.Mods)).Msg("key")
559 }
560 switch action, vt := hotkey(ke, security.Protected); action {
561 case hotkeyQuit:
562 opts.Log.Warn().Str("component", "input").Str("reason", "emergency-key").Msg("quit")
563 return nil, ErrEmergencyQuit
564 case hotkeyVT:
565 opts.Seat.SwitchVT(vt)
566 return nil, nil
567 }
568 return ke, nil
569}
570
571type hotkeyAction int
572
573const (
574 hotkeyNone hotkeyAction = iota
575 hotkeyQuit
576 hotkeyVT
577)
578
579// hotkey detects compositor-reserved keys on press: Ctrl+Alt+Backspace and
580// Ctrl+Alt+F1..F12. Protection disables emergency quit, but not VT switching.
581func hotkey(ke ports.KeyEvent, protected bool) (hotkeyAction, int) {
582 if !ke.Pressed {
583 return hotkeyNone, 0
584 }
585 if vt, ok := strings.CutPrefix(ke.Keysym, "XF86Switch_VT_"); ok {
586 var n int
587 if _, err := fmt.Sscan(vt, &n); err == nil && n >= 1 && n <= 12 {
588 return hotkeyVT, n
589 }
590 }
591 if ke.Mods&(ports.ModCtrl|ports.ModAlt) != ports.ModCtrl|ports.ModAlt {
592 return hotkeyNone, 0
593 }
594 switch {
595 case ke.Keysym == "BackSpace" || ke.Keycode == 14:
596 if !protected {
597 return hotkeyQuit, 0
598 }
599 case ke.Keycode >= 59 && ke.Keycode <= 68: // F1..F10
600 return hotkeyVT, int(ke.Keycode) - 58
601 case ke.Keycode == 87 || ke.Keycode == 88: // F11, F12
602 return hotkeyVT, int(ke.Keycode) - 76
603 }
604 return hotkeyNone, 0
605}
606
607// pointer tracks the cursor in global logical coordinates. Relative motion
608// is divided by the scale of the output under the pointer, so it moves the
609// same number of physical pixels on every output.
610type pointer struct {
611 x, y float64
612 layout ports.Layout
613 touched bool // false until the user moves it: it follows the primary output
614 constraint ports.PointerConstraint
615}
616
617// newPointer starts at the centre of the primary output, else the first.
618func newPointer(l ports.Layout) *pointer {
619 p := &pointer{}
620 p.setLayout(l)
621 return p
622}
623
624// setLayout keeps the pointer where it is, or clamps it onto an output.
625// Until it is first moved it stays centred on the primary output, which
626// may be plugged in after the others.
627func (p *pointer) setLayout(l ports.Layout) {
628 p.layout = l
629 if !p.touched && len(l) > 0 {
630 o := l[0]
631 for _, v := range l {
632 if v.Primary {
633 o = v
634 }
635 }
636 p.x, p.y = float64(o.X)+float64(o.Width)/2, float64(o.Y)+float64(o.Height)/2
637 return
638 }
639 p.x, p.y = l.Clamp(p.x, p.y, p.x, p.y)
640}
641
642func (p *pointer) scale() float64 {
643 if o, ok := p.layout.At(p.x, p.y); ok && o.Scale > 0 {
644 return o.Scale
645 }
646 return 1
647}
648
649// move applies a relative motion and returns it with the logical deltas.
650// A locked pointer stays still; a confined one stays in its rectangle.
651func (p *pointer) move(dx, dy float64) ports.PointerMotion {
652 s := p.scale()
653 dx, dy = dx/s, dy/s
654 if p.constraint.Mode != ports.ConstraintLock {
655 p.set(p.x+dx, p.y+dy)
656 }
657 return ports.PointerMotion{X: p.x, Y: p.y, DX: dx, DY: dy}
658}
659
660func (p *pointer) set(x, y float64) (float64, float64) {
661 p.touched = true
662 p.x, p.y = p.constraint.Clamp(p.layout.Clamp(p.x, p.y, x, y))
663 return p.x, p.y
664}
665
666// constrain applies a new constraint. Entering or leaving a lock resyncs
667// the pointer to core's cursor, which stood still meanwhile; a confined
668// pointer moves inside. A warp moves it to core's cursor.
669func (p *pointer) constrain(c ports.PointerConstraint) {
670 if c.Warp || c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
671 p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
672 }
673 p.constraint = c
674 p.x, p.y = c.Clamp(p.x, p.y)
675}
676
677// bounds is the rectangle holding every output.
678func (p *pointer) bounds() ports.Rect {
679 var r ports.Rect
680 for _, o := range p.layout {
681 r.W = max(r.W, o.X+o.Width)
682 r.H = max(r.H, o.Y+o.Height)
683 }
684 return r
685}
686
687// moved reports the physical position on the output under the pointer;
688// motion tells the device moved it.
689func (p *pointer) moved(move func(output string, x, y float64, motion bool), motion bool) {
690 if o, ok := p.layout.At(p.x, p.y); ok && move != nil {
691 move(o.Info.Name, (p.x-float64(o.X))*o.Scale, (p.y-float64(o.Y))*o.Scale, motion)
692 }
693}