main f2d166b

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// control is one control message of Options for Run to apply. keymap is the
241// replacement keymap it carries, if any: a control dropped at shutdown
242// closes it instead.
243type control struct {
244	apply  func()
245	keymap *xkb.Keymap
246}
247
248func (c control) discard() {
249	if c.keymap != nil {
250		c.keymap.Close()
251	}
252}
253
254// relayControls hands the control messages of opts to Run on controls,
255// waking its poll for each, until ctx ends. It only reads the channels of
256// opts; the controls run on Run's goroutine, the only one to touch p, in, li
257// and opts.Keymap.
258func relayControls(ctx context.Context, opts *Options, p *pointer, in *inputState, li uintptr, controls chan<- control, wake func()) {
259	for {
260		var c control
261		var f func()
262		select {
263		case <-ctx.Done():
264			return
265		case on := <-opts.Active:
266			f = func() {
267				if on {
268					resume(li)
269				} else {
270					suspend(li)
271				}
272				opts.Log.Info().Bool("active", on).Msg("input")
273			}
274		case km := <-opts.Keymaps:
275			c.keymap = km
276			f = func() {
277				if opts.Security != nil {
278					km.QuarantineFrom(opts.Keymap)
279				}
280				opts.Keymap.Close()
281				opts.Keymap = km
282				opts.Log.Info().Msg("keymap replaced")
283			}
284		case l := <-opts.Layouts:
285			f = func() {
286				p.setLayout(l)
287				p.moved(opts.MoveCursor, false)
288			}
289		case pc := <-opts.Constraints:
290			f = func() {
291				p.constrain(pc)
292				p.moved(opts.MoveCursor, false)
293			}
294		case dc := <-opts.DeviceConfigs:
295			f = func() {
296				in.cfg = dc
297				for dev := range in.devices {
298					in.configure(dev, opts.Log)
299				}
300				t, m := dc.Touchpad, dc.Mouse
301				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).
302					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")
303			}
304		}
305		c.apply = f
306		select {
307		case controls <- c:
308			wake()
309		case <-ctx.Done():
310			c.discard()
311			return
312		}
313	}
314}
315
316// statsEvery is how often Run logs input throughput.
317const statsEvery = 10 * time.Second
318
319// Run owns libinput and the keymap, converting device events into input events.
320func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error {
321	defer func() { opts.Keymap.Close() }()
322	if err := load(); err != nil {
323		return err
324	}
325	active = opts.Seat
326	udev := udevNew()
327	if udev == 0 {
328		return errors.New("udev_new failed")
329	}
330	defer udevUnref(udev)
331	li := createContext(unsafe.Pointer(&iface), 0, udev)
332	if li == 0 {
333		return errors.New("libinput_udev_create_context failed")
334	}
335	defer unref(li)
336	name := opts.SeatName
337	if name == "" {
338		name = "seat0"
339	}
340	if assignSeat(li, name) != 0 {
341		return fmt.Errorf("libinput_udev_assign_seat %s failed", name)
342	}
343	p := newPointer(opts.Layout)
344	p.moved(opts.MoveCursor, false)
345	in := &inputState{cfg: opts.DeviceConfig, dev: libinputDevices{}, devices: map[uintptr]bool{}}
346	defer in.release()
347	waiter, err := fdwait.New()
348	if err != nil {
349		return fmt.Errorf("libinput waiter: %w", err)
350	}
351	defer waiter.Close()
352	waiter.StopOn(ctx)
353	fd := getFD(li)
354	if err := waiter.Pin(fd); err != nil {
355		return fmt.Errorf("libinput waiter: %w", err)
356	}
357	fwd := newForwarder(opts.Log)
358	fwdCtx, stopFwd := context.WithCancel(ctx)
359	var fwdDone sync.WaitGroup
360	fwdDone.Go(func() { fwd.run(fwdCtx, input) })
361	// Control messages wake the poll instead of a polling timeout; they are
362	// applied on this goroutine, which owns libinput.
363	controls := make(chan control, 8)
364	fwdDone.Go(func() { relayControls(fwdCtx, &opts, p, in, li, controls, waiter.Wake) })
365	defer func() {
366		stopFwd()
367		fwdDone.Wait()
368		// Controls never applied: free what they carry.
369		for len(controls) > 0 {
370			(<-controls).discard()
371		}
372	}()
373	fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}}
374	nextStats := time.Now().Add(statsEvery)
375	for ctx.Err() == nil {
376		if !time.Now().Before(nextStats) {
377			nextStats = time.Now().Add(statsEvery)
378			if s := fwd.take(); s.Motions > 0 || s.Sent > 0 {
379				opts.Log.Info().Int("motions", s.Motions).Int("coalesced", s.Coalesced).Int("sent", s.Sent).Dur("max_block_ms", s.MaxBlock).Msg("input stats")
380			}
381		}
382	apply:
383		for {
384			select {
385			case c := <-controls:
386				c.apply()
387			default:
388				break apply
389			}
390		}
391		n, err := waiter.Poll(fds, max(0, int(time.Until(nextStats).Milliseconds())+1))
392		if err != nil && !errors.Is(err, unix.EINTR) {
393			return fmt.Errorf("libinput poll: %w", err)
394		}
395		if n <= 0 {
396			continue
397		}
398		if dispatch(li) != 0 {
399			opts.Log.Warn().Msg("libinput_dispatch failed")
400		}
401		for ev := getEvent(li); ev != 0; ev = getEvent(li) {
402			out, err := translate(ev, opts, p, in)
403			eventDestroy(ev)
404			if err != nil {
405				return err
406			}
407			if out != nil {
408				fwd.push(out)
409			}
410		}
411	}
412	return nil
413}
414
415// inputState is the device state Run owns: the device config, the devices
416// it configures (referenced until removed) and the gesture in progress on
417// the touchpads. dev applies the config to libinput; it is a zero-size
418// type, so storing it in the interface allocates nothing.
419type inputState struct {
420	cfg      ports.InputDevicesConfig
421	dev      deviceConfig
422	devices  map[uintptr]bool
423	gestures gestures
424}
425
426// deviceConfig is libinput's device configuration side: what a device is
427// and the settings it takes. libinputDevices is the real one; tests use its
428// Mockery mock. It serves device-added and config reload only, never the
429// event path. A setting the device lacks is skipped without error; one it
430// refuses returns a sentinel error.
431type deviceConfig interface {
432	// IsTouchpad reports the gesture capability libinput gives touchpads.
433	IsTouchpad(dev uintptr) bool
434	// IsPointer reports the pointer capability: mice, trackballs,
435	// trackpoints and touchpads.
436	IsPointer(dev uintptr) bool
437	// Name is the device name, for logs.
438	Name(dev uintptr) string
439	SetNaturalScroll(dev uintptr, on bool) error
440	// SetAccel sets the pointer speed and, when the device offers it, the
441	// profile (ports.AccelFlat or ports.AccelAdaptive).
442	SetAccel(dev uintptr, speed float64, profile string) error
443	SetLeftHanded(dev uintptr, on bool) error
444	// SetTap enables tap to click, with one finger left, two right, three
445	// middle. Devices that cannot tap skip it.
446	SetTap(dev uintptr, on bool) error
447}
448
449// errRejected is a setting the device refuses (libinput status not success).
450var errRejected = errors.New("rejected by libinput")
451
452// libinputDevices implements deviceConfig over the libinput functions.
453type libinputDevices struct{}
454
455const (
456	// capPointer is LIBINPUT_DEVICE_CAP_POINTER.
457	capPointer = 1
458	// capGesture is LIBINPUT_DEVICE_CAP_GESTURE: libinput gives it to
459	// touchpads, with or without tapping.
460	capGesture = 5
461	// tapButtonMap is LIBINPUT_CONFIG_TAP_MAP_LRM.
462	tapButtonMap = 0
463)
464
465func (libinputDevices) IsTouchpad(dev uintptr) bool { return hasCapability(dev, capGesture) != 0 }
466func (libinputDevices) IsPointer(dev uintptr) bool  { return hasCapability(dev, capPointer) != 0 }
467func (libinputDevices) Name(dev uintptr) string     { return deviceName(dev) }
468
469func (libinputDevices) SetNaturalScroll(dev uintptr, on bool) error {
470	if hasNatural(dev) != 0 && setNatural(dev, flag(on)) != 0 {
471		return errRejected
472	}
473	return nil
474}
475
476func (libinputDevices) SetAccel(dev uintptr, speed float64, profile string) error {
477	if accelAvailable(dev) == 0 {
478		return nil
479	}
480	var err error
481	if setAccelSpeed(dev, speed) != 0 {
482		err = errRejected
483	}
484	if p := accelProfile(profile); accelProfiles(dev)&p != 0 && setAccelProfile(dev, p) != 0 {
485		err = errRejected
486	}
487	return err
488}
489
490func (libinputDevices) SetLeftHanded(dev uintptr, on bool) error {
491	if leftHandedAvailable(dev) != 0 && setLeftHanded(dev, flag(on)) != 0 {
492		return errRejected
493	}
494	return nil
495}
496
497func (libinputDevices) SetTap(dev uintptr, on bool) error {
498	if tapFingers(dev) <= 0 {
499		return nil
500	}
501	var err error
502	if setTap(dev, flag(on)) != 0 {
503		err = errRejected
504	}
505	if setTapMap(dev, tapButtonMap) != 0 {
506		err = errRejected
507	}
508	return err
509}
510
511// configurable reports whether the device config applies to dev: a
512// touchpad or another pointer. Keyboards, switches and the like are left
513// alone and not referenced.
514func (s *inputState) configurable(dev uintptr) bool {
515	return s.dev.IsTouchpad(dev) || s.dev.IsPointer(dev)
516}
517
518// configure applies the config to dev by type: touchpads take the touchpad
519// settings and tapping, other pointers (mice, trackballs, trackpoints) the
520// mouse settings, other devices nothing. A refused setting is logged and
521// the rest still apply. The success path logs and allocates nothing.
522func (s *inputState) configure(dev uintptr, log zerowrap.Logger) {
523	switch {
524	case s.dev.IsTouchpad(dev):
525		s.applyPointer(dev, s.cfg.Touchpad.PointerConfig, log)
526		s.check(dev, "tap to click", s.dev.SetTap(dev, s.cfg.Touchpad.Tap), log)
527	case s.dev.IsPointer(dev):
528		s.applyPointer(dev, s.cfg.Mouse, log)
529	}
530}
531
532// applyPointer applies the settings touchpads and mice share.
533func (s *inputState) applyPointer(dev uintptr, c ports.PointerConfig, log zerowrap.Logger) {
534	s.check(dev, "natural scroll", s.dev.SetNaturalScroll(dev, c.NaturalScroll), log)
535	s.check(dev, "accel", s.dev.SetAccel(dev, c.AccelSpeed, c.AccelProfile), log)
536	s.check(dev, "left handed", s.dev.SetLeftHanded(dev, c.LeftHanded), log)
537}
538
539// check logs a refused setting; the device name is only read then.
540func (s *inputState) check(dev uintptr, setting string, err error, log zerowrap.Logger) {
541	if err != nil {
542		log.Warn().Err(err).Str("device", s.dev.Name(dev)).Str("setting", setting).Msg("input device setting rejected")
543	}
544}
545
546// accelProfile is the libinput accel profile bit: LIBINPUT_CONFIG_ACCEL_
547// PROFILE_FLAT (1) or _ADAPTIVE (2).
548func accelProfile(name string) uint32 {
549	if name == ports.AccelFlat {
550		return 1
551	}
552	return 2
553}
554
555func flag(on bool) int32 {
556	if on {
557		return 1
558	}
559	return 0
560}
561
562func (s *inputState) release() {
563	for dev := range s.devices {
564		deviceUnref(dev)
565	}
566	clear(s.devices)
567}
568
569// gestures streams one touchpad gesture at a time to core: a swipe of any
570// finger count, a pinch or a hold. Core follows the fingers of a three- or
571// four-finger swipe and hands the other gestures to the window under the
572// pointer. owner is the touchpad of the gesture in progress (0: none) and
573// kind its kind; another touchpad's gesture, or another kind's event, is
574// ignored until it ends, so core never sees two streams interleaved.
575type gestures struct {
576	owner uintptr
577	kind  ports.GestureKind
578}
579
580func (g *gestures) begin(dev uintptr, kind ports.GestureKind, fingers int, at time.Duration) ports.InputEvent {
581	if dev == 0 || fingers <= 0 || g.owner != 0 {
582		return nil
583	}
584	g.owner, g.kind = dev, kind
585	switch kind {
586	case ports.GesturePinch:
587		return ports.PinchBegin{Fingers: fingers, Time: at}
588	case ports.GestureHold:
589		return ports.HoldBegin{Fingers: fingers, Time: at}
590	}
591	return ports.SwipeBegin{Fingers: fingers, Time: at}
592}
593
594// running reports whether dev has a gesture of the given kind in progress.
595func (g *gestures) running(dev uintptr, kind ports.GestureKind) bool {
596	return dev != 0 && g.owner == dev && g.kind == kind
597}
598
599func (g *gestures) swipeUpdate(dev uintptr, dx, dy, accelDX, accelDY float64, at time.Duration) ports.InputEvent {
600	if !g.running(dev, ports.GestureSwipe) {
601		return nil
602	}
603	return ports.SwipeUpdate{DX: dx, DY: dy, AccelDX: accelDX, AccelDY: accelDY, Time: at}
604}
605
606func (g *gestures) pinchUpdate(dev uintptr, dx, dy, scale, rotation float64, at time.Duration) ports.InputEvent {
607	if !g.running(dev, ports.GesturePinch) {
608		return nil
609	}
610	return ports.PinchUpdate{DX: dx, DY: dy, Scale: scale, Rotation: rotation, Time: at}
611}
612
613// end finishes the gesture of the given kind on dev.
614func (g *gestures) end(dev uintptr, kind ports.GestureKind, cancelled bool, at time.Duration) ports.InputEvent {
615	if !g.running(dev, kind) {
616		return nil
617	}
618	return g.finish(cancelled, at)
619}
620
621// abort cancels whatever gesture dev has in progress (the device went).
622func (g *gestures) abort(dev uintptr) ports.InputEvent {
623	if dev == 0 || g.owner != dev {
624		return nil
625	}
626	return g.finish(true, 0)
627}
628
629func (g *gestures) finish(cancelled bool, at time.Duration) ports.InputEvent {
630	g.owner = 0
631	switch g.kind {
632	case ports.GesturePinch:
633		return ports.PinchEnd{Cancelled: cancelled, Time: at}
634	case ports.GestureHold:
635		return ports.HoldEnd{Cancelled: cancelled, Time: at}
636	}
637	return ports.SwipeEnd{Cancelled: cancelled, Time: at}
638}
639
640// usec is a libinput timestamp as a duration.
641func usec(v uint64) time.Duration { return time.Duration(v) * time.Microsecond }
642
643// translate converts one libinput event. Timestamps are the device's
644// (CLOCK_MONOTONIC), so clients measure speeds (kinetic scrolling) on
645// when events happened, not when they were read.
646// It snapshots admission before translating and queueing any input kind.
647func translate(ev uintptr, opts Options, p *pointer, in *inputState) (ports.InputEvent, error) {
648	state := securitySnapshot(opts.Security)
649	out, err := translateEvent(ev, opts, p, in, state)
650	return secureInput(out, opts.Security, state), err
651}
652
653func securitySnapshot(security ports.SessionSecurity) ports.SecurityState {
654	if security != nil {
655		return security.Snapshot()
656	}
657	return ports.SecurityState{}
658}
659
660func secureInput(ev ports.InputEvent, security ports.SessionSecurity, state ports.SecurityState) ports.InputEvent {
661	if ev == nil || security == nil {
662		return ev
663	}
664	return ports.SecurityInput{State: state, Event: ev}
665}
666
667func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state ports.SecurityState) (ports.InputEvent, error) {
668	log := opts.Log
669	switch eventType(ev) {
670	case evDeviceAdded:
671		dev := eventDevice(ev)
672		log.Info().Str("device", deviceName(dev)).Msg("input device added")
673		if in.configurable(dev) && !in.devices[dev] {
674			in.devices[deviceRef(dev)] = true
675			in.configure(dev, log)
676		}
677	case evDeviceRemoved:
678		dev := eventDevice(ev)
679		log.Info().Str("device", deviceName(dev)).Msg("input device removed")
680		if in.devices[dev] {
681			delete(in.devices, dev)
682			deviceUnref(dev)
683		}
684		// Core waits for the end of a gesture the device began.
685		return in.gestures.abort(dev), nil
686	case evSwipeBegin, evPinchBegin, evHoldBegin:
687		ge := gestureEvent(ev)
688		kind := ports.GestureSwipe
689		if eventType(ev) == evPinchBegin {
690			kind = ports.GesturePinch
691		} else if eventType(ev) == evHoldBegin {
692			kind = ports.GestureHold
693		}
694		return in.gestures.begin(eventDevice(ev), kind, int(gestureFingers(ge)), usec(gestureUsec(ge))), nil
695	case evSwipeUpdate:
696		ge := gestureEvent(ev)
697		return in.gestures.swipeUpdate(eventDevice(ev), gestureDX(ge), gestureDY(ge), gestureAccelDX(ge), gestureAccelDY(ge), usec(gestureUsec(ge))), nil
698	case evPinchUpdate:
699		ge := gestureEvent(ev)
700		return in.gestures.pinchUpdate(eventDevice(ev), gestureAccelDX(ge), gestureAccelDY(ge), gestureScale(ge), gestureAngle(ge), usec(gestureUsec(ge))), nil
701	case evSwipeEnd:
702		ge := gestureEvent(ev)
703		return in.gestures.end(eventDevice(ev), ports.GestureSwipe, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
704	case evPinchEnd:
705		ge := gestureEvent(ev)
706		return in.gestures.end(eventDevice(ev), ports.GesturePinch, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
707	case evHoldEnd:
708		ge := gestureEvent(ev)
709		return in.gestures.end(eventDevice(ev), ports.GestureHold, gestureCanceled(ge) != 0, usec(gestureUsec(ge))), nil
710	case evKeyboardKey:
711		k := keyboardEvent(ev)
712		code, pressed := keyboardKey(k), keyboardState(k) == 1
713		return translateKeyboard(code, pressed, usec(keyboardUsec(k)), opts, state)
714	case evPointerMotion:
715		pe := pointerEvent(ev)
716		m := p.move(pointerDX(pe), pointerDY(pe))
717		m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)
718		m.Time = usec(pointerUsec(pe))
719		p.moved(opts.MoveCursor, true)
720		if opts.LogMotion {
721			log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
722		}
723		return m, nil
724	case evPointerAbs:
725		// Absolute devices (tablets, VMs) map to the whole layout.
726		pe := pointerEvent(ev)
727		now := usec(pointerUsec(pe))
728		b := p.bounds()
729		if p.constraint.Mode == ports.ConstraintLock {
730			return ports.PointerMotion{X: p.x, Y: p.y, Time: now}, nil
731		}
732		x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
733		p.moved(opts.MoveCursor, true)
734		return ports.PointerMotion{X: x, Y: y, Time: now}, nil
735	case evPointerButton:
736		pe := pointerEvent(ev)
737		b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
738		log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
739		return ports.PointerButton{Button: b, Pressed: pressed, Time: usec(pointerUsec(pe))}, nil
740	case evScrollWheel, evScrollFinger, evScrollCont:
741		pe := pointerEvent(ev)
742		// The three scroll events follow each other as the sources do.
743		source := ports.AxisSource(eventType(ev) - evScrollWheel)
744		a := ports.PointerAxis{Source: source, Time: usec(pointerUsec(pe))}
745		// libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
746		for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
747			if pointerHasAxis(pe, uint32(i)) == 0 {
748				continue
749			}
750			s.Set, s.Value = true, scrollValue(pe, uint32(i))
751			if source == ports.AxisFinger {
752				// Two-finger scroll is the touchpad's only scroll source.
753				s.Value *= in.cfg.Touchpad.ScrollFactor
754			}
755			if source == ports.AxisWheel {
756				s.V120 = int32(scrollV120(pe, uint32(i)))
757			} else {
758				s.Stop = s.Value == 0
759			}
760		}
761		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")
762		return a, nil
763	}
764	return nil, nil
765}
766
767// translateKeyboard applies the production epoch before native translation.
768// Quarantined events never reach logging, reserved hotkeys or core.
769func translateKeyboard(code uint32, pressed bool, t time.Duration, opts Options, state ports.SecurityState) (ports.InputEvent, error) {
770	var ke ports.KeyEvent
771	if opts.Security != nil {
772		var deliver bool
773		var err error
774		ke, deliver, err = opts.Keymap.KeySecure(code, pressed, t, state)
775		if err != nil || !deliver {
776			return nil, err
777		}
778	} else {
779		ke = opts.Keymap.Key(code, pressed, t)
780	}
781	return translateKey(ke, opts, state)
782}
783
784// translateKey applies the keyboard policy without native calls. One coherent
785// snapshot governs both logging and reserved hotkeys; protected keys still go
786// to core for the locker. VT switching is allowed and does not release the lock.
787func translateKey(ke ports.KeyEvent, opts Options, security ports.SecurityState) (ports.InputEvent, error) {
788	if opts.LogKeys && !security.Protected {
789		opts.Log.Debug().Uint32("code", ke.Keycode).Str("keysym", ke.Keysym).Bool("pressed", ke.Pressed).Uint8("mods", uint8(ke.Mods)).Msg("key")
790	}
791	switch action, vt := hotkey(ke, security.Protected); action {
792	case hotkeyQuit:
793		opts.Log.Warn().Str("reason", "emergency-key").Msg("quit")
794		return nil, ErrEmergencyQuit
795	case hotkeyVT:
796		opts.Seat.SwitchVT(vt)
797		return nil, nil
798	}
799	return ke, nil
800}
801
802type hotkeyAction int
803
804const (
805	hotkeyNone hotkeyAction = iota
806	hotkeyQuit
807	hotkeyVT
808)
809
810// hotkey detects compositor-reserved keys on press: Ctrl+Alt+Backspace and
811// Ctrl+Alt+F1..F12. Protection disables emergency quit, but not VT switching.
812func hotkey(ke ports.KeyEvent, protected bool) (hotkeyAction, int) {
813	if !ke.Pressed {
814		return hotkeyNone, 0
815	}
816	if vt, ok := strings.CutPrefix(ke.Keysym, "XF86Switch_VT_"); ok {
817		var n int
818		if _, err := fmt.Sscan(vt, &n); err == nil && n >= 1 && n <= 12 {
819			return hotkeyVT, n
820		}
821	}
822	if ke.Mods&(ports.ModCtrl|ports.ModAlt) != ports.ModCtrl|ports.ModAlt {
823		return hotkeyNone, 0
824	}
825	switch {
826	case ke.Keysym == "BackSpace" || ke.Keycode == 14:
827		if !protected {
828			return hotkeyQuit, 0
829		}
830	case ke.Keycode >= 59 && ke.Keycode <= 68: // F1..F10
831		return hotkeyVT, int(ke.Keycode) - 58
832	case ke.Keycode == 87 || ke.Keycode == 88: // F11, F12
833		return hotkeyVT, int(ke.Keycode) - 76
834	}
835	return hotkeyNone, 0
836}
837
838// pointer tracks the cursor in global logical coordinates. Relative motion
839// is divided by the scale of the output under the pointer, so it moves the
840// same number of physical pixels on every output.
841type pointer struct {
842	x, y       float64
843	layout     ports.Layout
844	touched    bool // false until the user moves it: it follows the primary output
845	constraint ports.PointerConstraint
846}
847
848// newPointer starts at the centre of the primary output, else the first.
849func newPointer(l ports.Layout) *pointer {
850	p := &pointer{}
851	p.setLayout(l)
852	return p
853}
854
855// setLayout keeps the pointer where it is, or clamps it onto an output.
856// Until it is first moved it stays centred on the primary output, which
857// may be plugged in after the others.
858func (p *pointer) setLayout(l ports.Layout) {
859	p.layout = l
860	if !p.touched && len(l) > 0 {
861		o := l[0]
862		for _, v := range l {
863			if v.Primary {
864				o = v
865			}
866		}
867		p.x, p.y = float64(o.X)+float64(o.Width)/2, float64(o.Y)+float64(o.Height)/2
868		return
869	}
870	p.x, p.y = l.Clamp(p.x, p.y, p.x, p.y)
871}
872
873func (p *pointer) scale() float64 {
874	if o, ok := p.layout.At(p.x, p.y); ok && o.Scale > 0 {
875		return o.Scale
876	}
877	return 1
878}
879
880// move applies a relative motion and returns it with the logical deltas.
881// A locked pointer stays still; a confined one stays in its rectangle.
882func (p *pointer) move(dx, dy float64) ports.PointerMotion {
883	s := p.scale()
884	dx, dy = dx/s, dy/s
885	if p.constraint.Mode != ports.ConstraintLock {
886		p.set(p.x+dx, p.y+dy)
887	}
888	return ports.PointerMotion{X: p.x, Y: p.y, DX: dx, DY: dy}
889}
890
891func (p *pointer) set(x, y float64) (float64, float64) {
892	p.touched = true
893	p.x, p.y = p.constraint.Clamp(p.layout.Clamp(p.x, p.y, x, y))
894	return p.x, p.y
895}
896
897// constrain applies a new constraint. Entering or leaving a lock resyncs
898// the pointer to core's cursor, which stood still meanwhile; a confined
899// pointer moves inside. A warp moves it to core's cursor.
900func (p *pointer) constrain(c ports.PointerConstraint) {
901	if c.Warp || c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
902		p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
903	}
904	p.constraint = c
905	p.x, p.y = c.Clamp(p.x, p.y)
906}
907
908// bounds is the rectangle holding every output.
909func (p *pointer) bounds() ports.Rect {
910	var r ports.Rect
911	for _, o := range p.layout {
912		r.W = max(r.W, o.X+o.Width)
913		r.H = max(r.H, o.Y+o.Height)
914	}
915	return r
916}
917
918// moved reports the target position (physical pixels after the output
919// transform) on the output under the pointer;
920// motion tells the device moved it.
921func (p *pointer) moved(move func(output string, x, y float64, motion bool), motion bool) {
922	if o, ok := p.layout.At(p.x, p.y); ok && move != nil {
923		tx, ty := o.ToTarget(p.x, p.y)
924		move(o.Info.Name, tx, ty, motion)
925	}
926}

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