v0.8.0 446d1e3

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

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 23

Tags 10

↑↓ moveenter openesc close