v0.4.0 b6be3a8

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

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