detached 5e501a1

feat(examples): layer surface counter button with NeferGUI

commit
5e501a1d527b0c98ae82bab2c3ef8e4daac56cc3
author
brice · · 2026-10-01 19:13 UTC
parent
8d3baba
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +364 -0
A examples/doc.go +4 -0
@@ -0,0 +1,4 @@
1+// Package examples holds the neferclient and NeferGUI example programs
2+// (layer, lock) and their headless tests. The two libraries never import each
3+// other: each program copies plain fields between them.
4+package examples
@@ -0,0 +1,4 @@
1+// Package examples holds the neferclient and NeferGUI example programs
2+// (layer, lock) and their headless tests. The two libraries never import each
3+// other: each program copies plain fields between them.
4+package examples
A examples/go.mod +8 -0
@@ -0,0 +1,8 @@
1+module github.com/bnema/neferclient/examples
2+
3+go 1.27
4+
5+require (
6+ github.com/bnema/neferclient v0.1.0
7+ github.com/bnema/nefergui v0.5.0
8+)
@@ -0,0 +1,8 @@
1+module github.com/bnema/neferclient/examples
2+
3+go 1.27
4+
5+require (
6+ github.com/bnema/neferclient v0.1.0
7+ github.com/bnema/nefergui v0.5.0
8+)
A examples/layer/main.go +352 -0
@@ -0,0 +1,352 @@
1+// Command layer shows a wlr-layer-shell surface with a counter button: the
2+// smallest end-to-end use of neferclient (Wayland client) and NeferGUI
3+// (renderer). The two libraries do not import each other; this file is the
4+// glue, and every step is marked.
5+//
6+// Everything runs on one owner goroutine: the loop in run waits for a wake-up,
7+// calls Conn.Dispatch, and all Handler methods, renderer calls and Present
8+// calls happen inside that call or right after it.
9+//
10+// With -frames N the program redraws continuously and exits after N frames
11+// (used by the headless test); without it, it runs until the surface is closed
12+// or it is interrupted.
13+package main
14+
15+import (
16+ "context"
17+ "errors"
18+ "flag"
19+ "fmt"
20+ "os"
21+ "os/signal"
22+ "time"
23+
24+ "github.com/bnema/neferclient"
25+ "github.com/bnema/nefergui"
26+)
27+
28+// Layout of the surface, in logical pixels. The headless test samples a pixel
29+// inside the button at the same place.
30+const (
31+ surfaceW, surfaceH = 240, 90
32+ buttonColor = "#2060c0"
33+)
34+
35+type model struct{ count int }
36+
37+// view builds one frame from the model. Button.Activated is true during the
38+// frame built after a click.
39+func view(f *nefergui.Frame, m *model) {
40+ root := f.Root().Column(nefergui.Inline("padding:10px;gap:10px;color:#ffffff"))
41+ root.TextInt("Count: ", int64(m.count), nefergui.Inline("height:20px"))
42+ if root.Button("Add one", nefergui.Key("add"),
43+ nefergui.Inline("width:200px;height:40px;background:"+buttonColor)).Activated() {
44+ m.count++
45+ }
46+}
47+
48+type app struct {
49+ neferclient.NopHandler // methods this program does not need
50+
51+ conn *neferclient.Conn
52+ surf *neferclient.Surface
53+ seat *neferclient.Seat
54+
55+ r *nefergui.Renderer
56+ rwake <-chan struct{} // Renderer.Wake: nil until the renderer exists
57+ out nefergui.Output
58+ model model
59+ damage []neferclient.Rect
60+
61+ configured bool
62+ canPresent bool // configured, and the last frame callback fired
63+ haveAcquire bool
64+ cursor neferclient.CursorShape
65+
66+ frames, shown int // -frames limit and frames acknowledged so far
67+ done bool
68+ err error
69+}
70+
71+func main() {
72+ frames := flag.Int("frames", 0, "redraw continuously and exit after this many frames (0: run until closed)")
73+ flag.Parse()
74+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
75+ defer stop()
76+ if err := run(ctx, *frames); err != nil {
77+ fmt.Fprintln(os.Stderr, "layer:", err)
78+ os.Exit(1)
79+ }
80+}
81+
82+func run(ctx context.Context, frames int) (err error) {
83+ conn, err := neferclient.Connect(ctx, "") // WAYLAND_DISPLAY
84+ if err != nil {
85+ return fmt.Errorf("connect: %w", err)
86+ }
87+ a := &app{conn: conn, frames: frames, cursor: neferclient.CursorDefault}
88+ defer func() {
89+ if a.r != nil {
90+ err = errors.Join(err, a.r.Close())
91+ }
92+ err = errors.Join(err, conn.Close())
93+ }()
94+ a.surf, err = conn.NewLayerSurface(neferclient.LayerConfig{
95+ Level: neferclient.LayerTop,
96+ Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
97+ Width: surfaceW,
98+ Height: surfaceH,
99+ Keyboard: neferclient.KeyboardOnDemand,
100+ })
101+ if err != nil {
102+ return fmt.Errorf("layer surface: %w", err)
103+ }
104+ a.seat = conn.Seat()
105+
106+ var tick <-chan time.Time
107+ for !a.done {
108+ select {
109+ case <-ctx.Done():
110+ return nil
111+ case <-conn.Wake():
112+ if err = conn.Dispatch(a); err != nil {
113+ return fmt.Errorf("dispatch: %w", err)
114+ }
115+ case <-a.rwake: // another goroutine asked for a frame
116+ case <-tick: // a built frame was waiting for the GPU
117+ }
118+ if a.err != nil {
119+ return a.err
120+ }
121+ if err = a.draw(); err != nil {
122+ return err
123+ }
124+ tick = nil
125+ if a.r != nil && a.r.Pending() {
126+ tick = time.After(2 * time.Millisecond)
127+ }
128+ }
129+ return nil
130+}
131+
132+// FeedbackDone: dmabuf feedback → nefergui.RendererConfig. The renderer opens
133+// the compositor's main device and picks a format and modifier from the list.
134+func (a *app) FeedbackDone(neferclient.SurfaceID) { a.setup() }
135+
136+func (a *app) Configure(neferclient.SurfaceID, int32, int32) {
137+ a.configured, a.canPresent = true, true
138+ a.resize()
139+ a.setup()
140+}
141+
142+func (a *app) Scale(neferclient.SurfaceID, float64) { a.resize() }
143+
144+func (a *app) setup() {
145+ fb := a.surf.Feedback()
146+ if a.r != nil || !a.configured || fb == nil {
147+ return
148+ }
149+ cfg := nefergui.RendererConfig{MainDevice: fb.MainDevice, Formats: make([]nefergui.Format, len(fb.Formats))}
150+ for i, f := range fb.Formats {
151+ cfg.Formats[i] = nefergui.Format{FourCC: f.FourCC, Modifier: f.Modifier}
152+ }
153+ r, err := nefergui.NewRenderer(cfg)
154+ if err != nil {
155+ a.fail(fmt.Errorf("renderer: %w", err))
156+ return
157+ }
158+ a.r, a.rwake = r, r.Wake()
159+ a.resize()
160+}
161+
162+// resize forwards the logical size and scale to the renderer.
163+func (a *app) resize() {
164+ if a.r == nil {
165+ return
166+ }
167+ w, h, scale := a.surf.Size()
168+ a.r.Resize(int(w), int(h), scale)
169+}
170+
171+// Frame: the compositor showed the last commit; the next Present is allowed.
172+func (a *app) Frame(neferclient.SurfaceID) {
173+ a.canPresent = true
174+ a.shown++
175+ if a.frames > 0 {
176+ if a.shown >= a.frames {
177+ a.done = true
178+ return
179+ }
180+ a.r.Invalidate()
181+ }
182+}
183+
184+func (a *app) Closed(neferclient.SurfaceID) { a.done = true }
185+
186+func (a *app) Error(err error) { a.fail(err) }
187+
188+func (a *app) fail(err error) {
189+ if a.err == nil {
190+ a.err = err
191+ }
192+}
193+
194+// FDReady: the release eventfd of a buffer is readable (watched with
195+// Conn.WatchFD in present, id is the buffer): hand the buffer back.
196+func (a *app) FDReady(id uint64) {
197+ if err := a.r.Released(id); err != nil {
198+ a.fail(fmt.Errorf("release buffer %d: %w", id, err))
199+ }
200+}
201+
202+// Pointer and Key: neferclient events → nefergui.Input, field by field.
203+func (a *app) Pointer(ev *neferclient.PointerEvent) {
204+ if a.r == nil {
205+ return
206+ }
207+ in := nefergui.Input{X: ev.X, Y: ev.Y}
208+ switch ev.Kind {
209+ case neferclient.PointerEnter:
210+ in.Kind = nefergui.InputPointerMotion
211+ // The cursor shape is only valid after the enter: set the shape of
212+ // the last frame again.
213+ if err := a.seat.SetCursor(a.cursor); err != nil {
214+ a.fail(fmt.Errorf("cursor: %w", err))
215+ }
216+ case neferclient.PointerMotion:
217+ in.Kind = nefergui.InputPointerMotion
218+ case neferclient.PointerLeave:
219+ in.Kind = nefergui.InputPointerLeave
220+ case neferclient.PointerButton:
221+ in.Kind, in.Button = nefergui.InputPointerRelease, ev.Button
222+ if ev.Pressed {
223+ in.Kind = nefergui.InputPointerPress
224+ }
225+ case neferclient.PointerAxis:
226+ in.Kind, in.DX, in.DY = nefergui.InputPointerAxis, ev.DX, ev.DY
227+ default:
228+ return
229+ }
230+ a.r.Input(&in)
231+}
232+
233+func (a *app) Key(ev *neferclient.KeyEvent) {
234+ if a.r == nil {
235+ return
236+ }
237+ a.r.Input(&nefergui.Input{
238+ Kind: nefergui.InputKey,
239+ Keysym: ev.Keysym,
240+ Text: ev.Text, // valid only during the call, as for Input
241+ Pressed: ev.Pressed,
242+ Repeat: ev.Repeat,
243+ Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super
244+ })
245+}
246+
247+func (a *app) KeyboardFocus(_ neferclient.SurfaceID, focused bool) {
248+ if a.r == nil {
249+ return
250+ }
251+ kind := nefergui.InputFocusOut
252+ if focused {
253+ kind = nefergui.InputFocusIn
254+ }
255+ a.r.Input(&nefergui.Input{Kind: kind})
256+}
257+
258+// draw renders a frame when something changed and presents it.
259+func (a *app) draw() error {
260+ if a.r == nil || !a.canPresent {
261+ return nil
262+ }
263+ ok, err := a.r.Render(&a.out, &a.model, view)
264+ if err != nil {
265+ return fmt.Errorf("render: %w", err)
266+ }
267+ if !ok {
268+ return nil
269+ }
270+ if err = a.present(); err != nil {
271+ return err
272+ }
273+ return a.setCursor()
274+}
275+
276+// present: Renderer.Render output → ImportBuffer, ImportTimeline, Present.
277+func (a *app) present() error {
278+ out := &a.out
279+ // A resize retired buffers: stop watching and destroy them first.
280+ for _, rt := range out.Retired {
281+ if err := a.conn.UnwatchFD(rt.ReleaseFD); err != nil {
282+ return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
283+ }
284+ if err := a.surf.DestroyBuffer(rt.Buffer); err != nil {
285+ return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
286+ }
287+ }
288+ if out.NewBuffer {
289+ buf := neferclient.Buffer{
290+ Width: out.Width, Height: out.Height, FourCC: out.FourCC, Modifier: out.Modifier,
291+ PlaneCount: out.PlaneCount,
292+ }
293+ for i, p := range out.Planes {
294+ buf.Planes[i] = neferclient.Plane{FD: p.FD, Offset: p.Offset, Stride: p.Stride}
295+ }
296+ if err := a.surf.ImportBuffer(out.Buffer, &buf); err != nil {
297+ return fmt.Errorf("import buffer: %w", err)
298+ }
299+ // The release eventfd is stable per buffer: watch it once; FDReady
300+ // calls Renderer.Released.
301+ if err := a.conn.WatchFD(out.ReleaseFD, out.Buffer); err != nil {
302+ return fmt.Errorf("watch release fd: %w", err)
303+ }
304+ }
305+ if out.NewTimelines {
306+ if !a.haveAcquire { // the acquire timeline is shared by every buffer
307+ if err := a.surf.ImportTimeline(out.Acquire.ID, out.Acquire.FD); err != nil {
308+ return fmt.Errorf("import acquire timeline: %w", err)
309+ }
310+ a.haveAcquire = true
311+ }
312+ if err := a.surf.ImportTimeline(out.Release.ID, out.Release.FD); err != nil {
313+ return fmt.Errorf("import release timeline: %w", err)
314+ }
315+ }
316+ a.damage = a.damage[:0]
317+ for _, d := range out.Damage {
318+ a.damage = append(a.damage, neferclient.Rect{X: d.X, Y: d.Y, Width: d.Width, Height: d.Height})
319+ }
320+ err := a.surf.Present(&neferclient.Present{
321+ Buffer: out.Buffer,
322+ AcquireTimeline: out.Acquire.ID,
323+ ReleaseTimeline: out.Release.ID,
324+ AcquirePoint: out.AcquirePoint,
325+ ReleasePoint: out.ReleasePoint,
326+ Damage: a.damage,
327+ Opaque: true, // XRGB8888: RendererConfig.Transparent is false
328+ })
329+ if err != nil {
330+ return fmt.Errorf("present: %w", err)
331+ }
332+ a.canPresent = false // until Frame
333+ return nil
334+}
335+
336+// setCursor: the cursor the hovered element asked for → Seat.SetCursor.
337+func (a *app) setCursor() error {
338+ switch a.out.Cursor {
339+ case nefergui.CursorPointer:
340+ a.cursor = neferclient.CursorPointer
341+ case nefergui.CursorText:
342+ a.cursor = neferclient.CursorText
343+ case nefergui.CursorNotAllowed:
344+ a.cursor = neferclient.CursorNotAllowed
345+ default:
346+ a.cursor = neferclient.CursorDefault
347+ }
348+ if err := a.seat.SetCursor(a.cursor); err != nil {
349+ return fmt.Errorf("cursor: %w", err)
350+ }
351+ return nil
352+}
@@ -0,0 +1,352 @@
1+// Command layer shows a wlr-layer-shell surface with a counter button: the
2+// smallest end-to-end use of neferclient (Wayland client) and NeferGUI
3+// (renderer). The two libraries do not import each other; this file is the
4+// glue, and every step is marked.
5+//
6+// Everything runs on one owner goroutine: the loop in run waits for a wake-up,
7+// calls Conn.Dispatch, and all Handler methods, renderer calls and Present
8+// calls happen inside that call or right after it.
9+//
10+// With -frames N the program redraws continuously and exits after N frames
11+// (used by the headless test); without it, it runs until the surface is closed
12+// or it is interrupted.
13+package main
14+
15+import (
16+ "context"
17+ "errors"
18+ "flag"
19+ "fmt"
20+ "os"
21+ "os/signal"
22+ "time"
23+
24+ "github.com/bnema/neferclient"
25+ "github.com/bnema/nefergui"
26+)
27+
28+// Layout of the surface, in logical pixels. The headless test samples a pixel
29+// inside the button at the same place.
30+const (
31+ surfaceW, surfaceH = 240, 90
32+ buttonColor = "#2060c0"
33+)
34+
35+type model struct{ count int }
36+
37+// view builds one frame from the model. Button.Activated is true during the
38+// frame built after a click.
39+func view(f *nefergui.Frame, m *model) {
40+ root := f.Root().Column(nefergui.Inline("padding:10px;gap:10px;color:#ffffff"))
41+ root.TextInt("Count: ", int64(m.count), nefergui.Inline("height:20px"))
42+ if root.Button("Add one", nefergui.Key("add"),
43+ nefergui.Inline("width:200px;height:40px;background:"+buttonColor)).Activated() {
44+ m.count++
45+ }
46+}
47+
48+type app struct {
49+ neferclient.NopHandler // methods this program does not need
50+
51+ conn *neferclient.Conn
52+ surf *neferclient.Surface
53+ seat *neferclient.Seat
54+
55+ r *nefergui.Renderer
56+ rwake <-chan struct{} // Renderer.Wake: nil until the renderer exists
57+ out nefergui.Output
58+ model model
59+ damage []neferclient.Rect
60+
61+ configured bool
62+ canPresent bool // configured, and the last frame callback fired
63+ haveAcquire bool
64+ cursor neferclient.CursorShape
65+
66+ frames, shown int // -frames limit and frames acknowledged so far
67+ done bool
68+ err error
69+}
70+
71+func main() {
72+ frames := flag.Int("frames", 0, "redraw continuously and exit after this many frames (0: run until closed)")
73+ flag.Parse()
74+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
75+ defer stop()
76+ if err := run(ctx, *frames); err != nil {
77+ fmt.Fprintln(os.Stderr, "layer:", err)
78+ os.Exit(1)
79+ }
80+}
81+
82+func run(ctx context.Context, frames int) (err error) {
83+ conn, err := neferclient.Connect(ctx, "") // WAYLAND_DISPLAY
84+ if err != nil {
85+ return fmt.Errorf("connect: %w", err)
86+ }
87+ a := &app{conn: conn, frames: frames, cursor: neferclient.CursorDefault}
88+ defer func() {
89+ if a.r != nil {
90+ err = errors.Join(err, a.r.Close())
91+ }
92+ err = errors.Join(err, conn.Close())
93+ }()
94+ a.surf, err = conn.NewLayerSurface(neferclient.LayerConfig{
95+ Level: neferclient.LayerTop,
96+ Anchors: neferclient.AnchorTop | neferclient.AnchorLeft,
97+ Width: surfaceW,
98+ Height: surfaceH,
99+ Keyboard: neferclient.KeyboardOnDemand,
100+ })
101+ if err != nil {
102+ return fmt.Errorf("layer surface: %w", err)
103+ }
104+ a.seat = conn.Seat()
105+
106+ var tick <-chan time.Time
107+ for !a.done {
108+ select {
109+ case <-ctx.Done():
110+ return nil
111+ case <-conn.Wake():
112+ if err = conn.Dispatch(a); err != nil {
113+ return fmt.Errorf("dispatch: %w", err)
114+ }
115+ case <-a.rwake: // another goroutine asked for a frame
116+ case <-tick: // a built frame was waiting for the GPU
117+ }
118+ if a.err != nil {
119+ return a.err
120+ }
121+ if err = a.draw(); err != nil {
122+ return err
123+ }
124+ tick = nil
125+ if a.r != nil && a.r.Pending() {
126+ tick = time.After(2 * time.Millisecond)
127+ }
128+ }
129+ return nil
130+}
131+
132+// FeedbackDone: dmabuf feedback → nefergui.RendererConfig. The renderer opens
133+// the compositor's main device and picks a format and modifier from the list.
134+func (a *app) FeedbackDone(neferclient.SurfaceID) { a.setup() }
135+
136+func (a *app) Configure(neferclient.SurfaceID, int32, int32) {
137+ a.configured, a.canPresent = true, true
138+ a.resize()
139+ a.setup()
140+}
141+
142+func (a *app) Scale(neferclient.SurfaceID, float64) { a.resize() }
143+
144+func (a *app) setup() {
145+ fb := a.surf.Feedback()
146+ if a.r != nil || !a.configured || fb == nil {
147+ return
148+ }
149+ cfg := nefergui.RendererConfig{MainDevice: fb.MainDevice, Formats: make([]nefergui.Format, len(fb.Formats))}
150+ for i, f := range fb.Formats {
151+ cfg.Formats[i] = nefergui.Format{FourCC: f.FourCC, Modifier: f.Modifier}
152+ }
153+ r, err := nefergui.NewRenderer(cfg)
154+ if err != nil {
155+ a.fail(fmt.Errorf("renderer: %w", err))
156+ return
157+ }
158+ a.r, a.rwake = r, r.Wake()
159+ a.resize()
160+}
161+
162+// resize forwards the logical size and scale to the renderer.
163+func (a *app) resize() {
164+ if a.r == nil {
165+ return
166+ }
167+ w, h, scale := a.surf.Size()
168+ a.r.Resize(int(w), int(h), scale)
169+}
170+
171+// Frame: the compositor showed the last commit; the next Present is allowed.
172+func (a *app) Frame(neferclient.SurfaceID) {
173+ a.canPresent = true
174+ a.shown++
175+ if a.frames > 0 {
176+ if a.shown >= a.frames {
177+ a.done = true
178+ return
179+ }
180+ a.r.Invalidate()
181+ }
182+}
183+
184+func (a *app) Closed(neferclient.SurfaceID) { a.done = true }
185+
186+func (a *app) Error(err error) { a.fail(err) }
187+
188+func (a *app) fail(err error) {
189+ if a.err == nil {
190+ a.err = err
191+ }
192+}
193+
194+// FDReady: the release eventfd of a buffer is readable (watched with
195+// Conn.WatchFD in present, id is the buffer): hand the buffer back.
196+func (a *app) FDReady(id uint64) {
197+ if err := a.r.Released(id); err != nil {
198+ a.fail(fmt.Errorf("release buffer %d: %w", id, err))
199+ }
200+}
201+
202+// Pointer and Key: neferclient events → nefergui.Input, field by field.
203+func (a *app) Pointer(ev *neferclient.PointerEvent) {
204+ if a.r == nil {
205+ return
206+ }
207+ in := nefergui.Input{X: ev.X, Y: ev.Y}
208+ switch ev.Kind {
209+ case neferclient.PointerEnter:
210+ in.Kind = nefergui.InputPointerMotion
211+ // The cursor shape is only valid after the enter: set the shape of
212+ // the last frame again.
213+ if err := a.seat.SetCursor(a.cursor); err != nil {
214+ a.fail(fmt.Errorf("cursor: %w", err))
215+ }
216+ case neferclient.PointerMotion:
217+ in.Kind = nefergui.InputPointerMotion
218+ case neferclient.PointerLeave:
219+ in.Kind = nefergui.InputPointerLeave
220+ case neferclient.PointerButton:
221+ in.Kind, in.Button = nefergui.InputPointerRelease, ev.Button
222+ if ev.Pressed {
223+ in.Kind = nefergui.InputPointerPress
224+ }
225+ case neferclient.PointerAxis:
226+ in.Kind, in.DX, in.DY = nefergui.InputPointerAxis, ev.DX, ev.DY
227+ default:
228+ return
229+ }
230+ a.r.Input(&in)
231+}
232+
233+func (a *app) Key(ev *neferclient.KeyEvent) {
234+ if a.r == nil {
235+ return
236+ }
237+ a.r.Input(&nefergui.Input{
238+ Kind: nefergui.InputKey,
239+ Keysym: ev.Keysym,
240+ Text: ev.Text, // valid only during the call, as for Input
241+ Pressed: ev.Pressed,
242+ Repeat: ev.Repeat,
243+ Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super
244+ })
245+}
246+
247+func (a *app) KeyboardFocus(_ neferclient.SurfaceID, focused bool) {
248+ if a.r == nil {
249+ return
250+ }
251+ kind := nefergui.InputFocusOut
252+ if focused {
253+ kind = nefergui.InputFocusIn
254+ }
255+ a.r.Input(&nefergui.Input{Kind: kind})
256+}
257+
258+// draw renders a frame when something changed and presents it.
259+func (a *app) draw() error {
260+ if a.r == nil || !a.canPresent {
261+ return nil
262+ }
263+ ok, err := a.r.Render(&a.out, &a.model, view)
264+ if err != nil {
265+ return fmt.Errorf("render: %w", err)
266+ }
267+ if !ok {
268+ return nil
269+ }
270+ if err = a.present(); err != nil {
271+ return err
272+ }
273+ return a.setCursor()
274+}
275+
276+// present: Renderer.Render output → ImportBuffer, ImportTimeline, Present.
277+func (a *app) present() error {
278+ out := &a.out
279+ // A resize retired buffers: stop watching and destroy them first.
280+ for _, rt := range out.Retired {
281+ if err := a.conn.UnwatchFD(rt.ReleaseFD); err != nil {
282+ return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
283+ }
284+ if err := a.surf.DestroyBuffer(rt.Buffer); err != nil {
285+ return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
286+ }
287+ }
288+ if out.NewBuffer {
289+ buf := neferclient.Buffer{
290+ Width: out.Width, Height: out.Height, FourCC: out.FourCC, Modifier: out.Modifier,
291+ PlaneCount: out.PlaneCount,
292+ }
293+ for i, p := range out.Planes {
294+ buf.Planes[i] = neferclient.Plane{FD: p.FD, Offset: p.Offset, Stride: p.Stride}
295+ }
296+ if err := a.surf.ImportBuffer(out.Buffer, &buf); err != nil {
297+ return fmt.Errorf("import buffer: %w", err)
298+ }
299+ // The release eventfd is stable per buffer: watch it once; FDReady
300+ // calls Renderer.Released.
301+ if err := a.conn.WatchFD(out.ReleaseFD, out.Buffer); err != nil {
302+ return fmt.Errorf("watch release fd: %w", err)
303+ }
304+ }
305+ if out.NewTimelines {
306+ if !a.haveAcquire { // the acquire timeline is shared by every buffer
307+ if err := a.surf.ImportTimeline(out.Acquire.ID, out.Acquire.FD); err != nil {
308+ return fmt.Errorf("import acquire timeline: %w", err)
309+ }
310+ a.haveAcquire = true
311+ }
312+ if err := a.surf.ImportTimeline(out.Release.ID, out.Release.FD); err != nil {
313+ return fmt.Errorf("import release timeline: %w", err)
314+ }
315+ }
316+ a.damage = a.damage[:0]
317+ for _, d := range out.Damage {
318+ a.damage = append(a.damage, neferclient.Rect{X: d.X, Y: d.Y, Width: d.Width, Height: d.Height})
319+ }
320+ err := a.surf.Present(&neferclient.Present{
321+ Buffer: out.Buffer,
322+ AcquireTimeline: out.Acquire.ID,
323+ ReleaseTimeline: out.Release.ID,
324+ AcquirePoint: out.AcquirePoint,
325+ ReleasePoint: out.ReleasePoint,
326+ Damage: a.damage,
327+ Opaque: true, // XRGB8888: RendererConfig.Transparent is false
328+ })
329+ if err != nil {
330+ return fmt.Errorf("present: %w", err)
331+ }
332+ a.canPresent = false // until Frame
333+ return nil
334+}
335+
336+// setCursor: the cursor the hovered element asked for → Seat.SetCursor.
337+func (a *app) setCursor() error {
338+ switch a.out.Cursor {
339+ case nefergui.CursorPointer:
340+ a.cursor = neferclient.CursorPointer
341+ case nefergui.CursorText:
342+ a.cursor = neferclient.CursorText
343+ case nefergui.CursorNotAllowed:
344+ a.cursor = neferclient.CursorNotAllowed
345+ default:
346+ a.cursor = neferclient.CursorDefault
347+ }
348+ if err := a.seat.SetCursor(a.cursor); err != nil {
349+ return fmt.Errorf("cursor: %w", err)
350+ }
351+ return nil
352+}

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