v0.2.0 e178e0b

seat.go RawHistoryFolder

1package neferclient
2
3import (
4	"errors"
5	"fmt"
6
7	"github.com/bnema/go-wayland-bindings/client/cursorshape"
8	"github.com/bnema/go-wayland-bindings/client/wayland"
9	"github.com/bnema/wlturbo/wl"
10	"golang.org/x/sys/unix"
11)
12
13// maxSeatVersion is the highest wl_seat version bound. Version 5 adds the
14// wl_seat.release request and wl_pointer.frame, 8 adds wl_pointer.axis_value120.
15const maxSeatVersion = 9
16
17// CursorShape is a wp_cursor_shape_v1 shape. The values are the protocol's.
18type CursorShape uint32
19
20const (
21	CursorDefault CursorShape = iota + 1
22	CursorContextMenu
23	CursorHelp
24	CursorPointer
25	CursorProgress
26	CursorWait
27	CursorCell
28	CursorCrosshair
29	CursorText
30	CursorVerticalText
31	CursorAlias
32	CursorCopy
33	CursorMove
34	CursorNoDrop
35	CursorNotAllowed
36	CursorGrab
37	CursorGrabbing
38	CursorEResize
39	CursorNResize
40	CursorNEResize
41	CursorNWResize
42	CursorSResize
43	CursorSEResize
44	CursorSWResize
45	CursorWResize
46	CursorEWResize
47	CursorNSResize
48	CursorNESWResize
49	CursorNWSEResize
50	CursorColResize
51	CursorRowResize
52	CursorAllScroll
53	CursorZoomIn
54	CursorZoomOut
55	CursorDndAsk    // needs cursor-shape version 2
56	CursorAllResize // needs cursor-shape version 2
57)
58
59// PointerKind tells which pointer event a [PointerEvent] is.
60type PointerKind uint8
61
62const (
63	PointerEnter PointerKind = iota + 1
64	PointerLeave
65	PointerMotion
66	PointerButton
67	// PointerAxis is a scroll step on Axis. DX or DY carry the continuous
68	// value in logical pixels; Value120 carries the wheel value in 1/120 of a
69	// notch. A compositor can send both for one physical step: use one.
70	PointerAxis
71	// PointerAxisStop ends a scroll sequence on Axis.
72	PointerAxisStop
73)
74
75// Scroll axes of [PointerEvent.Axis].
76const (
77	AxisVertical   = 0
78	AxisHorizontal = 1
79)
80
81// PointerEvent is one pointer event over a surface of the connection. X and Y
82// are surface-local logical pixels (already logical: never divide by scale)
83// and hold the latest position for every kind.
84type PointerEvent struct {
85	Surface  SurfaceID
86	Kind     PointerKind
87	X, Y     float64
88	DX, DY   float64 // PointerAxis: continuous scroll value
89	Button   uint32  // PointerButton: evdev button code
90	Pressed  bool    // PointerButton
91	Axis     uint32  // PointerAxis, PointerAxisStop: AxisVertical or AxisHorizontal
92	Value120 int32   // PointerAxis: wheel value in 1/120 notches
93}
94
95// KeyEvent is one key press, repeat or release.
96type KeyEvent struct {
97	Surface   SurfaceID
98	Keycode   uint32 // evdev key code; 0 when Secret is set
99	Keysym    uint32 // xkb keysym in the current layout and modifier state; 0 when Secret is set
100	Modifiers Modifiers
101	Group     uint32 // effective layout index
102	Pressed   bool
103	Repeat    bool
104	// Secret is set while a [SecretBuffer] is installed, for the press,
105	// repeats and release of a key that produced text (or is a dead or compose
106	// key): the text went into the buffer, and Keycode and Keysym are 0 so the
107	// event does not reveal which character was typed. Other keys (Enter,
108	// Escape, BackSpace, modifiers, Control combinations) keep their keysym.
109	Secret bool
110	// Text is the UTF-8 text of the key, nil for releases and keys without
111	// text. It aliases library storage that is wiped when Handler.Key
112	// returns. It is always nil while a [SecretBuffer] is installed.
113	Text []byte
114}
115
116// Seat is the connection's first wl_seat: pointer, keyboard and cursor. It
117// belongs to the owner goroutine. Without a seat global every method is a
118// no-op. The seat is bound once, at first use: a seat announced later is not
119// picked up, and a removed one is not replaced (no seat hotplug).
120type Seat struct {
121	c     *Conn
122	proxy *wayland.Seat
123	name  uint32 // registry name of the global
124	ver   uint32
125
126	pointer *wayland.Pointer
127	ptrGen  uint32 // generation of pointer; bumped on every create and drop
128	ptrSurf SurfaceID
129	px, py  float64
130	pev     PointerEvent
131
132	keyboard *wayland.Keyboard
133	kbGen    uint32
134	kbSurf   SurfaceID
135	kb       *keyboard
136	kev      KeyEvent
137
138	secret *SecretBuffer
139
140	shapes      *cursorshape.WpCursorShapeManager
141	shapesVer   uint32
142	shapeDev    *cursorshape.WpCursorShapeDevice
143	enterSerial uint32 // latest pointer enter serial, 0 while outside
144	shape       CursorShape
145}
146
147// Seat returns the connection's seat (nil after Close), binding wl_seat on first use. The seat
148// is also bound when the first surface is created. Input starts flowing to the
149// Handler once the compositor announces the capabilities.
150func (c *Conn) Seat() *Seat {
151	c.ensureSeat()
152	return c.seat
153}
154
155// ensureSeat binds wl_seat and the optional cursor-shape manager once. A
156// failure is reported by the next Dispatch through Handler.Error.
157func (c *Conn) ensureSeat() {
158	if c.seat != nil || c.closed {
159		return
160	}
161	s := &Seat{c: c}
162	c.seat = s
163	g, ok := c.display.Registry().FindGlobal(wayland.SeatInterface)
164	if !ok {
165		return
166	}
167	q := c.q
168	proxy, v, err := c.Bind(wayland.SeatInterface, maxSeatVersion, func(ctx *wl.Context) *wayland.Seat {
169		p := wayland.NewSeat(ctx)
170		p.OnCapabilities(func(caps uint32) {
171			e := event{kind: evSeatCaps, a: int32(caps)}
172			q.post(&e)
173		})
174		return p
175	})
176	if err != nil {
177		c.seatErr = fmt.Errorf("neferclient: bind wl_seat: %w", err)
178		return
179	}
180	s.proxy, s.name, s.ver = proxy, g.Name, v
181	m, mv, err := c.Bind(cursorshape.WpCursorShapeManagerInterface, 2, cursorshape.NewWpCursorShapeManager)
182	if err != nil {
183		if !errors.Is(err, ErrGlobalNotFound) {
184			c.seatErr = fmt.Errorf("neferclient: bind cursor shape manager: %w", err)
185		}
186		return
187	}
188	s.shapes, s.shapesVer = m, mv
189}
190
191// SetSecret installs b to receive typed text, or removes it with nil. While
192// installed, printable text goes only into b and never into KeyEvent.Text;
193// BackSpace removes one code point; every other key is still delivered as a
194// KeyEvent without text. Keys that produced text are delivered with
195// KeyEvent.Secret set and Keycode and Keysym 0, for the press, repeats and
196// release; Enter, Escape, BackSpace and Control combinations keep their
197// keysym. Installing or removing cancels held keys and repeat. The previous
198// buffer is not wiped: that is the caller's call.
199//
200// Limits: runtime/secret (GOEXPERIMENT=runtimesecret) erases Go registers,
201// stack and heap temporaries, but not the native memory of libxkbcommon, which
202// processes the key text.
203func (s *Seat) SetSecret(b *SecretBuffer) {
204	if s == nil {
205		return
206	}
207	s.secret = b
208	if s.kb != nil {
209		s.kb.setSecret(b)
210	}
211}
212
213// SetCursor asks the compositor to show shape for the pointer. It is a no-op
214// when the compositor has no cursor-shape support, the pointer is outside the
215// connection's surfaces, or shape is unchanged since the last enter.
216func (s *Seat) SetCursor(shape CursorShape) error {
217	if s == nil || s.c.closed {
218		return ErrClosed
219	}
220	if shape < CursorDefault || shape > CursorAllResize {
221		return fmt.Errorf("neferclient: invalid cursor shape %d", shape)
222	}
223	if shape >= CursorDndAsk && s.shapes != nil && s.shapesVer < 2 {
224		return &CapabilityError{Name: cursorshape.WpCursorShapeManagerInterface, Cause: fmt.Errorf("shape %d requires version 2", shape)}
225	}
226	if s.shapes == nil || s.pointer == nil || s.ptrSurf == 0 || shape == s.shape {
227		return nil
228	}
229	if s.shapeDev == nil {
230		dev, err := s.shapes.GetPointer(s.pointer)
231		if err != nil {
232			return fmt.Errorf("neferclient: cursor shape device: %w", err)
233		}
234		s.shapeDev = dev
235	}
236	if err := s.shapeDev.SetShape(s.enterSerial, uint32(shape)); err != nil {
237		return fmt.Errorf("neferclient: set cursor shape: %w", err)
238	}
239	s.shape = shape
240	return nil
241}
242
243// surfaceByObject finds the surface whose wl_surface has object id obj.
244func (c *Conn) surfaceByObject(obj uint32) *Surface {
245	for _, s := range c.surfaces {
246		if !s.closed && s.surf != nil && s.surf.ID() == obj {
247			return s
248		}
249	}
250	return nil
251}
252
253func (c *Conn) seatTimer(fd int32) bool {
254	return c.seat != nil && c.seat.kb != nil && int(fd) == c.seat.kb.tfd
255}
256
257// seatRepeat handles the repeat timer: at most one repeat per expiration.
258func (c *Conn) seatRepeat(h Handler) {
259	s := c.seat
260	if s.kb.expired() {
261		ke := &s.kev
262		*ke = KeyEvent{Surface: s.kbSurf}
263		ok, err := s.kb.repeat(ke)
264		switch {
265		case err != nil:
266			h.Error(err)
267		case ok:
268			s.deliverKey(ke, h)
269		}
270	}
271	if err := s.kb.takeTimerErr(); err != nil {
272		h.Error(err)
273	}
274	if !c.closed {
275		if err := c.armFD(s.kb.tfd, unix.EPOLL_CTL_MOD); err != nil {
276			h.Error(err)
277		}
278	}
279}
280
281func (s *Seat) deliverKey(ke *KeyEvent, h Handler) {
282	h.Key(ke)
283	clear(s.kb.scratch[:]) // ordinary text must not outlive the call
284	*ke = KeyEvent{}
285	if s.kb.changed {
286		h.SecretChanged(s.secret.Len())
287	}
288}
289
290// seatSurfaceGone forgets focus on a closing surface.
291func (c *Conn) seatSurfaceGone(sf *Surface) {
292	s := c.seat
293	if s == nil {
294		return
295	}
296	if s.ptrSurf == sf.id {
297		s.ptrSurf = 0
298		s.enterSerial, s.shape = 0, 0
299	}
300	if s.kbSurf == sf.id {
301		s.kbSurf = 0
302		if s.kb != nil {
303			s.kb.setFocus(false)
304		}
305	}
306}
307
308func (c *Conn) seatGlobalRemoved(global uint32, h Handler) {
309	if s := c.seat; s != nil && s.proxy != nil && s.name == global {
310		s.release(h)
311	}
312}
313
314// release drops the devices and the seat proxy.
315func (s *Seat) release(h Handler) {
316	s.dropPointer(h)
317	s.dropKeyboard(h)
318	if s.ver >= 5 {
319		_ = s.proxy.Release()
320	} else {
321		s.c.wlctx.Unregister(s.proxy)
322	}
323	s.proxy = nil
324}
325
326func releaseDevice(ver uint32, c *wl.Context, p interface {
327	wl.Proxy
328	Release() error
329}) {
330	if ver >= 3 {
331		_ = p.Release()
332	} else {
333		c.Unregister(p)
334	}
335}
336
337func (s *Seat) dropPointer(h Handler) {
338	if s.pointer == nil {
339		return
340	}
341	if s.shapeDev != nil {
342		_ = s.shapeDev.Destroy()
343		s.shapeDev = nil
344	}
345	s.enterSerial, s.shape = 0, 0
346	releaseDevice(s.ver, s.c.wlctx, s.pointer)
347	s.pointer = nil
348	s.ptrGen++
349	if id := s.ptrSurf; id != 0 {
350		s.ptrSurf = 0
351		pe := &s.pev
352		*pe = PointerEvent{Surface: id, Kind: PointerLeave, X: s.px, Y: s.py}
353		h.Pointer(pe)
354	}
355}
356
357func (s *Seat) dropKeyboard(h Handler) {
358	if s.keyboard == nil {
359		return
360	}
361	releaseDevice(s.ver, s.c.wlctx, s.keyboard)
362	s.keyboard = nil
363	s.kbGen++
364	if id := s.kbSurf; id != 0 {
365		s.kbSurf = 0
366		s.kb.setFocus(false)
367		h.KeyboardFocus(id, false)
368	} else if s.kb != nil {
369		s.kb.setFocus(false)
370	}
371}
372
373func (s *Seat) newPointer() error {
374	c := s.c
375	c.pause() // handlers go on after the request, before any event is read
376	defer c.resume()
377	p, err := s.proxy.GetPointer()
378	if err != nil {
379		return fmt.Errorf("neferclient: get wl_pointer: %w", err)
380	}
381	s.pointer = p
382	s.ptrGen++
383	gen, q := s.ptrGen, c.q
384	post := func(e *event) {
385		e.version = gen
386		q.post(e)
387	}
388	p.OnEnter(func(serial, surf uint32, x, y wl.Fixed) {
389		post(&event{kind: evPtrEnter, global: surf, serial: serial, a: int32(x), b: int32(y)})
390	})
391	p.OnLeave(func(_, surf uint32) { post(&event{kind: evPtrLeave, global: surf}) })
392	p.OnMotion(func(_ uint32, x, y wl.Fixed) { post(&event{kind: evPtrMotion, a: int32(x), b: int32(y)}) })
393	p.OnButton(func(serial, _, button, state uint32) {
394		post(&event{kind: evPtrButton, serial: serial, flags: button, a: int32(state)})
395	})
396	p.OnAxis(func(_, axis uint32, v wl.Fixed) { post(&event{kind: evPtrAxis, flags: axis, a: int32(v)}) })
397	p.OnAxisValue120(func(axis uint32, v int32) { post(&event{kind: evPtrAxisValue, flags: axis, a: v}) })
398	p.OnAxisStop(func(_, axis uint32) { post(&event{kind: evPtrAxisStop, flags: axis}) })
399	return nil
400}
401
402func (s *Seat) newKeyboard() error {
403	c := s.c
404	if s.kb == nil {
405		kb, err := newKeyboard(composeLocale())
406		if err != nil {
407			return fmt.Errorf("neferclient: keyboard: %w", err)
408		}
409		if err = c.armFD(kb.tfd, unix.EPOLL_CTL_ADD); err != nil {
410			kb.close()
411			return err // armFD wraps it
412		}
413		kb.secret = s.secret
414		s.kb = kb
415	}
416	c.pause()
417	defer c.resume()
418	k, err := s.proxy.GetKeyboard()
419	if err != nil {
420		return fmt.Errorf("neferclient: get wl_keyboard: %w", err)
421	}
422	s.keyboard = k
423	s.kbGen++
424	gen, q := s.kbGen, c.q
425	post := func(e *event) {
426		e.version = gen
427		q.post(e)
428	}
429	k.OnKeymap(func(format uint32, fd *wl.OwnedFD, size uint32) {
430		n, err := fd.Take()
431		if err != nil {
432			post(&event{kind: evSeatBad, a: badKeymapFD})
433			return
434		}
435		post(&event{kind: evKbKeymap, fd: int32(n), flags: size, a: int32(format)})
436	})
437	k.OnEnter(func(serial, surf uint32, _ []byte) { post(&event{kind: evKbEnter, global: surf, serial: serial}) })
438	k.OnLeave(func(_, surf uint32) { post(&event{kind: evKbLeave, global: surf}) })
439	k.OnKey(func(serial, _, key, state uint32) {
440		post(&event{kind: evKbKey, serial: serial, flags: key, a: int32(state)})
441	})
442	k.OnModifiers(func(_, depressed, latched, locked, group uint32) {
443		post(&event{kind: evKbModifiers, flags: depressed, dev: uint64(latched) | uint64(locked)<<32, a: int32(group)})
444	})
445	k.OnRepeatInfo(func(rate, delay int32) { post(&event{kind: evKbRepeat, a: rate, b: delay}) })
446	return nil
447}
448
449// Codes of evSeatBad.
450const badKeymapFD int32 = 100
451
452// applySeat handles the seat events on the owner.
453func (c *Conn) applySeat(ev *event, h Handler) {
454	defer ev.closeFD()
455	s := c.seat
456	if s == nil {
457		return
458	}
459	switch ev.kind {
460	case evSeatBad:
461		h.Error(errors.New("neferclient: keymap descriptor unavailable"))
462	case evSeatCaps:
463		s.applyCaps(uint32(ev.a), h)
464	case evPtrEnter, evPtrLeave, evPtrMotion, evPtrButton, evPtrAxis, evPtrAxisValue, evPtrAxisStop:
465		if s.pointer != nil && ev.version == s.ptrGen {
466			s.applyPointer(ev, h)
467		}
468	default:
469		if s.keyboard != nil && ev.version == s.kbGen {
470			s.applyKeyboard(ev, h)
471			if err := s.kb.takeTimerErr(); err != nil {
472				h.Error(err)
473			}
474		}
475	}
476}
477
478func (s *Seat) applyCaps(caps uint32, h Handler) {
479	if s.proxy == nil {
480		return
481	}
482	if caps&1 == 0 {
483		s.dropPointer(h)
484	}
485	if caps&2 == 0 {
486		s.dropKeyboard(h)
487	}
488	if caps&1 != 0 && s.pointer == nil {
489		if err := s.newPointer(); err != nil {
490			h.Error(err)
491		}
492	}
493	if caps&2 != 0 && s.keyboard == nil {
494		if err := s.newKeyboard(); err != nil {
495			h.Error(err)
496		}
497	}
498}
499
500func (s *Seat) applyPointer(ev *event, h Handler) {
501	pe := &s.pev
502	*pe = PointerEvent{}
503	fixed := func(v int32) float64 { return wl.Fixed(v).Float64() }
504	switch ev.kind {
505	case evPtrEnter:
506		sf := s.c.surfaceByObject(ev.global)
507		if sf == nil {
508			return
509		}
510		s.ptrSurf = sf.id
511		s.px, s.py = fixed(ev.a), fixed(ev.b)
512		s.enterSerial, s.shape = ev.serial, 0 // the compositor forgets the cursor on leave
513		pe.Kind = PointerEnter
514	case evPtrLeave:
515		if sf := s.c.surfaceByObject(ev.global); s.ptrSurf == 0 || sf == nil || sf.id != s.ptrSurf {
516			return
517		}
518		pe.Kind = PointerLeave
519		pe.Surface = s.ptrSurf
520		pe.X, pe.Y = s.px, s.py
521		s.ptrSurf = 0
522		s.enterSerial, s.shape = 0, 0
523		h.Pointer(pe)
524		return
525	default:
526		if s.ptrSurf == 0 {
527			return
528		}
529		switch ev.kind {
530		case evPtrMotion:
531			s.px, s.py = fixed(ev.a), fixed(ev.b)
532			pe.Kind = PointerMotion
533		case evPtrButton:
534			pe.Kind, pe.Button, pe.Pressed = PointerButton, ev.flags, ev.a == 1
535		case evPtrAxis:
536			pe.Kind, pe.Axis = PointerAxis, ev.flags
537			if ev.flags == AxisVertical {
538				pe.DY = fixed(ev.a)
539			} else {
540				pe.DX = fixed(ev.a)
541			}
542		case evPtrAxisValue:
543			pe.Kind, pe.Axis, pe.Value120 = PointerAxis, ev.flags, ev.a
544		case evPtrAxisStop:
545			pe.Kind, pe.Axis = PointerAxisStop, ev.flags
546		}
547	}
548	pe.Surface, pe.X, pe.Y = s.ptrSurf, s.px, s.py
549	h.Pointer(pe)
550}
551
552func (s *Seat) applyKeyboard(ev *event, h Handler) {
553	kb := s.kb
554	switch ev.kind {
555	case evKbKeymap:
556		fd := int(ev.fd)
557		ev.fd = -1     // replaceFD consumes fd; the deferred closeFD must not touch the number again
558		if ev.a == 0 { // no_keymap: the compositor sends no usable map; nothing to report
559			_ = unix.Close(fd)
560			return
561		}
562		if ev.a != 1 || ev.flags == 0 || ev.flags > keymapMax {
563			_ = unix.Close(fd)
564			h.Error(fmt.Errorf("neferclient: unsupported keymap format %d or size %d", ev.a, ev.flags))
565			return
566		}
567		if err := kb.replaceFD(fd, int(ev.flags)); err != nil {
568			h.Error(fmt.Errorf("neferclient: keymap: %w", err))
569		}
570	case evKbEnter:
571		sf := s.c.surfaceByObject(ev.global)
572		if sf == nil {
573			return
574		}
575		s.kbSurf = sf.id
576		kb.setFocus(true)
577		h.KeyboardFocus(sf.id, true)
578	case evKbLeave:
579		sf := s.c.surfaceByObject(ev.global)
580		if sf == nil || s.kbSurf != sf.id {
581			return
582		}
583		s.kbSurf = 0
584		kb.setFocus(false)
585		h.KeyboardFocus(sf.id, false)
586	case evKbKey:
587		ke := &s.kev
588		*ke = KeyEvent{Surface: s.kbSurf}
589		if ev.a == 1 {
590			ok, err := kb.press(ev.flags, ke)
591			switch {
592			case err != nil:
593				h.Error(err)
594			case ok:
595				s.deliverKey(ke, h)
596			}
597		} else if kb.release(ev.flags, ke) {
598			s.deliverKey(ke, h)
599		}
600	case evKbModifiers:
601		if err := kb.setMask(ev.flags, uint32(ev.dev), uint32(ev.dev>>32), uint32(ev.a)); err != nil {
602			h.Error(err)
603		}
604	case evKbRepeat:
605		kb.setRepeat(ev.a, ev.b)
606	}
607}
608
609func (c *Conn) closeSeat() {
610	if s := c.seat; s != nil && s.kb != nil {
611		s.kb.close()
612		s.kb = nil
613	}
614}

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 1

Tags 4

↑↓ moveenter openesc close