detached 5f1ad00

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

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