refactor/kvconf 3421f16

screen.go RawHistoryFolder

1package visual
2
3import (
4	"errors"
5	"fmt"
6	"strings"
7	"time"
8
9	"github.com/bnema/neferclient"
10	"github.com/bnema/nefergui"
11)
12
13// screen is one Wayland surface and the NeferGUI renderer that draws it. It
14// copies plain fields between the two libraries, which never import each
15// other. Everything runs on the owner goroutine that calls Conn.Dispatch.
16type screen struct {
17	conn        *neferclient.Conn
18	surf        *neferclient.Surface
19	output      uint32 // wl_output registry name
20	name        string // connector name, for error reports
21	transparent bool   // ARGB buffers, presented as non-opaque
22	view        func(*nefergui.Frame)
23
24	r      *nefergui.Renderer
25	fb     *neferclient.Feedback // feedback r was built from
26	out    nefergui.Output
27	damage []neferclient.Rect
28	live   map[uint64]int // buffer → release eventfd still watched
29
30	configured, canPresent, haveAcquire bool
31	built                               bool // a renderer was built once: later setups are rebuilds
32}
33
34func newScreen(conn *neferclient.Conn, surf *neferclient.Surface, o neferclient.Output, transparent bool) *screen {
35	return &screen{conn: conn, surf: surf, output: o.Global, name: o.Name, transparent: transparent, live: map[uint64]int{}}
36}
37
38// renderView adapts the per-screen view to Renderer.Render without a
39// per-frame closure.
40func renderView(f *nefergui.Frame, s *screen) { s.view(f) }
41
42// configure applies Handler.Configure. Only the first one lifts the
43// frame-callback gate; later ones must wait for Handler.Frame.
44func (s *screen) configure() error {
45	if !s.configured {
46		s.canPresent = true
47	}
48	s.configured = true
49	s.resize()
50	return s.setup()
51}
52
53// setup builds the renderer once the surface is configured and its dmabuf
54// feedback is complete, and rebuilds it when the compositor changes its
55// preferred device or formats (a repeated round is not a change).
56func (s *screen) setup() error {
57	fb := s.surf.Feedback()
58	if !s.configured || fb == nil || s.r != nil && fb.Equal(s.fb) {
59		return nil
60	}
61	if err := s.dropRenderer(); err != nil {
62		return err
63	}
64	cfg := nefergui.RendererConfig{MainDevice: fb.MainDevice, Formats: make([]nefergui.Format, len(fb.Formats)), Transparent: s.transparent}
65	for i, f := range fb.Formats {
66		cfg.Formats[i] = nefergui.Format{FourCC: f.FourCC, Modifier: f.Modifier}
67	}
68	r, err := nefergui.NewRenderer(cfg)
69	if err != nil {
70		return fmt.Errorf("renderer: %w", err)
71	}
72	s.r, s.fb, s.built = r, fb.Clone(), true // Feedback storage is reused by later rounds
73	s.resize()
74	return nil
75}
76
77func (s *screen) resize() {
78	if s.r != nil {
79		w, h, scale := s.surf.Size()
80		s.r.Resize(int(w), int(h), scale)
81	}
82}
83
84func (s *screen) invalidate() {
85	if s.r != nil {
86		s.r.Invalidate()
87	}
88}
89
90// draw renders when something changed and presents the frame. It reports
91// whether a built frame still waits for the GPU.
92func (s *screen) draw() (pending bool, err error) {
93	if s.r == nil {
94		return false, nil
95	}
96	if s.canPresent {
97		ok, err := s.r.Render(&s.out, s, renderView)
98		if err != nil {
99			return false, fmt.Errorf("render: %w", err)
100		}
101		if ok {
102			if err := s.present(); err != nil {
103				return false, err
104			}
105		}
106	}
107	return s.r.Pending(), nil
108}
109
110// watchID packs the surface and a buffer into the Conn.WatchFD id.
111func (s *screen) watchID(buffer uint64) uint64 { return uint64(s.surf.ID())<<32 | buffer }
112
113// present: Renderer output → ImportBuffer, ImportTimeline, Present. The input
114// region is not forwarded: it is fixed when the surface is created.
115func (s *screen) present() error {
116	out := &s.out
117	for _, rt := range out.Retired { // buffers of an older size
118		if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil {
119			return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
120		}
121		delete(s.live, rt.Buffer)
122		if err := s.surf.DestroyBuffer(rt.Buffer); err != nil {
123			return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
124		}
125		// The release timeline of a buffer is imported under the buffer's id.
126		if err := s.surf.DestroyTimeline(rt.Buffer); err != nil {
127			return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err)
128		}
129	}
130	if out.NewBuffer {
131		buf := neferclient.Buffer{Width: out.Width, Height: out.Height, FourCC: out.FourCC, Modifier: out.Modifier, PlaneCount: out.PlaneCount}
132		for i, p := range out.Planes {
133			buf.Planes[i] = neferclient.Plane{FD: p.FD, Offset: p.Offset, Stride: p.Stride}
134		}
135		if err := s.surf.ImportBuffer(out.Buffer, &buf); err != nil {
136			return fmt.Errorf("import buffer: %w", err)
137		}
138		// The release eventfd is stable per buffer: watched once, FDReady
139		// hands the buffer back with Renderer.Released.
140		if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil {
141			return fmt.Errorf("watch release fd: %w", err)
142		}
143		s.live[out.Buffer] = out.ReleaseFD
144	}
145	if out.NewTimelines {
146		if !s.haveAcquire { // one acquire timeline shared by every buffer
147			if err := s.surf.ImportTimeline(out.Acquire.ID, out.Acquire.FD); err != nil {
148				return fmt.Errorf("import acquire timeline: %w", err)
149			}
150			s.haveAcquire = true
151		}
152		if err := s.surf.ImportTimeline(out.Release.ID, out.Release.FD); err != nil {
153			return fmt.Errorf("import release timeline: %w", err)
154		}
155	}
156	s.damage = s.damage[:0]
157	for _, d := range out.Damage {
158		s.damage = append(s.damage, neferclient.Rect{X: d.X, Y: d.Y, Width: d.Width, Height: d.Height})
159	}
160	if err := s.surf.Present(&neferclient.Present{
161		Buffer: out.Buffer, AcquireTimeline: out.Acquire.ID, ReleaseTimeline: out.Release.ID,
162		AcquirePoint: out.AcquirePoint, ReleasePoint: out.ReleasePoint, Damage: s.damage,
163		Opaque: !s.transparent,
164	}); err != nil {
165		return fmt.Errorf("present: %w", err)
166	}
167	s.canPresent = false // until Handler.Frame
168	return nil
169}
170
171// released hands a buffer back after its release eventfd became readable.
172func (s *screen) released(buffer uint64) error {
173	if s.r == nil {
174		return nil
175	}
176	if err := s.r.Released(buffer); err != nil {
177		return fmt.Errorf("release buffer: %w", err)
178	}
179	return nil
180}
181
182// dropRenderer stops watching the release eventfds (they belong to the
183// renderer), destroys the imported buffers and timelines (allowed while a
184// frame is pending), then closes the renderer.
185func (s *screen) dropRenderer() error {
186	if s.r == nil {
187		return nil
188	}
189	var errs []error
190	for buffer, fd := range s.live {
191		if err := s.conn.UnwatchFD(fd); err != nil {
192			errs = append(errs, fmt.Errorf("unwatch buffer %d: %w", buffer, err))
193		}
194	}
195	clear(s.live)
196	errs = append(errs, s.surf.DestroyImports(), s.r.Close())
197	s.r, s.fb, s.haveAcquire = nil, nil, false
198	return errors.Join(errs...)
199}
200
201// close frees the renderer and the surface.
202func (s *screen) close() error {
203	return errors.Join(s.dropRenderer(), s.surf.Close())
204}
205
206// closeAfterConn frees what is left once the connection is closed: the
207// connection dropped the descriptor watches and every Wayland object, so only
208// the renderer's GPU resources remain.
209func (s *screen) closeAfterConn() error {
210	if s.r == nil {
211		return nil
212	}
213	err := s.r.Close()
214	s.r = nil
215	return err
216}
217
218// screens holds the surfaces of one connection and implements the Handler
219// methods they share. Failures from handler methods are kept in err; the
220// owner loop returns it after Dispatch.
221type screens struct {
222	neferclient.NopHandler
223
224	conn *neferclient.Conn
225	byID map[neferclient.SurfaceID]*screen
226	err  error
227	// onSetup reports a screen whose renderer could not be built (rebuild:
228	// after a feedback change); it returns the error that ends the run, or
229	// nil to drop only that screen.
230	onSetup func(s *screen, rebuild bool, err error) error
231	// onError reports an event error (Handler.Error); it returns the error
232	// that ends the run, or nil to go on.
233	onError func(err error) error
234}
235
236func newScreens(conn *neferclient.Conn) *screens {
237	ss := &screens{conn: conn, byID: map[neferclient.SurfaceID]*screen{}}
238	ss.onSetup = func(_ *screen, _ bool, err error) error { return err }
239	ss.onError = func(err error) error { return err }
240	return ss
241}
242
243func (ss *screens) fail(err error) {
244	if ss.err == nil {
245		ss.err = err
246	}
247}
248
249func (ss *screens) add(s *screen) { ss.byID[s.surf.ID()] = s }
250
251// forget drops a screen and frees it.
252func (ss *screens) forget(s *screen) error {
253	delete(ss.byID, s.surf.ID())
254	return s.close()
255}
256
257// Error applies onError to a failure while handling an event. Connection
258// failures are not reported here: Conn.Dispatch returns them and they end
259// the run.
260func (ss *screens) Error(err error) {
261	if err = ss.onError(err); err != nil {
262		ss.fail(err)
263	}
264}
265
266// keyboardError reports whether a neferclient event error leaves the seat
267// without a usable keyboard (seat, keyboard or keymap setup). neferclient has
268// no typed errors for these, so its messages are matched.
269func keyboardError(err error) bool {
270	msg := err.Error()
271	for _, p := range [...]string{"neferclient: keymap", "neferclient: unsupported keymap", "neferclient: keyboard:", "neferclient: bind wl_seat"} {
272		if strings.HasPrefix(msg, p) {
273			return true
274		}
275	}
276	return false
277}
278
279func (ss *screens) setupFailed(s *screen, err error) {
280	if err = ss.onSetup(s, s.built, err); err != nil {
281		ss.fail(err)
282		return
283	}
284	if err := ss.forget(s); err != nil {
285		ss.fail(err)
286	}
287}
288
289func (ss *screens) Configure(id neferclient.SurfaceID, _, _ int32) {
290	if s := ss.byID[id]; s != nil {
291		if err := s.configure(); err != nil {
292			ss.setupFailed(s, err)
293		}
294	}
295}
296
297func (ss *screens) FeedbackDone(id neferclient.SurfaceID) {
298	if s := ss.byID[id]; s != nil {
299		if err := s.setup(); err != nil {
300			ss.setupFailed(s, err)
301		}
302	}
303}
304
305func (ss *screens) Scale(id neferclient.SurfaceID, _ float64) {
306	if s := ss.byID[id]; s != nil {
307		s.resize()
308	}
309}
310
311func (ss *screens) Frame(id neferclient.SurfaceID) {
312	if s := ss.byID[id]; s != nil {
313		s.canPresent = true
314	}
315}
316
317func (ss *screens) FDReady(id uint64) {
318	if s := ss.byID[neferclient.SurfaceID(id>>32)]; s != nil {
319		if err := s.released(id & 0xffffffff); err != nil {
320			ss.fail(err)
321		}
322	}
323}
324
325// OutputRemoved frees the screens of an output that went away.
326func (ss *screens) OutputRemoved(global uint32) {
327	for _, s := range ss.byID {
328		if s.output == global {
329			if err := ss.forget(s); err != nil {
330				ss.fail(fmt.Errorf("output %s removed: %w", s.name, err))
331			}
332		}
333	}
334}
335
336// covers reports whether an output already has a screen.
337func (ss *screens) covers(global uint32) bool {
338	for _, s := range ss.byID {
339		if s.output == global {
340			return true
341		}
342	}
343	return false
344}
345
346func (ss *screens) invalidateAll() {
347	for _, s := range ss.byID {
348		s.invalidate()
349	}
350}
351
352// drawAll draws every screen and reports whether one waits for the GPU.
353func (ss *screens) drawAll() (pending bool, err error) {
354	for _, s := range ss.byID {
355		p, err := s.draw()
356		if err != nil {
357			return false, fmt.Errorf("output %s: %w", s.name, err)
358		}
359		pending = pending || p
360	}
361	return pending, nil
362}
363
364// closeAll frees every screen after the connection was closed.
365func (ss *screens) closeAll() error {
366	var errs []error
367	for id, s := range ss.byID {
368		delete(ss.byID, id)
369		errs = append(errs, s.closeAfterConn())
370	}
371	return errors.Join(errs...)
372}
373
374// gpuRetry is how soon to render again while a frame waits for the GPU; no
375// event announces that the GPU is done.
376const gpuRetry = 2 * time.Millisecond

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 2

Tags 9

↑↓ moveenter openesc close