detached 63e3cbe

fix(examples): handle output removal, per-screen cursor and focus marker in lock

commit
63e3cbe3a263eb2ca1a7d4a6ff69e13047f8ffb6
author
brice · · 2026-10-01 19:23 UTC
parent
1988d1e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +96 -16
M examples/lock/main.go +96 -16
@@ -12,8 +12,9 @@
1212 // SecretBuffer.Len(). The program never converts the secret to a string and
1313 // never prints it; the buffer is wiped on unlock and on exit.
1414 //
15-// The program prints "locked" and "unlocked" on standard output as progress
16-// markers (the headless test waits for them).
15+// The program prints "locked", "focused" (the keyboard reached a lock surface)
16+// and "unlocked" on standard output as progress markers; the headless test
17+// waits for them. It never prints anything typed.
1718 //
1819 // All logic runs on the owner goroutine, inside or right after Conn.Dispatch.
1920 // The lock needs one surface per output; each gets its own Renderer.
@@ -25,6 +26,7 @@ import (
2526 "fmt"
2627 "os"
2728 "os/signal"
29+ "syscall"
2830 "time"
2931
3032 "github.com/bnema/neferclient"
@@ -33,6 +35,10 @@ import (
3335
3436 const keysymReturn, keysymKPEnter = 0xff0d, 0xff8d // xkb keysyms
3537
38+// guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI use
39+// the same bit order for them, so the plain bits can be copied.
40+const guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper
41+
3642 // model is what the view reads. The secret stays in its buffer; the view only
3743 // asks for its length.
3844 type model struct{ secret *neferclient.SecretBuffer }
@@ -47,6 +53,7 @@ func view(f *nefergui.Frame, m *model) {
4753
4854 // screen is the lock surface of one output and its renderer.
4955 type screen struct {
56+ output uint32 // wl_output registry name (Output.Global)
5057 conn *neferclient.Conn
5158 model *model
5259 surf *neferclient.Surface
@@ -55,6 +62,7 @@ type screen struct {
5562 damage []neferclient.Rect
5663
5764 configured, canPresent, haveAcquire bool
65+ live map[uint64]int // buffer → release eventfd still watched
5866 }
5967
6068 type app struct {
@@ -68,12 +76,13 @@ type app struct {
6876 screens map[neferclient.SurfaceID]*screen
6977
7078 cursor neferclient.CursorShape
79+ hover neferclient.SurfaceID // surface under the pointer, 0 outside
7180 locked, unlocked bool
7281 err error
7382 }
7483
7584 func main() {
76- ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
85+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
7786 defer stop()
7887 if err := run(ctx); err != nil {
7988 fmt.Fprintln(os.Stderr, "lock:", err)
@@ -89,12 +98,15 @@ func run(ctx context.Context) (err error) {
8998 a := &app{conn: conn, screens: map[neferclient.SurfaceID]*screen{}, cursor: neferclient.CursorDefault}
9099 a.secret = neferclient.NewSecretBuffer(256)
91100 a.model.secret = a.secret
101+ // Shutdown order: wipe the secret, close the connection (this drops its
102+ // descriptor watches), then free the renderers and their GPU resources,
103+ // including the release eventfds. The same order as the layer example.
92104 defer func() {
93105 a.secret.Wipe()
106+ err = errors.Join(err, conn.Close())
94107 for _, s := range a.screens {
95108 err = errors.Join(err, s.close())
96109 }
97- err = errors.Join(err, conn.Close())
98110 }()
99111
100112 // The compositor may announce its first output just after Connect.
@@ -105,7 +117,7 @@ func run(ctx context.Context) (err error) {
105117 return fmt.Errorf("dispatch: %w", err)
106118 }
107119 case <-ctx.Done():
108- return ctx.Err()
120+ return nil // interrupt or SIGTERM before the lock: nothing to undo
109121 }
110122 }
111123 if a.lock, err = conn.Lock(); err != nil {
@@ -125,7 +137,10 @@ func run(ctx context.Context) (err error) {
125137 for !a.unlocked {
126138 select {
127139 case <-ctx.Done():
128- return ctx.Err() // the session stays locked: only Unlock ends it
140+ // Interrupt or SIGTERM: a clean exit. The session stays locked,
141+ // because only Lock.Unlock ends a lock; the compositor decides
142+ // what happens to a lock whose client is gone.
143+ return nil
129144 case <-conn.Wake():
130145 if err = conn.Dispatch(a); err != nil {
131146 return fmt.Errorf("dispatch: %w", err)
@@ -144,8 +159,9 @@ func run(ctx context.Context) (err error) {
144159 if err != nil {
145160 return err
146161 }
147- if shown {
148- // The cursor the hovered element asked for → Seat.SetCursor.
162+ // The cursor the hovered element asked for → Seat.SetCursor, only
163+ // from the screen under the pointer.
164+ if shown && s.surf.ID() == a.hover {
149165 a.cursor = cursorShape(s.out.Cursor)
150166 if err = a.seat.SetCursor(a.cursor); err != nil {
151167 return fmt.Errorf("cursor: %w", err)
@@ -166,7 +182,7 @@ func (a *app) addScreen(output uint32) error {
166182 if err != nil {
167183 return fmt.Errorf("lock surface: %w", err)
168184 }
169- a.screens[surf.ID()] = &screen{conn: a.conn, model: &a.model, surf: surf}
185+ a.screens[surf.ID()] = &screen{output: output, conn: a.conn, model: &a.model, surf: surf, live: map[uint64]int{}}
170186 return nil
171187 }
172188
@@ -188,6 +204,23 @@ func (a *app) OutputAdded(out *neferclient.Output) {
188204 }
189205 }
190206
207+// OutputRemoved: stop watching the screen's release fds, free its renderer and
208+// surface, and forget it.
209+func (a *app) OutputRemoved(global uint32) {
210+ for id, s := range a.screens {
211+ if s.output != global {
212+ continue
213+ }
214+ if a.hover == id {
215+ a.hover = 0
216+ }
217+ delete(a.screens, id)
218+ if err := s.release(); err != nil {
219+ a.fail(fmt.Errorf("output %d removed: %w", global, err))
220+ }
221+ }
222+}
223+
191224 func (a *app) Locked() {
192225 a.locked = true
193226 fmt.Println("locked")
@@ -198,17 +231,30 @@ func (a *app) LockFinished() {
198231 }
199232
200233 // FeedbackDone: dmabuf feedback → nefergui.RendererConfig.
234+//
235+// Only the first complete feedback is used. A later one (the compositor
236+// changing its preferred device or formats) is ignored: this demo assumes a
237+// single GPU, and rebuilding the renderers is application policy that would
238+// obscure the glue. A real application would create a new Renderer here and
239+// re-import its buffers.
201240 func (a *app) FeedbackDone(id neferclient.SurfaceID) {
202241 if s := a.screens[id]; s != nil {
203- a.fail(s.setup())
242+ if err := s.setup(); err != nil {
243+ a.fail(err)
244+ }
204245 }
205246 }
206247
207248 func (a *app) Configure(id neferclient.SurfaceID, _, _ int32) {
208249 if s := a.screens[id]; s != nil {
209- s.configured, s.canPresent = true, true
250+ if !s.configured { // later configures must not lift the frame-callback gate
251+ s.canPresent = true
252+ }
253+ s.configured = true
210254 s.resize()
211- a.fail(s.setup())
255+ if err := s.setup(); err != nil {
256+ a.fail(err)
257+ }
212258 }
213259 }
214260
@@ -238,15 +284,26 @@ func (a *app) FDReady(id uint64) {
238284
239285 // Pointer: neferclient events → nefergui.Input, field by field.
240286 func (a *app) Pointer(ev *neferclient.PointerEvent) {
287+ // Track the screen under the pointer first: the cursor shape is applied
288+ // only from it.
289+ switch ev.Kind {
290+ case neferclient.PointerEnter:
291+ a.hover = ev.Surface
292+ // The shape only applies after an enter: send the last one again.
293+ if err := a.seat.SetCursor(a.cursor); err != nil {
294+ a.fail(fmt.Errorf("cursor: %w", err))
295+ }
296+ case neferclient.PointerLeave:
297+ if a.hover == ev.Surface {
298+ a.hover = 0
299+ }
300+ }
241301 s := a.screens[ev.Surface]
242302 if s == nil || s.r == nil {
243303 return
244304 }
245305 in := nefergui.Input{X: ev.X, Y: ev.Y, Kind: nefergui.InputPointerMotion}
246306 switch ev.Kind {
247- case neferclient.PointerEnter:
248- // The shape only applies after an enter: send the last one again.
249- a.fail(a.seat.SetCursor(a.cursor))
250307 case neferclient.PointerLeave:
251308 in.Kind = nefergui.InputPointerLeave
252309 case neferclient.PointerButton:
@@ -274,12 +331,15 @@ func (a *app) Key(ev *neferclient.KeyEvent) {
274331 if s := a.screens[ev.Surface]; s != nil && s.r != nil {
275332 s.r.Input(&nefergui.Input{
276333 Kind: nefergui.InputKey, Keysym: ev.Keysym, Pressed: ev.Pressed, Repeat: ev.Repeat,
277- Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super
334+ Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods),
278335 })
279336 }
280337 }
281338
282339 func (a *app) KeyboardFocus(id neferclient.SurfaceID, focused bool) {
340+ if focused {
341+ fmt.Println("focused")
342+ }
283343 if s := a.screens[id]; s != nil && s.r != nil {
284344 kind := nefergui.InputFocusOut
285345 if focused {
@@ -376,9 +436,14 @@ func (s *screen) present() error {
376436 if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil {
377437 return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
378438 }
439+ delete(s.live, rt.Buffer)
379440 if err := s.surf.DestroyBuffer(rt.Buffer); err != nil {
380441 return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
381442 }
443+ // The release timeline of a buffer is imported under the buffer's id.
444+ if err := s.surf.DestroyTimeline(rt.Buffer); err != nil {
445+ return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err)
446+ }
382447 }
383448 if out.NewBuffer {
384449 buf := neferclient.Buffer{
@@ -396,6 +461,7 @@ func (s *screen) present() error {
396461 if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil {
397462 return fmt.Errorf("watch release fd: %w", err)
398463 }
464+ s.live[out.Buffer] = out.ReleaseFD
399465 }
400466 if out.NewTimelines {
401467 if !s.haveAcquire { // the acquire timeline is shared by every buffer
@@ -428,6 +494,20 @@ func (s *screen) present() error {
428494 return nil
429495 }
430496
497+// release stops watching the live release eventfds, then frees the renderer and
498+// the surface (an output went away). The release eventfds belong to the
499+// renderer, so they are unwatched before it closes.
500+func (s *screen) release() error {
501+ var errs []error
502+ for buffer, fd := range s.live {
503+ if err := s.conn.UnwatchFD(fd); err != nil {
504+ errs = append(errs, fmt.Errorf("unwatch buffer %d: %w", buffer, err))
505+ }
506+ }
507+ clear(s.live)
508+ return errors.Join(append(errs, s.close())...)
509+}
510+
431511 // close frees the renderer and the surface. The session lock itself is only
432512 // ended by Lock.Unlock.
433513 func (s *screen) close() error {
@@ -12,8 +12,9 @@
12 // SecretBuffer.Len(). The program never converts the secret to a string and12 // SecretBuffer.Len(). The program never converts the secret to a string and
13 // never prints it; the buffer is wiped on unlock and on exit.13 // never prints it; the buffer is wiped on unlock and on exit.
14 //14 //
15-// The program prints "locked" and "unlocked" on standard output as progress15+// The program prints "locked", "focused" (the keyboard reached a lock surface)
16-// markers (the headless test waits for them).16+// and "unlocked" on standard output as progress markers; the headless test
17+// waits for them. It never prints anything typed.
17 //18 //
18 // All logic runs on the owner goroutine, inside or right after Conn.Dispatch.19 // All logic runs on the owner goroutine, inside or right after Conn.Dispatch.
19 // The lock needs one surface per output; each gets its own Renderer.20 // The lock needs one surface per output; each gets its own Renderer.
@@ -25,6 +26,7 @@ import (
25 "fmt"26 "fmt"
26 "os"27 "os"
27 "os/signal"28 "os/signal"
29+ "syscall"
28 "time"30 "time"
29 31
30 "github.com/bnema/neferclient"32 "github.com/bnema/neferclient"
@@ -33,6 +35,10 @@ import (
33 35
34 const keysymReturn, keysymKPEnter = 0xff0d, 0xff8d // xkb keysyms36 const keysymReturn, keysymKPEnter = 0xff0d, 0xff8d // xkb keysyms
35 37
38+// guiMods is the set of modifiers NeferGUI knows. neferclient and NeferGUI use
39+// the same bit order for them, so the plain bits can be copied.
40+const guiMods = neferclient.ModShift | neferclient.ModCtrl | neferclient.ModAlt | neferclient.ModSuper
41+
36 // model is what the view reads. The secret stays in its buffer; the view only42 // model is what the view reads. The secret stays in its buffer; the view only
37 // asks for its length.43 // asks for its length.
38 type model struct{ secret *neferclient.SecretBuffer }44 type model struct{ secret *neferclient.SecretBuffer }
@@ -47,6 +53,7 @@ func view(f *nefergui.Frame, m *model) {
47 53
48 // screen is the lock surface of one output and its renderer.54 // screen is the lock surface of one output and its renderer.
49 type screen struct {55 type screen struct {
56+ output uint32 // wl_output registry name (Output.Global)
50 conn *neferclient.Conn57 conn *neferclient.Conn
51 model *model58 model *model
52 surf *neferclient.Surface59 surf *neferclient.Surface
@@ -55,6 +62,7 @@ type screen struct {
55 damage []neferclient.Rect62 damage []neferclient.Rect
56 63
57 configured, canPresent, haveAcquire bool64 configured, canPresent, haveAcquire bool
65+ live map[uint64]int // buffer → release eventfd still watched
58 }66 }
59 67
60 type app struct {68 type app struct {
@@ -68,12 +76,13 @@ type app struct {
68 screens map[neferclient.SurfaceID]*screen76 screens map[neferclient.SurfaceID]*screen
69 77
70 cursor neferclient.CursorShape78 cursor neferclient.CursorShape
79+ hover neferclient.SurfaceID // surface under the pointer, 0 outside
71 locked, unlocked bool80 locked, unlocked bool
72 err error81 err error
73 }82 }
74 83
75 func main() {84 func main() {
76- ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)85+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
77 defer stop()86 defer stop()
78 if err := run(ctx); err != nil {87 if err := run(ctx); err != nil {
79 fmt.Fprintln(os.Stderr, "lock:", err)88 fmt.Fprintln(os.Stderr, "lock:", err)
@@ -89,12 +98,15 @@ func run(ctx context.Context) (err error) {
89 a := &app{conn: conn, screens: map[neferclient.SurfaceID]*screen{}, cursor: neferclient.CursorDefault}98 a := &app{conn: conn, screens: map[neferclient.SurfaceID]*screen{}, cursor: neferclient.CursorDefault}
90 a.secret = neferclient.NewSecretBuffer(256)99 a.secret = neferclient.NewSecretBuffer(256)
91 a.model.secret = a.secret100 a.model.secret = a.secret
101+ // Shutdown order: wipe the secret, close the connection (this drops its
102+ // descriptor watches), then free the renderers and their GPU resources,
103+ // including the release eventfds. The same order as the layer example.
92 defer func() {104 defer func() {
93 a.secret.Wipe()105 a.secret.Wipe()
106+ err = errors.Join(err, conn.Close())
94 for _, s := range a.screens {107 for _, s := range a.screens {
95 err = errors.Join(err, s.close())108 err = errors.Join(err, s.close())
96 }109 }
97- err = errors.Join(err, conn.Close())
98 }()110 }()
99 111
100 // The compositor may announce its first output just after Connect.112 // The compositor may announce its first output just after Connect.
@@ -105,7 +117,7 @@ func run(ctx context.Context) (err error) {
105 return fmt.Errorf("dispatch: %w", err)117 return fmt.Errorf("dispatch: %w", err)
106 }118 }
107 case <-ctx.Done():119 case <-ctx.Done():
108- return ctx.Err()120+ return nil // interrupt or SIGTERM before the lock: nothing to undo
109 }121 }
110 }122 }
111 if a.lock, err = conn.Lock(); err != nil {123 if a.lock, err = conn.Lock(); err != nil {
@@ -125,7 +137,10 @@ func run(ctx context.Context) (err error) {
125 for !a.unlocked {137 for !a.unlocked {
126 select {138 select {
127 case <-ctx.Done():139 case <-ctx.Done():
128- return ctx.Err() // the session stays locked: only Unlock ends it140+ // Interrupt or SIGTERM: a clean exit. The session stays locked,
141+ // because only Lock.Unlock ends a lock; the compositor decides
142+ // what happens to a lock whose client is gone.
143+ return nil
129 case <-conn.Wake():144 case <-conn.Wake():
130 if err = conn.Dispatch(a); err != nil {145 if err = conn.Dispatch(a); err != nil {
131 return fmt.Errorf("dispatch: %w", err)146 return fmt.Errorf("dispatch: %w", err)
@@ -144,8 +159,9 @@ func run(ctx context.Context) (err error) {
144 if err != nil {159 if err != nil {
145 return err160 return err
146 }161 }
147- if shown {162+ // The cursor the hovered element asked for → Seat.SetCursor, only
148- // The cursor the hovered element asked for → Seat.SetCursor.163+ // from the screen under the pointer.
164+ if shown && s.surf.ID() == a.hover {
149 a.cursor = cursorShape(s.out.Cursor)165 a.cursor = cursorShape(s.out.Cursor)
150 if err = a.seat.SetCursor(a.cursor); err != nil {166 if err = a.seat.SetCursor(a.cursor); err != nil {
151 return fmt.Errorf("cursor: %w", err)167 return fmt.Errorf("cursor: %w", err)
@@ -166,7 +182,7 @@ func (a *app) addScreen(output uint32) error {
166 if err != nil {182 if err != nil {
167 return fmt.Errorf("lock surface: %w", err)183 return fmt.Errorf("lock surface: %w", err)
168 }184 }
169- a.screens[surf.ID()] = &screen{conn: a.conn, model: &a.model, surf: surf}185+ a.screens[surf.ID()] = &screen{output: output, conn: a.conn, model: &a.model, surf: surf, live: map[uint64]int{}}
170 return nil186 return nil
171 }187 }
172 188
@@ -188,6 +204,23 @@ func (a *app) OutputAdded(out *neferclient.Output) {
188 }204 }
189 }205 }
190 206
207+// OutputRemoved: stop watching the screen's release fds, free its renderer and
208+// surface, and forget it.
209+func (a *app) OutputRemoved(global uint32) {
210+ for id, s := range a.screens {
211+ if s.output != global {
212+ continue
213+ }
214+ if a.hover == id {
215+ a.hover = 0
216+ }
217+ delete(a.screens, id)
218+ if err := s.release(); err != nil {
219+ a.fail(fmt.Errorf("output %d removed: %w", global, err))
220+ }
221+ }
222+}
223+
191 func (a *app) Locked() {224 func (a *app) Locked() {
192 a.locked = true225 a.locked = true
193 fmt.Println("locked")226 fmt.Println("locked")
@@ -198,17 +231,30 @@ func (a *app) LockFinished() {
198 }231 }
199 232
200 // FeedbackDone: dmabuf feedback → nefergui.RendererConfig.233 // FeedbackDone: dmabuf feedback → nefergui.RendererConfig.
234+//
235+// Only the first complete feedback is used. A later one (the compositor
236+// changing its preferred device or formats) is ignored: this demo assumes a
237+// single GPU, and rebuilding the renderers is application policy that would
238+// obscure the glue. A real application would create a new Renderer here and
239+// re-import its buffers.
201 func (a *app) FeedbackDone(id neferclient.SurfaceID) {240 func (a *app) FeedbackDone(id neferclient.SurfaceID) {
202 if s := a.screens[id]; s != nil {241 if s := a.screens[id]; s != nil {
203- a.fail(s.setup())242+ if err := s.setup(); err != nil {
243+ a.fail(err)
244+ }
204 }245 }
205 }246 }
206 247
207 func (a *app) Configure(id neferclient.SurfaceID, _, _ int32) {248 func (a *app) Configure(id neferclient.SurfaceID, _, _ int32) {
208 if s := a.screens[id]; s != nil {249 if s := a.screens[id]; s != nil {
209- s.configured, s.canPresent = true, true250+ if !s.configured { // later configures must not lift the frame-callback gate
251+ s.canPresent = true
252+ }
253+ s.configured = true
210 s.resize()254 s.resize()
211- a.fail(s.setup())255+ if err := s.setup(); err != nil {
256+ a.fail(err)
257+ }
212 }258 }
213 }259 }
214 260
@@ -238,15 +284,26 @@ func (a *app) FDReady(id uint64) {
238 284
239 // Pointer: neferclient events → nefergui.Input, field by field.285 // Pointer: neferclient events → nefergui.Input, field by field.
240 func (a *app) Pointer(ev *neferclient.PointerEvent) {286 func (a *app) Pointer(ev *neferclient.PointerEvent) {
287+ // Track the screen under the pointer first: the cursor shape is applied
288+ // only from it.
289+ switch ev.Kind {
290+ case neferclient.PointerEnter:
291+ a.hover = ev.Surface
292+ // The shape only applies after an enter: send the last one again.
293+ if err := a.seat.SetCursor(a.cursor); err != nil {
294+ a.fail(fmt.Errorf("cursor: %w", err))
295+ }
296+ case neferclient.PointerLeave:
297+ if a.hover == ev.Surface {
298+ a.hover = 0
299+ }
300+ }
241 s := a.screens[ev.Surface]301 s := a.screens[ev.Surface]
242 if s == nil || s.r == nil {302 if s == nil || s.r == nil {
243 return303 return
244 }304 }
245 in := nefergui.Input{X: ev.X, Y: ev.Y, Kind: nefergui.InputPointerMotion}305 in := nefergui.Input{X: ev.X, Y: ev.Y, Kind: nefergui.InputPointerMotion}
246 switch ev.Kind {306 switch ev.Kind {
247- case neferclient.PointerEnter:
248- // The shape only applies after an enter: send the last one again.
249- a.fail(a.seat.SetCursor(a.cursor))
250 case neferclient.PointerLeave:307 case neferclient.PointerLeave:
251 in.Kind = nefergui.InputPointerLeave308 in.Kind = nefergui.InputPointerLeave
252 case neferclient.PointerButton:309 case neferclient.PointerButton:
@@ -274,12 +331,15 @@ func (a *app) Key(ev *neferclient.KeyEvent) {
274 if s := a.screens[ev.Surface]; s != nil && s.r != nil {331 if s := a.screens[ev.Surface]; s != nil && s.r != nil {
275 s.r.Input(&nefergui.Input{332 s.r.Input(&nefergui.Input{
276 Kind: nefergui.InputKey, Keysym: ev.Keysym, Pressed: ev.Pressed, Repeat: ev.Repeat,333 Kind: nefergui.InputKey, Keysym: ev.Keysym, Pressed: ev.Pressed, Repeat: ev.Repeat,
277- Modifiers: nefergui.Modifiers(ev.Modifiers & 0x0f), // shift, ctrl, alt, super334+ Modifiers: nefergui.Modifiers(ev.Modifiers & guiMods),
278 })335 })
279 }336 }
280 }337 }
281 338
282 func (a *app) KeyboardFocus(id neferclient.SurfaceID, focused bool) {339 func (a *app) KeyboardFocus(id neferclient.SurfaceID, focused bool) {
340+ if focused {
341+ fmt.Println("focused")
342+ }
283 if s := a.screens[id]; s != nil && s.r != nil {343 if s := a.screens[id]; s != nil && s.r != nil {
284 kind := nefergui.InputFocusOut344 kind := nefergui.InputFocusOut
285 if focused {345 if focused {
@@ -376,9 +436,14 @@ func (s *screen) present() error {
376 if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil {436 if err := s.conn.UnwatchFD(rt.ReleaseFD); err != nil {
377 return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)437 return fmt.Errorf("unwatch retired buffer %d: %w", rt.Buffer, err)
378 }438 }
439+ delete(s.live, rt.Buffer)
379 if err := s.surf.DestroyBuffer(rt.Buffer); err != nil {440 if err := s.surf.DestroyBuffer(rt.Buffer); err != nil {
380 return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)441 return fmt.Errorf("destroy retired buffer %d: %w", rt.Buffer, err)
381 }442 }
443+ // The release timeline of a buffer is imported under the buffer's id.
444+ if err := s.surf.DestroyTimeline(rt.Buffer); err != nil {
445+ return fmt.Errorf("destroy retired timeline %d: %w", rt.Buffer, err)
446+ }
382 }447 }
383 if out.NewBuffer {448 if out.NewBuffer {
384 buf := neferclient.Buffer{449 buf := neferclient.Buffer{
@@ -396,6 +461,7 @@ func (s *screen) present() error {
396 if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil {461 if err := s.conn.WatchFD(out.ReleaseFD, s.watchID(out.Buffer)); err != nil {
397 return fmt.Errorf("watch release fd: %w", err)462 return fmt.Errorf("watch release fd: %w", err)
398 }463 }
464+ s.live[out.Buffer] = out.ReleaseFD
399 }465 }
400 if out.NewTimelines {466 if out.NewTimelines {
401 if !s.haveAcquire { // the acquire timeline is shared by every buffer467 if !s.haveAcquire { // the acquire timeline is shared by every buffer
@@ -428,6 +494,20 @@ func (s *screen) present() error {
428 return nil494 return nil
429 }495 }
430 496
497+// release stops watching the live release eventfds, then frees the renderer and
498+// the surface (an output went away). The release eventfds belong to the
499+// renderer, so they are unwatched before it closes.
500+func (s *screen) release() error {
501+ var errs []error
502+ for buffer, fd := range s.live {
503+ if err := s.conn.UnwatchFD(fd); err != nil {
504+ errs = append(errs, fmt.Errorf("unwatch buffer %d: %w", buffer, err))
505+ }
506+ }
507+ clear(s.live)
508+ return errors.Join(append(errs, s.close())...)
509+}
510+
431 // close frees the renderer and the surface. The session lock itself is only511 // close frees the renderer and the surface. The session lock itself is only
432 // ended by Lock.Unlock.512 // ended by Lock.Unlock.
433 func (s *screen) close() error {513 func (s *screen) close() error {

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