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

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