libinput.go RawHistoryFolder

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

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 26

Tags 10

↑↓ moveenter openesc close