v0.1.0 d9218d6

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)
34
35var (
36	loadOnce sync.Once
37	loadErr  error
38
39	udevNew         func() uintptr
40	udevUnref       func(uintptr) uintptr
41	createContext   func(iface unsafe.Pointer, data uintptr, udev uintptr) uintptr
42	assignSeat      func(li uintptr, seat string) int32
43	unref           func(li uintptr) uintptr
44	getFD           func(li uintptr) int32
45	dispatch        func(li uintptr) int32
46	getEvent        func(li uintptr) uintptr
47	eventType       func(ev uintptr) int32
48	eventDestroy    func(ev uintptr)
49	eventDevice     func(ev uintptr) uintptr
50	deviceName      func(dev uintptr) string
51	suspend         func(li uintptr)
52	resume          func(li uintptr) int32
53	keyboardEvent   func(ev uintptr) uintptr
54	keyboardKey     func(kev uintptr) uint32
55	keyboardState   func(kev uintptr) int32
56	pointerEvent    func(ev uintptr) uintptr
57	pointerDX       func(pev uintptr) float64
58	pointerDY       func(pev uintptr) float64
59	pointerRawDX    func(pev uintptr) float64
60	pointerRawDY    func(pev uintptr) float64
61	pointerUsec     func(pev uintptr) uint64
62	pointerAbsX     func(pev uintptr, width uint32) float64
63	pointerAbsY     func(pev uintptr, height uint32) float64
64	pointerButton   func(pev uintptr) uint32
65	pointerBtnState func(pev uintptr) int32
66	pointerHasAxis  func(pev uintptr, axis uint32) int32
67	scrollValue     func(pev uintptr, axis uint32) float64
68	scrollV120      func(pev uintptr, axis uint32) float64
69	iface           [2]uintptr
70	active          ports.Seat
71)
72
73func load() error {
74	loadOnce.Do(func() {
75		udev, err := purego.Dlopen("libudev.so.1", purego.RTLD_NOW|purego.RTLD_LOCAL)
76		if err != nil {
77			loadErr = fmt.Errorf("open libudev.so.1: %w", err)
78			return
79		}
80		lib, err := purego.Dlopen("libinput.so.10", purego.RTLD_NOW|purego.RTLD_LOCAL)
81		if err != nil {
82			loadErr = fmt.Errorf("open libinput.so.10: %w", err)
83			return
84		}
85		defer func() {
86			if r := recover(); r != nil {
87				loadErr = fmt.Errorf("libinput: %v", r)
88			}
89		}()
90		purego.RegisterLibFunc(&udevNew, udev, "udev_new")
91		purego.RegisterLibFunc(&udevUnref, udev, "udev_unref")
92		reg := func(fn any, name string) { purego.RegisterLibFunc(fn, lib, "libinput_"+name) }
93		reg(&createContext, "udev_create_context")
94		reg(&assignSeat, "udev_assign_seat")
95		reg(&unref, "unref")
96		reg(&getFD, "get_fd")
97		reg(&dispatch, "dispatch")
98		reg(&getEvent, "get_event")
99		reg(&eventType, "event_get_type")
100		reg(&eventDestroy, "event_destroy")
101		reg(&eventDevice, "event_get_device")
102		reg(&deviceName, "device_get_name")
103		reg(&suspend, "suspend")
104		reg(&resume, "resume")
105		reg(&keyboardEvent, "event_get_keyboard_event")
106		reg(&keyboardKey, "event_keyboard_get_key")
107		reg(&keyboardState, "event_keyboard_get_key_state")
108		reg(&pointerEvent, "event_get_pointer_event")
109		reg(&pointerDX, "event_pointer_get_dx")
110		reg(&pointerDY, "event_pointer_get_dy")
111		reg(&pointerRawDX, "event_pointer_get_dx_unaccelerated")
112		reg(&pointerRawDY, "event_pointer_get_dy_unaccelerated")
113		reg(&pointerUsec, "event_pointer_get_time_usec")
114		reg(&pointerAbsX, "event_pointer_get_absolute_x_transformed")
115		reg(&pointerAbsY, "event_pointer_get_absolute_y_transformed")
116		reg(&pointerButton, "event_pointer_get_button")
117		reg(&pointerBtnState, "event_pointer_get_button_state")
118		reg(&pointerHasAxis, "event_pointer_has_axis")
119		reg(&scrollValue, "event_pointer_get_scroll_value")
120		reg(&scrollV120, "event_pointer_get_scroll_value_v120")
121		iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {
122			fd, err := active.OpenDevice(goString(path))
123			if err != nil {
124				return uintptr(uint32(^uint32(unix.EACCES) + 1)) // -EACCES as C int
125			}
126			return uintptr(fd)
127		})
128		iface[1] = purego.NewCallback(func(fd, _ uintptr) { active.CloseDevice(int(int32(fd))) })
129	})
130	return loadErr
131}
132
133func goString(p *byte) string {
134	if p == nil {
135		return ""
136	}
137	n := 0
138	for *(*byte)(unsafe.Add(unsafe.Pointer(p), n)) != 0 {
139		n++
140	}
141	return string(unsafe.Slice(p, n))
142}
143
144// Options configures the input adapter.
145type Options struct {
146	Seat     ports.Seat
147	SeatName string
148	Keymap   *xkb.Keymap
149	// Keymaps replaces Keymap live; Run takes ownership and closes the old one.
150	Keymaps <-chan *xkb.Keymap
151	// Layout is the initial output layout; Layouts replaces it live.
152	Layout  ports.Layout
153	Layouts <-chan ports.Layout
154	// Constraints holds the pointer lock or confinement of the focused
155	// window, in global logical coordinates.
156	Constraints <-chan ports.PointerConstraint
157	Active      <-chan bool
158	// MoveCursor, when set, places the hardware cursor as soon as motion is
159	// read, before core sees the event: the output under the pointer and
160	// the physical position on it.
161	MoveCursor func(output string, x, y float64)
162	Log        zerowrap.Logger
163	// LogMotion logs every pointer motion (--debug=input-motion); motions
164	// are otherwise only counted in the input stats.
165	LogMotion bool
166}
167
168// statsEvery is how often Run logs input throughput.
169const statsEvery = 10 * time.Second
170
171// Run owns libinput and the keymap, converting device events into input events.
172func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error {
173	defer func() { opts.Keymap.Close() }()
174	if err := load(); err != nil {
175		return err
176	}
177	active = opts.Seat
178	udev := udevNew()
179	if udev == 0 {
180		return errors.New("udev_new failed")
181	}
182	defer udevUnref(udev)
183	li := createContext(unsafe.Pointer(&iface), 0, udev)
184	if li == 0 {
185		return errors.New("libinput_udev_create_context failed")
186	}
187	defer unref(li)
188	name := opts.SeatName
189	if name == "" {
190		name = "seat0"
191	}
192	if assignSeat(li, name) != 0 {
193		return fmt.Errorf("libinput_udev_assign_seat %s failed", name)
194	}
195	p := newPointer(opts.Layout)
196	p.moved(opts.MoveCursor)
197	fd := getFD(li)
198	fwd := newForwarder(opts.Log)
199	fwdCtx, stopFwd := context.WithCancel(ctx)
200	var fwdDone sync.WaitGroup
201	fwdDone.Go(func() { fwd.run(fwdCtx, input) })
202	defer func() { stopFwd(); fwdDone.Wait() }()
203	lastStats := time.Now()
204	for ctx.Err() == nil {
205		if time.Since(lastStats) >= statsEvery {
206			lastStats = time.Now()
207			if s := fwd.take(); s.Motions > 0 || s.Sent > 0 {
208				opts.Log.Info().Int("motions", s.Motions).Int("coalesced", s.Coalesced).Int("sent", s.Sent).Dur("max_block_ms", s.MaxBlock).Msg("input stats")
209			}
210		}
211		select {
212		case on := <-opts.Active:
213			if on {
214				resume(li)
215			} else {
216				suspend(li)
217			}
218			opts.Log.Info().Bool("active", on).Msg("input")
219		case km := <-opts.Keymaps:
220			opts.Keymap.Close()
221			opts.Keymap = km
222			opts.Log.Info().Msg("keymap replaced")
223		case l := <-opts.Layouts:
224			p.setLayout(l)
225			p.moved(opts.MoveCursor)
226		case c := <-opts.Constraints:
227			p.constrain(c)
228			p.moved(opts.MoveCursor)
229		default:
230		}
231		fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}}
232		n, err := unix.Poll(fds, 100)
233		if err != nil && !errors.Is(err, unix.EINTR) {
234			return fmt.Errorf("libinput poll: %w", err)
235		}
236		if n <= 0 {
237			continue
238		}
239		if dispatch(li) != 0 {
240			opts.Log.Warn().Msg("libinput_dispatch failed")
241		}
242		for ev := getEvent(li); ev != 0; ev = getEvent(li) {
243			out, err := translate(ev, opts, p)
244			eventDestroy(ev)
245			if err != nil {
246				return err
247			}
248			if out != nil {
249				fwd.push(out)
250			}
251		}
252	}
253	return nil
254}
255
256func translate(ev uintptr, opts Options, p *pointer) (ports.InputEvent, error) {
257	now := uint32(time.Now().UnixMilli())
258	log := opts.Log
259	switch eventType(ev) {
260	case evDeviceAdded:
261		log.Info().Str("device", deviceName(eventDevice(ev))).Msg("input device added")
262	case evDeviceRemoved:
263		log.Info().Str("device", deviceName(eventDevice(ev))).Msg("input device removed")
264	case evKeyboardKey:
265		k := keyboardEvent(ev)
266		code, pressed := keyboardKey(k), keyboardState(k) == 1
267		ke := opts.Keymap.Key(code, pressed, now)
268		log.Debug().Uint32("code", code).Str("keysym", ke.Keysym).Bool("pressed", pressed).Uint8("mods", uint8(ke.Mods)).Msg("key")
269		switch action, vt := hotkey(ke); action {
270		case hotkeyQuit:
271			log.Warn().Str("reason", "emergency-key").Msg("quit")
272			return nil, ErrEmergencyQuit
273		case hotkeyVT:
274			opts.Seat.SwitchVT(vt)
275			return nil, nil
276		}
277		return ke, nil
278	case evPointerMotion:
279		pe := pointerEvent(ev)
280		m := p.move(pointerDX(pe), pointerDY(pe))
281		m.UnaccelDX, m.UnaccelDY = pointerRawDX(pe), pointerRawDY(pe)
282		m.TimeMsec, m.TimeUsec = now, pointerUsec(pe)
283		p.moved(opts.MoveCursor)
284		if opts.LogMotion {
285			log.Debug().Float64("x", m.X).Float64("y", m.Y).Msg("pointer")
286		}
287		return m, nil
288	case evPointerAbs:
289		// Absolute devices (tablets, VMs) map to the whole layout.
290		pe := pointerEvent(ev)
291		b := p.bounds()
292		if p.constraint.Mode == ports.ConstraintLock {
293			return ports.PointerMotion{X: p.x, Y: p.y, TimeMsec: now}, nil
294		}
295		x, y := p.set(float64(b.X)+pointerAbsX(pe, uint32(b.W)), float64(b.Y)+pointerAbsY(pe, uint32(b.H)))
296		p.moved(opts.MoveCursor)
297		return ports.PointerMotion{X: x, Y: y, TimeMsec: now}, nil
298	case evPointerButton:
299		pe := pointerEvent(ev)
300		b, pressed := pointerButton(pe), pointerBtnState(pe) == 1
301		log.Debug().Uint32("button", b).Bool("pressed", pressed).Msg("button")
302		return ports.PointerButton{Button: b, Pressed: pressed, TimeMsec: now}, nil
303	case evScrollWheel, evScrollFinger, evScrollCont:
304		pe := pointerEvent(ev)
305		// The three scroll events follow each other as the sources do.
306		source := ports.AxisSource(eventType(ev) - evScrollWheel)
307		a := ports.PointerAxis{Source: source, TimeMsec: now}
308		// libinput axis 0 is vertical, 1 horizontal, as in wl_pointer.
309		for i, s := range []*ports.ScrollAxis{&a.Vertical, &a.Horizontal} {
310			if pointerHasAxis(pe, uint32(i)) == 0 {
311				continue
312			}
313			s.Set, s.Value = true, scrollValue(pe, uint32(i))
314			if source == ports.AxisWheel {
315				s.V120 = int32(scrollV120(pe, uint32(i)))
316			} else {
317				s.Stop = s.Value == 0
318			}
319		}
320		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")
321		return a, nil
322	}
323	return nil, nil
324}
325
326type hotkeyAction int
327
328const (
329	hotkeyNone hotkeyAction = iota
330	hotkeyQuit
331	hotkeyVT
332)
333
334// hotkey detects compositor-reserved keys on press: Ctrl+Alt+Backspace and Ctrl+Alt+F1..F12.
335func hotkey(ke ports.KeyEvent) (hotkeyAction, int) {
336	if !ke.Pressed {
337		return hotkeyNone, 0
338	}
339	if vt, ok := strings.CutPrefix(ke.Keysym, "XF86Switch_VT_"); ok {
340		var n int
341		if _, err := fmt.Sscan(vt, &n); err == nil && n >= 1 && n <= 12 {
342			return hotkeyVT, n
343		}
344	}
345	if ke.Mods&(ports.ModCtrl|ports.ModAlt) != ports.ModCtrl|ports.ModAlt {
346		return hotkeyNone, 0
347	}
348	switch {
349	case ke.Keysym == "BackSpace" || ke.Keycode == 14:
350		return hotkeyQuit, 0
351	case ke.Keycode >= 59 && ke.Keycode <= 68: // F1..F10
352		return hotkeyVT, int(ke.Keycode) - 58
353	case ke.Keycode == 87 || ke.Keycode == 88: // F11, F12
354		return hotkeyVT, int(ke.Keycode) - 76
355	}
356	return hotkeyNone, 0
357}
358
359// pointer tracks the cursor in global logical coordinates. Relative motion
360// is divided by the scale of the output under the pointer, so it moves the
361// same number of physical pixels on every output.
362type pointer struct {
363	x, y       float64
364	layout     ports.Layout
365	touched    bool // false until the user moves it: it follows the primary output
366	constraint ports.PointerConstraint
367}
368
369// newPointer starts at the centre of the primary output, else the first.
370func newPointer(l ports.Layout) *pointer {
371	p := &pointer{}
372	p.setLayout(l)
373	return p
374}
375
376// setLayout keeps the pointer where it is, or clamps it onto an output.
377// Until it is first moved it stays centred on the primary output, which
378// may be plugged in after the others.
379func (p *pointer) setLayout(l ports.Layout) {
380	p.layout = l
381	if !p.touched && len(l) > 0 {
382		o := l[0]
383		for _, v := range l {
384			if v.Primary {
385				o = v
386			}
387		}
388		p.x, p.y = float64(o.X)+float64(o.Width)/2, float64(o.Y)+float64(o.Height)/2
389		return
390	}
391	p.x, p.y = l.Clamp(p.x, p.y, p.x, p.y)
392}
393
394func (p *pointer) scale() float64 {
395	if o, ok := p.layout.At(p.x, p.y); ok && o.Scale > 0 {
396		return o.Scale
397	}
398	return 1
399}
400
401// move applies a relative motion and returns it with the logical deltas.
402// A locked pointer stays still; a confined one stays in its rectangle.
403func (p *pointer) move(dx, dy float64) ports.PointerMotion {
404	s := p.scale()
405	dx, dy = dx/s, dy/s
406	if p.constraint.Mode != ports.ConstraintLock {
407		p.set(p.x+dx, p.y+dy)
408	}
409	return ports.PointerMotion{X: p.x, Y: p.y, DX: dx, DY: dy}
410}
411
412func (p *pointer) set(x, y float64) (float64, float64) {
413	p.touched = true
414	p.x, p.y = p.constraint.Clamp(p.layout.Clamp(p.x, p.y, x, y))
415	return p.x, p.y
416}
417
418// constrain applies a new constraint. Entering or leaving a lock resyncs
419// the pointer to core's cursor, which stood still meanwhile; a confined
420// pointer moves inside.
421func (p *pointer) constrain(c ports.PointerConstraint) {
422	if c.Mode == ports.ConstraintLock || p.constraint.Mode == ports.ConstraintLock {
423		p.x, p.y = p.layout.Clamp(c.X, c.Y, c.X, c.Y)
424	}
425	p.constraint = c
426	p.x, p.y = c.Clamp(p.x, p.y)
427}
428
429// bounds is the rectangle holding every output.
430func (p *pointer) bounds() ports.Rect {
431	var r ports.Rect
432	for _, o := range p.layout {
433		r.W = max(r.W, o.X+o.Width)
434		r.H = max(r.H, o.Y+o.Height)
435	}
436	return r
437}
438
439// moved reports the physical position on the output under the pointer.
440func (p *pointer) moved(move func(output string, x, y float64)) {
441	if o, ok := p.layout.At(p.x, p.y); ok && move != nil {
442		move(o.Info.Name, (p.x-float64(o.X))*o.Scale, (p.y-float64(o.Y))*o.Scale)
443	}
444}

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