v0.4.0 2de4e1d

window.go RawHistoryFolder

1//go:build linux
2
3// Package wayland owns the minimal xdg-shell window and presentation protocol
4// objects. Vulkan allocations and DMA-BUF image creation remain the renderer's
5// responsibility; a window may not attach until the first configure is acked.
6package wayland
7
8import (
9	"context"
10	"encoding/binary"
11	"errors"
12	"fmt"
13	"math"
14	"os"
15	"path/filepath"
16	"sync"
17	"sync/atomic"
18	"time"
19
20	"github.com/bnema/wlturbo"
21	"github.com/bnema/wlturbo/protocol/core"
22	"github.com/bnema/wlturbo/protocol/cursorshape"
23	"github.com/bnema/wlturbo/protocol/drmsyncobj"
24	"github.com/bnema/wlturbo/protocol/extsessionlock"
25	"github.com/bnema/wlturbo/protocol/fractionalscale"
26	"github.com/bnema/wlturbo/protocol/linuxdmabuf"
27	"github.com/bnema/wlturbo/protocol/viewporter"
28	"github.com/bnema/wlturbo/protocol/xdgshell"
29	"github.com/bnema/wlturbo/wl"
30
31	"github.com/bnema/nefergui/internal/platform/wayland/layershell"
32)
33
34// CapabilityError reports a missing or insufficient required protocol.
35type CapabilityError struct {
36	Name  string
37	Cause error
38}
39
40func (e *CapabilityError) Error() string {
41	return fmt.Sprintf("Wayland capability %s: %v", e.Name, e.Cause)
42}
43func (e *CapabilityError) Unwrap() error { return e.Cause }
44
45const (
46	DRMXRGB8888 = uint32(0x34325258)
47	DRMARGB8888 = uint32(0x34325241)
48)
49
50type Window struct {
51	connection                *Connection
52	ownsConnection            bool
53	LockSurface               *extsessionlock.ExtSessionLockSurface
54	Display                   *wl.Display
55	Compositor                *core.Compositor
56	Shell                     *xdgshell.XdgWmBase
57	Dmabuf                    *linuxdmabuf.LinuxDmabuf
58	SyncManager               *drmsyncobj.WpLinuxDrmSyncobjManager
59	LayerShell                *layershell.LayerShell   // nil for xdg windows
60	LayerSurface              *layershell.LayerSurface // nil for xdg windows
61	Surface                   *core.Surface
62	XdgSurface                *xdgshell.XdgSurface
63	Toplevel                  *xdgshell.XdgToplevel
64	SyncSurface               *drmsyncobj.WpLinuxDrmSyncobjSurface
65	Viewporter                *viewporter.WpViewporter
66	Viewport                  *viewporter.WpViewport
67	ScaleManager              *fractionalscale.WpFractionalScaleManager
68	Fractional                *fractionalscale.WpFractionalScale
69	Feedback                  *linuxdmabuf.LinuxDmabufFeedback
70	Seat                      *core.Seat
71	Pointer                   *core.Pointer
72	CursorShapes              *cursorshape.WpCursorShapeManager // optional
73	cursor                    cursorState
74	pointerGen                uint64 // bumped per wl_pointer; session loop only
75	WLKeyboard                *core.Keyboard
76	keymapCache               *keyboardMapCache // owner retires each keyboard generation
77	seatName, seatVersion     uint32
78	Width, Height             int32
79	Scale                     float64
80	Configured, Closed, Dirty bool
81	Transparent               bool
82	FrameReady                bool
83	// Feedback fields are populated by the default feedback event stream.
84	FeedbackDone  bool // owner has applied the complete feedback round
85	MainDevice    uint64
86	Formats       []linuxdmabuf.FormatEntry
87	Tranches      [][]linuxdmabuf.FormatEntry
88	feedbackErr   error
89	events        chan Event
90	readerDone    chan struct{}
91	readerStop    chan struct{}
92	readerOnce    sync.Once
93	readerStarted atomic.Bool
94	rawInput      []Event // lock-only numeric protocol events, never ordinary text
95	inputAdapter  *InputAdapter
96	inputQueue    []InputEvent
97	// inputRects/inputCustom record the current input region; session loop only.
98	inputRects          []Rect
99	inputCustom         bool
100	layerOutputGlobal   uint32
101	lockOutput          uint32
102	layerOutputSelected bool
103	// InputOverflow counts events discarded when the 4096-event queue fills.
104	InputOverflow uint64
105}
106
107// Connect opens a default xdg-toplevel window with full-surface input. It
108// cannot be cancelled; use ConnectWithOptions for a cancellable startup.
109func Connect(name string, width, height int32, transparent bool) (*Window, error) {
110	return ConnectWithOptions(context.Background(), name, width, height, transparent, SurfaceOptions{})
111}
112
113// dialTimeout bounds the unix-socket dial; a local dial is normally instant.
114const dialTimeout = 5 * time.Second
115
116// waylandSocketPath resolves name like wlturbo.Connect does.
117func waylandSocketPath(name string) (string, error) {
118	if name == "" {
119		name = os.Getenv("WAYLAND_DISPLAY")
120		if name == "" {
121			name = "wayland-0"
122		}
123	}
124	if filepath.IsAbs(name) {
125		return name, nil
126	}
127	runDir := os.Getenv("XDG_RUNTIME_DIR")
128	if runDir == "" {
129		return "", errors.New("XDG_RUNTIME_DIR not set")
130	}
131	return filepath.Join(runDir, name), nil
132}
133
134// ConnectWithOptions opens a window whose role and initial input region are
135// selected by opts. The zero SurfaceOptions is identical to Connect. Startup
136// (dial, registry, configure, dmabuf feedback) is abandoned with ctx: the
137// connection is closed and the returned error wraps ctx.Err(). Once it
138// returns successfully, ctx no longer affects the window.
139func ConnectWithOptions(ctx context.Context, name string, width, height int32, transparent bool, opts SurfaceOptions) (*Window, error) {
140	if ctx == nil {
141		return nil, errors.New("nil context")
142	}
143	if width <= 0 || height <= 0 {
144		return nil, fmt.Errorf("invalid logical size %dx%d", width, height)
145	}
146	if err := opts.Validate(); err != nil {
147		return nil, err
148	}
149	c, err := ConnectConnection(ctx, name)
150	if err != nil {
151		return nil, err
152	}
153	w, err := c.NewWindow(ctx, width, height, transparent, opts)
154	if err != nil {
155		_ = c.Close()
156		return nil, err
157	}
158	w.ownsConnection = true
159	return w, nil
160}
161
162// NewWindow creates a sibling on this connection. Before StartReader it waits
163// for initial configure/feedback; afterwards it returns unconfigured and the
164// owner must Apply tagged events before allocating/presenting buffers.
165func (c *Connection) NewWindow(ctx context.Context, width, height int32, transparent bool, opts SurfaceOptions) (res *Window, err error) {
166	if ctx == nil {
167		return nil, errors.New("nil context")
168	}
169	if width <= 0 || height <= 0 {
170		return nil, fmt.Errorf("invalid logical size %dx%d", width, height)
171	}
172	if err = opts.Validate(); err != nil {
173		return nil, err
174	}
175	if err = ctx.Err(); err != nil {
176		return nil, err
177	}
178	if c.closed {
179		return nil, errors.New("connection closed")
180	}
181	if opts.Lock != nil && (transparent || opts.Layer != nil || opts.InputRects != nil) {
182		return nil, errors.New("lock role requires opaque full-input surface")
183	}
184	if opts.Lock != nil && !c.started.Load() {
185		// A lock that was already refused or ended must not get surfaces; its
186		// lock and output events are held for the owner, not applied here.
187		if err = c.drainStartup(true); err != nil {
188			return nil, err
189		}
190	}
191	// Objects created below get their handlers after their request is sent;
192	// a running reader must not dispatch their first events in between.
193	defer c.pauseReader()()
194	d := c.Display
195	// Closing the display unblocks any pending read; Display.Close is idempotent.
196	stopWatch := context.AfterFunc(ctx, func() { _ = d.Close() })
197	w := &Window{connection: c, events: c.events, readerStop: c.stop, Display: d, Width: width, Height: height, Scale: 1, Transparent: transparent}
198	w.readerStarted.Store(c.started.Load())
199	defer func() {
200		if !stopWatch() && err == nil {
201			// Cancelled after the last step but before return: the watcher owns a
202			// closed display, so the window is unusable.
203			err = ctx.Err()
204		}
205		if err != nil {
206			if cerr := ctx.Err(); cerr != nil && !errors.Is(err, cerr) {
207				err = errors.Join(cerr, err)
208			}
209			_ = w.Close()
210			res = nil
211		}
212	}()
213	wctx := d.Context()
214	bind := func(name string, supported, minimum uint32, proxy wl.Proxy) error {
215		advertised, ok := d.Registry().FindGlobal(name)
216		if !ok || advertised.Version < minimum {
217			return &CapabilityError{Name: name, Cause: fmt.Errorf("requires version %d, announced %d", minimum, advertised.Version)}
218		}
219		got, e := d.Registry().BindNegotiated(name, supported, proxy)
220		if e != nil {
221			return &CapabilityError{Name: name, Cause: e}
222		}
223		if got < minimum {
224			return &CapabilityError{Name: name, Cause: fmt.Errorf("requires version %d, compositor negotiated %d", minimum, got)}
225		}
226		return nil
227	}
228	w.Compositor = core.NewCompositor(wctx)
229	if err = bind(core.CompositorInterface, 6, 4, w.Compositor); err != nil {
230		return nil, err
231	}
232	if opts.Layer != nil {
233		if err = w.bindLayerShell(bind); err != nil {
234			return nil, err
235		}
236	} else if opts.Lock == nil {
237		w.Shell = xdgshell.NewXdgWmBase(wctx)
238		if err = bind(xdgshell.XdgWmBaseInterface, 6, 1, w.Shell); err != nil {
239			return nil, err
240		}
241	}
242	w.Dmabuf = linuxdmabuf.NewLinuxDmabuf(wctx)
243	if err = bind(linuxdmabuf.LinuxDmabufInterface, 4, 4, w.Dmabuf); err != nil {
244		return nil, err
245	}
246	w.SyncManager = drmsyncobj.NewWpLinuxDrmSyncobjManager(wctx)
247	if err = bind(drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, w.SyncManager); err != nil {
248		return nil, err
249	}
250	w.Viewporter = viewporter.NewWpViewporter(wctx)
251	if _, e := d.Registry().BindNegotiated(viewporter.WpViewporterInterface, 1, w.Viewporter); e != nil {
252		if !errors.Is(e, wlturbo.ErrGlobalNotFound) {
253			return nil, e
254		}
255		wctx.Unregister(w.Viewporter)
256		w.Viewporter = nil
257	}
258	w.ScaleManager = fractionalscale.NewWpFractionalScaleManager(wctx)
259	if _, e := d.Registry().BindNegotiated(fractionalscale.WpFractionalScaleManagerInterface, 1, w.ScaleManager); e != nil {
260		if !errors.Is(e, wlturbo.ErrGlobalNotFound) {
261			return nil, e
262		}
263		wctx.Unregister(w.ScaleManager)
264		w.ScaleManager = nil
265	}
266	w.CursorShapes = cursorshape.NewWpCursorShapeManager(wctx)
267	if _, e := d.Registry().BindNegotiated(cursorshape.WpCursorShapeManagerInterface, 1, w.CursorShapes); e != nil {
268		if !errors.Is(e, wlturbo.ErrGlobalNotFound) {
269			return nil, e
270		}
271		wctx.Unregister(w.CursorShapes)
272		w.CursorShapes = nil
273	}
274	w.Seat, w.Pointer, w.WLKeyboard = c.seat.Seat, c.seat.Pointer, c.seat.WLKeyboard
275	if opts.Lock != nil {
276		w.Seat = nil
277	} // clipboard/IME constructors require Seat
278	w.pointerGen = c.seat.pointerGen
279	if w.Shell != nil {
280		w.Shell.OnPing(func(serial uint32) {
281			if e := w.Shell.Pong(serial); e != nil {
282				w.feedbackFailure(e)
283			}
284		})
285	}
286	w.Surface, err = w.Compositor.CreateSurface()
287	if err != nil {
288		return nil, err
289	}
290	if w.Viewporter != nil {
291		w.Viewport, err = w.Viewporter.GetViewport(w.Surface)
292		if err != nil {
293			return nil, err
294		}
295	}
296	if w.ScaleManager != nil {
297		w.Fractional, err = w.ScaleManager.GetFractionalScale(w.Surface)
298		if err != nil {
299			return nil, err
300		}
301		w.Fractional.OnPreferredScale(func(scale uint32) {
302			if w.readerStarted.Load() {
303				w.post(Event{Kind: PreferredScale, Scale: scale})
304				return
305			}
306			if scale == 0 {
307				w.feedbackFailure(fmt.Errorf("invalid fractional scale zero"))
308				return
309			}
310			if scale%120 != 0 && w.Viewport == nil {
311				w.feedbackFailure(&CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %d/120 requires viewporter", scale)})
312				return
313			}
314			if w.Scale != float64(scale)/120 {
315				w.Scale = float64(scale) / 120
316				w.Dirty = true
317			}
318		})
319	}
320	w.Surface.OnPreferredBufferScale(func(factor int32) {
321		if w.readerStarted.Load() {
322			w.post(Event{Kind: BufferScale, Factor: factor})
323			return
324		}
325		if w.Fractional == nil && factor > 0 && w.Scale != float64(factor) {
326			w.Scale = float64(factor)
327			w.Dirty = true
328		}
329	})
330	c.windows[w.Surface.ID()] = w
331	c.routes.Store(w.Surface.ID(), w)
332	if opts.Lock != nil {
333		if err = w.createLockSurface(*opts.Lock); err != nil {
334			return nil, err
335		}
336	} else if opts.Layer != nil {
337		if err = w.createLayerSurface(*opts.Layer); err != nil {
338			return nil, err
339		}
340	} else if err = w.createToplevel(); err != nil {
341		return nil, err
342	}
343	if opts.InputRects != nil {
344		if err = w.SetInputRects(opts.InputRects); err != nil {
345			return nil, err
346		}
347	}
348	w.SyncSurface, err = w.SyncManager.GetSurface(w.Surface)
349	if err != nil {
350		return nil, err
351	}
352	w.Feedback, err = w.Dmabuf.GetDefaultFeedback()
353	if err != nil {
354		return nil, err
355	}
356	w.watchFeedback()
357	// ext-session-lock configure is sent by get_lock_surface; unlike xdg and
358	// layer-shell it forbids the initial empty commit before ACK+buffer.
359	if opts.Lock == nil {
360		if err = w.Surface.Commit(); err != nil {
361			return nil, err
362		}
363	}
364	if c.started.Load() {
365		return w, nil
366	}
367	for !w.Configured || !w.FeedbackDone || w.MainDevice == 0 || len(w.Tranches) == 0 {
368		if w.Closed {
369			return nil, fmt.Errorf("window closed before initial configure and feedback")
370		}
371		if err = c.drainStartup(true); err != nil {
372			return nil, err // a finished already queued: do not wait for a configure
373		}
374		if err = w.Dispatch(); err != nil {
375			return nil, err
376		}
377		if err = c.drainStartup(true); err != nil {
378			return nil, err
379		}
380	}
381	return w, nil
382}
383
384func (w *Window) createToplevel() error {
385	var err error
386	w.XdgSurface, err = w.Shell.GetXdgSurface(w.Surface)
387	if err != nil {
388		return err
389	}
390	w.Toplevel, err = w.XdgSurface.GetToplevel()
391	if err != nil {
392		return err
393	}
394	w.Toplevel.OnClose(func() {
395		if w.readerStarted.Load() {
396			w.post(Event{Kind: CloseEvent})
397			return
398		}
399		w.Closed = true
400	})
401	w.Toplevel.OnConfigure(func(width, height int32, _ []byte) {
402		if w.readerStarted.Load() {
403			w.post(Event{Kind: ConfigureSize, Width: width, Height: height})
404			return
405		}
406		if width > 0 && height > 0 && (w.Width != width || w.Height != height) {
407			w.Width, w.Height, w.Dirty = width, height, true
408		}
409	})
410	w.XdgSurface.OnConfigure(func(serial uint32) {
411		if w.readerStarted.Load() {
412			w.post(Event{Kind: ConfigureSerial, Serial: serial})
413			return
414		}
415		if e := w.XdgSurface.AckConfigure(serial); e != nil {
416			w.feedbackFailure(e)
417			return
418		}
419		w.Configured, w.FrameReady = true, true
420	})
421	return nil
422}
423
424func (w *Window) Dispatch() error {
425	if w.connection != nil && w.connection.started.Load() {
426		return errors.New("connection reader already started")
427	}
428	if w.feedbackErr != nil {
429		return w.feedbackErr
430	}
431	if err := w.Display.Dispatch(); err != nil {
432		return err
433	}
434	return w.feedbackErr
435}
436
437// Ready reports that a buffer may be allocated and attached: the role's
438// configure was acknowledged, the dmabuf feedback round is complete and the
439// previous frame callback has fired.
440func (w *Window) Ready() bool { return w.Configured && w.FeedbackDone && w.FrameReady }
441
442// IsLock reports whether the window is an ext-session-lock surface.
443func (w *Window) IsLock() bool { return w != nil && w.LockSurface != nil }
444
445func (w *Window) PhysicalSize() (int32, int32, error) {
446	if w.Scale <= 0 || w.Width <= 0 || w.Height <= 0 {
447		return 0, 0, fmt.Errorf("invalid window geometry")
448	}
449	if w.Scale != math.Trunc(w.Scale) && w.Viewport == nil {
450		return 0, 0, &CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %g requires viewporter", w.Scale)}
451	}
452	// Ignore sub-nanopixel floating noise at exact integer boundaries.
453	x, y := math.Ceil(float64(w.Width)*w.Scale-1e-9), math.Ceil(float64(w.Height)*w.Scale-1e-9)
454	if x > math.MaxInt32 || y > math.MaxInt32 {
455		return 0, 0, fmt.Errorf("buffer dimensions overflow")
456	}
457	return int32(x), int32(y), nil
458}
459
460// Present sets both syncobj points on every buffer commit. The caller supplies
461// an already-imported wl_buffer and two persistent Wayland timeline objects.
462func (w *Window) Present(buffer *core.Buffer, acquire, release *drmsyncobj.WpLinuxDrmSyncobjTimeline, acquirePoint, releasePoint uint64) error {
463	if !w.Configured || buffer == nil || acquire == nil || release == nil {
464		return fmt.Errorf("unconfigured surface or missing buffer/timeline")
465	}
466	if !w.FeedbackDone {
467		return fmt.Errorf("dmabuf feedback incomplete")
468	}
469	if !w.FrameReady {
470		return fmt.Errorf("frame callback pending")
471	}
472	pw, ph, err := w.PhysicalSize()
473	if err != nil {
474		return err
475	}
476	if w.Viewport != nil {
477		if err = w.Viewport.SetDestination(w.Width, w.Height); err != nil {
478			return err
479		}
480	} else if w.Scale > 1 {
481		if err = w.Surface.SetBufferScale(int32(w.Scale)); err != nil {
482			return err
483		}
484	}
485	if w.Transparent {
486		if err = w.Surface.SetOpaqueRegion(nil); err != nil {
487			return err
488		}
489	} else {
490		region, e := w.Compositor.CreateRegion()
491		if e != nil {
492			return e
493		}
494		if err = region.Add(0, 0, w.Width, w.Height); err == nil {
495			err = w.Surface.SetOpaqueRegion(region)
496		}
497		_ = region.Destroy()
498		if err != nil {
499			return err
500		}
501	}
502	if err = w.SyncSurface.SetAcquirePoint(acquire, uint32(acquirePoint>>32), uint32(acquirePoint)); err != nil {
503		return err
504	}
505	if err = w.SyncSurface.SetReleasePoint(release, uint32(releasePoint>>32), uint32(releasePoint)); err != nil {
506		return err
507	}
508	if err = w.Surface.Attach(buffer, 0, 0); err != nil {
509		return err
510	}
511	if err = w.Surface.DamageBuffer(0, 0, pw, ph); err != nil {
512		return err
513	}
514	cb, e := w.Surface.Frame()
515	if e != nil {
516		return e
517	}
518	cb.OnDone(func(_ uint32) {
519		if w.readerStarted.Load() {
520			w.post(Event{Kind: FrameEvent})
521			return
522		}
523		w.FrameReady = true
524	})
525	if err = w.Surface.Commit(); err != nil {
526		return err
527	}
528	w.FrameReady, w.Dirty = false, false
529	return nil
530}
531
532// DestroySyncSurface stops associating new explicit-sync points with the surface.
533// The session calls it after committing a null attachment at shutdown.
534func (w *Window) DestroySyncSurface() error {
535	if w == nil || w.SyncSurface == nil {
536		return nil
537	}
538	err := w.SyncSurface.Destroy()
539	if err == nil {
540		w.SyncSurface = nil
541	}
542	return err
543}
544
545// Close destroys children before their parents. The caller first detaches and
546// waits for its own GPU work before destroying its Vulkan allocations.
547func (w *Window) Close() error {
548	if w == nil {
549		return nil
550	}
551	if w.ownsConnection && w.connection != nil && !w.connection.closed {
552		return w.connection.Close()
553	}
554	for _, ev := range w.rawInput {
555		CloseEventFD(ev)
556	}
557	clear(w.rawInput)
558	w.rawInput = nil
559	// The loop sends protocol destructors before Close; the reader is
560	// unblocked by the connection close at the end of this method.
561	if w.inputAdapter != nil {
562		w.inputAdapter.Close()
563		w.inputAdapter = nil
564	}
565	if w.Seat != nil && w.connection == nil {
566		_ = w.releaseSeat()
567	}
568	w.dropCursorDevice()
569	if w.CursorShapes != nil {
570		_ = w.CursorShapes.Destroy()
571		w.CursorShapes = nil
572	}
573	if w.Feedback != nil {
574		_ = w.Feedback.Destroy()
575		w.Feedback = nil
576	}
577	if w.SyncSurface != nil {
578		_ = w.SyncSurface.Destroy()
579		w.SyncSurface = nil
580	}
581	if w.connection != nil && w.Surface != nil {
582		delete(w.connection.windows, w.Surface.ID())
583		w.connection.routes.Delete(w.Surface.ID())
584	}
585	if w.LockSurface != nil {
586		_ = w.LockSurface.Destroy()
587		w.LockSurface = nil
588	}
589	w.closeLayer() // before wl_surface
590	if w.Toplevel != nil {
591		_ = w.Toplevel.Destroy()
592		w.Toplevel = nil
593	}
594	if w.XdgSurface != nil {
595		_ = w.XdgSurface.Destroy()
596		w.XdgSurface = nil
597	}
598	if w.Fractional != nil {
599		_ = w.Fractional.Destroy()
600		w.Fractional = nil
601	}
602	if w.Viewport != nil {
603		_ = w.Viewport.Destroy()
604		w.Viewport = nil
605	}
606	if w.Surface != nil {
607		_ = w.Surface.Destroy()
608		w.Surface = nil
609	}
610	if w.SyncManager != nil {
611		_ = w.SyncManager.Destroy()
612		w.SyncManager = nil
613	}
614	if w.Dmabuf != nil {
615		_ = w.Dmabuf.Destroy()
616		w.Dmabuf = nil
617	}
618	if w.ScaleManager != nil {
619		_ = w.ScaleManager.Destroy()
620		w.ScaleManager = nil
621	}
622	if w.Viewporter != nil {
623		_ = w.Viewporter.Destroy()
624		w.Viewporter = nil
625	}
626	if w.Shell != nil {
627		_ = w.Shell.Destroy()
628		w.Shell = nil
629	}
630	// wl_compositor is bound at most at version 6; its release destructor
631	// needs version 7, so the proxy is only forgotten locally.
632	if w.Compositor != nil {
633		if c := w.Compositor.Context(); c != nil {
634			c.Unregister(w.Compositor)
635		}
636		w.Compositor = nil
637	}
638	if w.connection != nil {
639		w.Display = nil
640		w.Closed = true
641		if w.ownsConnection {
642			return w.connection.Close()
643		}
644		return nil
645	}
646	if w.Display != nil {
647		if w.readerDone != nil {
648			close(w.readerStop)
649		}
650		err := w.Display.Close()
651		if w.readerDone != nil {
652			<-w.readerDone
653			w.readerDone = nil
654		}
655		for len(w.events) > 0 {
656			CloseEventFD(<-w.events)
657		}
658		w.Display = nil
659		return err
660	}
661	return nil
662}
663
664func (w *Window) watchFeedback() {
665	var table []linuxdmabuf.FormatEntry
666	var indices []uint16
667	w.Feedback.OnFormatTable(func(fd *wl.OwnedFD, size uint32) {
668		var err error
669		table, err = linuxdmabuf.ReadFormatTable(fd, size)
670		if err != nil {
671			w.feedbackFailure(err)
672		}
673	})
674	w.Feedback.OnMainDevice(func(device []byte) {
675		if len(device) != 8 {
676			w.feedbackFailure(fmt.Errorf("invalid dmabuf main_device length %d", len(device)))
677			return
678		}
679		if w.readerStarted.Load() {
680			w.post(Event{Kind: FeedbackMainDevice, Device: binary.NativeEndian.Uint64(device)})
681		} else {
682			w.MainDevice = binary.NativeEndian.Uint64(device)
683		}
684	})
685	w.Feedback.OnTrancheFormats(func(data []byte) {
686		var err error
687		indices, err = linuxdmabuf.TrancheIndices(data, len(table))
688		if err != nil {
689			w.feedbackFailure(err)
690		}
691	})
692	w.Feedback.OnTrancheDone(func() {
693		var tranche []linuxdmabuf.FormatEntry
694		for _, i := range indices {
695			tranche = append(tranche, table[i])
696		}
697		if w.readerStarted.Load() {
698			w.post(Event{Kind: FeedbackTranche, Tranche: tranche})
699		} else {
700			w.Tranches = append(w.Tranches, tranche)
701		}
702		indices = nil
703	})
704	w.Feedback.OnDone(func() {
705		if w.readerStarted.Load() {
706			w.post(Event{Kind: FeedbackFormats, Formats: table})
707			w.post(Event{Kind: FeedbackComplete})
708		} else {
709			w.Formats = table
710			w.FeedbackDone = true
711		}
712	})
713}
714
715func (w *Window) feedbackFailure(err error) {
716	if w.readerStarted.Load() {
717		w.post(Event{Kind: FeedbackError, Err: err})
718	} else {
719		w.feedbackErr = err
720	}
721}

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 12

↑↓ moveenter openesc close