detached c2ef3a9

fix(wayland): enforce layer role rules and xdg-output done semantics

commit
c2ef3a907c7534c039966ead0f3879a975e972a7
author
brice · · 2026-09-23 16:19 UTC
parent
aba8c9f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +104 -13
M internal/adapters/wayland/layer.go +12 -10
@@ -31,11 +31,7 @@ type layerSurface struct {
3131 serials []uint32
3232 }
3333
34-// Layer objects belong to the display goroutine, like xdg windows.
35-var layerSets = map[*Server]map[ports.WindowID]*layerSurface{}
36-
3734 func registerLayer(d *server.Display, o Options, s *Server) error {
38- layerSets[s] = make(map[ports.WindowID]*layerSurface)
3935 return wlrlayershell.NewZwlrLayerShellV1Global(d, 4, func(c server.Client, v, id uint32) {
4036 _, _ = wlrlayershell.NewZwlrLayerShellV1(c, int32(v), id, layerShell{s, uint32(o.OutputWidth), uint32(o.OutputHeight)})
4137 })
@@ -46,8 +42,8 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
4642 return
4743 }
4844 state := h.server.surfaces[w.Resource]
49- if state.kind != roleNone {
50- r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorRole), "surface already has role")
45+ if state.kind != roleNone || state.attached || state.current != nil || state.committed {
46+ r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")
5147 return
5248 }
5349 if layer > 3 {
@@ -65,10 +61,10 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
6561 state.kind = roleLayer
6662 state.layer = l
6763 state.role = l.commit
68- layerSets[h.server][l.id] = l
64+ h.server.layers[l.id] = l
6965 resource.OnDestroy = func() {
7066 l.unmap()
71- delete(layerSets[h.server], l.id)
67+ delete(h.server.layers, l.id)
7268 state.layer = nil
7369 state.role = nil
7470 state.current, state.pending = nil, nil
@@ -91,6 +87,8 @@ func (l *layerSurface) SetExclusiveZone(_ *wlrlayershell.ZwlrLayerSurfaceV1, z i
9187 func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, left int32) {
9288 l.pending.margin = [4]int32{t, r, b, left}
9389 }
90+
91+// Keyboard interactivity is deferred; layer-shell is capped at v4.
9492 func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) {
9593 if k > 2 {
9694 r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity")
@@ -98,6 +96,8 @@ func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfac
9896 }
9997 l.pending.keyboard = k
10098 }
99+
100+// Layer popups are deferred; layer-shell is capped at v4.
101101 func (*layerSurface) GetPopup(*wlrlayershell.ZwlrLayerSurfaceV1, *xdgshell.Popup) {}
102102 func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) {
103103 for i, v := range l.serials {
@@ -117,6 +117,8 @@ func (l *layerSurface) SetLayer(r *wlrlayershell.ZwlrLayerSurfaceV1, v uint32) {
117117 }
118118 l.pending.layer = ports.Layer(v)
119119 }
120+
121+// Exclusive edge is not implemented; layer-shell is capped at v4.
120122 func (*layerSurface) SetExclusiveEdge(*wlrlayershell.ZwlrLayerSurfaceV1, uint32) {}
121123 func (l *layerSurface) unmap() {
122124 if !l.mapped {
@@ -131,7 +133,7 @@ func (l *layerSurface) unmap() {
131133 }
132134 func (s *Server) layerChanged() {
133135 list := make([]ports.LayerSurface, 0)
134- for _, l := range layerSets[s] {
136+ for _, l := range s.layers {
135137 if !l.mapped {
136138 continue
137139 }
@@ -149,7 +151,7 @@ func (s *Server) layerChanged() {
149151 func (l *layerSurface) commit(buffer bool) {
150152 if l.surface.destroyed {
151153 l.unmap()
152- delete(layerSets[l.shell.server], l.id)
154+ delete(l.shell.server.layers, l.id)
153155 return
154156 }
155157 p := l.pending
@@ -31,11 +31,7 @@ type layerSurface struct {
31 serials []uint3231 serials []uint32
32 }32 }
33 33
34-// Layer objects belong to the display goroutine, like xdg windows.
35-var layerSets = map[*Server]map[ports.WindowID]*layerSurface{}
36-
37 func registerLayer(d *server.Display, o Options, s *Server) error {34 func registerLayer(d *server.Display, o Options, s *Server) error {
38- layerSets[s] = make(map[ports.WindowID]*layerSurface)
39 return wlrlayershell.NewZwlrLayerShellV1Global(d, 4, func(c server.Client, v, id uint32) {35 return wlrlayershell.NewZwlrLayerShellV1Global(d, 4, func(c server.Client, v, id uint32) {
40 _, _ = wlrlayershell.NewZwlrLayerShellV1(c, int32(v), id, layerShell{s, uint32(o.OutputWidth), uint32(o.OutputHeight)})36 _, _ = wlrlayershell.NewZwlrLayerShellV1(c, int32(v), id, layerShell{s, uint32(o.OutputWidth), uint32(o.OutputHeight)})
41 })37 })
@@ -46,8 +42,8 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
46 return42 return
47 }43 }
48 state := h.server.surfaces[w.Resource]44 state := h.server.surfaces[w.Resource]
49- if state.kind != roleNone {45+ if state.kind != roleNone || state.attached || state.current != nil || state.committed {
50- r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorRole), "surface already has role")46+ r.PostError(uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed), "surface already constructed")
51 return47 return
52 }48 }
53 if layer > 3 {49 if layer > 3 {
@@ -65,10 +61,10 @@ func (h layerShell) GetLayerSurface(r *wlrlayershell.ZwlrLayerShellV1, id uint32
65 state.kind = roleLayer61 state.kind = roleLayer
66 state.layer = l62 state.layer = l
67 state.role = l.commit63 state.role = l.commit
68- layerSets[h.server][l.id] = l64+ h.server.layers[l.id] = l
69 resource.OnDestroy = func() {65 resource.OnDestroy = func() {
70 l.unmap()66 l.unmap()
71- delete(layerSets[h.server], l.id)67+ delete(h.server.layers, l.id)
72 state.layer = nil68 state.layer = nil
73 state.role = nil69 state.role = nil
74 state.current, state.pending = nil, nil70 state.current, state.pending = nil, nil
@@ -91,6 +87,8 @@ func (l *layerSurface) SetExclusiveZone(_ *wlrlayershell.ZwlrLayerSurfaceV1, z i
91 func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, left int32) {87 func (l *layerSurface) SetMargin(_ *wlrlayershell.ZwlrLayerSurfaceV1, t, r, b, left int32) {
92 l.pending.margin = [4]int32{t, r, b, left}88 l.pending.margin = [4]int32{t, r, b, left}
93 }89 }
90+
91+// Keyboard interactivity is deferred; layer-shell is capped at v4.
94 func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) {92 func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfaceV1, k uint32) {
95 if k > 2 {93 if k > 2 {
96 r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity")94 r.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidKeyboardInteractivity), "invalid keyboard interactivity")
@@ -98,6 +96,8 @@ func (l *layerSurface) SetKeyboardInteractivity(r *wlrlayershell.ZwlrLayerSurfac
98 }96 }
99 l.pending.keyboard = k97 l.pending.keyboard = k
100 }98 }
99+
100+// Layer popups are deferred; layer-shell is capped at v4.
101 func (*layerSurface) GetPopup(*wlrlayershell.ZwlrLayerSurfaceV1, *xdgshell.Popup) {}101 func (*layerSurface) GetPopup(*wlrlayershell.ZwlrLayerSurfaceV1, *xdgshell.Popup) {}
102 func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) {102 func (l *layerSurface) AckConfigure(r *wlrlayershell.ZwlrLayerSurfaceV1, serial uint32) {
103 for i, v := range l.serials {103 for i, v := range l.serials {
@@ -117,6 +117,8 @@ func (l *layerSurface) SetLayer(r *wlrlayershell.ZwlrLayerSurfaceV1, v uint32) {
117 }117 }
118 l.pending.layer = ports.Layer(v)118 l.pending.layer = ports.Layer(v)
119 }119 }
120+
121+// Exclusive edge is not implemented; layer-shell is capped at v4.
120 func (*layerSurface) SetExclusiveEdge(*wlrlayershell.ZwlrLayerSurfaceV1, uint32) {}122 func (*layerSurface) SetExclusiveEdge(*wlrlayershell.ZwlrLayerSurfaceV1, uint32) {}
121 func (l *layerSurface) unmap() {123 func (l *layerSurface) unmap() {
122 if !l.mapped {124 if !l.mapped {
@@ -131,7 +133,7 @@ func (l *layerSurface) unmap() {
131 }133 }
132 func (s *Server) layerChanged() {134 func (s *Server) layerChanged() {
133 list := make([]ports.LayerSurface, 0)135 list := make([]ports.LayerSurface, 0)
134- for _, l := range layerSets[s] {136+ for _, l := range s.layers {
135 if !l.mapped {137 if !l.mapped {
136 continue138 continue
137 }139 }
@@ -149,7 +151,7 @@ func (s *Server) layerChanged() {
149 func (l *layerSurface) commit(buffer bool) {151 func (l *layerSurface) commit(buffer bool) {
150 if l.surface.destroyed {152 if l.surface.destroyed {
151 l.unmap()153 l.unmap()
152- delete(layerSets[l.shell.server], l.id)154+ delete(l.shell.server.layers, l.id)
153 return155 return
154 }156 }
155 p := l.pending157 p := l.pending
M internal/adapters/wayland/layer_test.go +86 -0
@@ -1,6 +1,7 @@
11 package wayland
22
33 import (
4+ "fmt"
45 "github.com/bnema/nefertty/internal/ports"
56 "github.com/bnema/purego-libwayland/protocol/wayland"
67 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
@@ -140,3 +141,88 @@ func TestWaybarLayer(t *testing.T) {
140141 t.Fatalf("waybar timeout stderr: %s", output.String())
141142 }
142143 }
144+
145+func TestLayerAlreadyConstructed(t *testing.T) {
146+ for _, scenario := range []string{"role", "attached", "committed"} {
147+ t.Run(scenario, func(t *testing.T) {
148+ s, _, _, dir := lifecycleServer(t)
149+ c := protocolClient(t, s, dir)
150+ comp := bindProtocol(t, c, "wl_compositor")
151+ shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
152+ surf := c.AllocateID()
153+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
154+ registerProtocol(t, c, surf)
155+ switch scenario {
156+ case "role":
157+ sub := bindProtocol(t, c, "wl_subcompositor")
158+ parent := c.AllocateID()
159+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, parent)
160+ registerProtocol(t, c, parent)
161+ id := c.AllocateID()
162+ requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, id, surf, parent)
163+ case "attached":
164+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
165+ case "committed":
166+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
167+ }
168+ id := c.AllocateID()
169+ requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, id, surf, uint32(0), uint32(ports.LayerTop), "test")
170+ expectProtocolError(t, c, shell, uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed))
171+ })
172+ }
173+}
174+
175+type doneProxy struct {
176+ wlturbo.BaseProxy
177+ done int
178+ opcode uint16
179+}
180+
181+func (p *doneProxy) Dispatch(e *wlturbo.Event) {
182+ if e.Opcode == p.opcode {
183+ p.done++
184+ }
185+}
186+func TestXDGOutputDone(t *testing.T) {
187+ for _, version := range []uint32{1, 2} {
188+ t.Run(fmt.Sprintf("wl_output_v%d", version), func(t *testing.T) {
189+ s, _, _, dir := lifecycleServer(t)
190+ c := protocolClient(t, s, dir)
191+ global, ok := c.Registry().FindGlobal("wl_output")
192+ if !ok {
193+ t.Fatal("missing wl_output")
194+ }
195+ output, err := c.Registry().BindID(global.Name, global.Interface, version)
196+ if err != nil {
197+ t.Fatal(err)
198+ }
199+ op := &doneProxy{opcode: uint16(wayland.OutputEventDone)}
200+ op.SetID(output)
201+ c.Context().Register(op)
202+ global, ok = c.Registry().FindGlobal("zxdg_output_manager_v1")
203+ if !ok {
204+ t.Fatal("missing xdg output manager")
205+ }
206+ manager, err := c.Registry().BindID(global.Name, global.Interface, 3)
207+ if err != nil {
208+ t.Fatal(err)
209+ }
210+ registerProtocol(t, c, manager)
211+ if err := c.Roundtrip(); err != nil {
212+ t.Fatal(err)
213+ }
214+ op.done = 0
215+ xdg := c.AllocateID()
216+ xp := &doneProxy{opcode: 2}
217+ xp.SetID(xdg)
218+ c.Context().Register(xp)
219+ requestProtocol(t, c, manager, 1, xdg, output)
220+ if err := c.Roundtrip(); err != nil {
221+ t.Fatal(err)
222+ }
223+ if version == 1 && (xp.done != 1 || op.done != 0) || version == 2 && (xp.done != 0 || op.done != 1) {
224+ t.Fatalf("wl_output v%d: xdg done=%d, wl_output done=%d", version, xp.done, op.done)
225+ }
226+ })
227+ }
228+}
@@ -1,6 +1,7 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "fmt"
4 "github.com/bnema/nefertty/internal/ports"5 "github.com/bnema/nefertty/internal/ports"
5 "github.com/bnema/purego-libwayland/protocol/wayland"6 "github.com/bnema/purego-libwayland/protocol/wayland"
6 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"7 "github.com/bnema/purego-libwayland/protocol/wlrlayershell"
@@ -140,3 +141,88 @@ func TestWaybarLayer(t *testing.T) {
140 t.Fatalf("waybar timeout stderr: %s", output.String())141 t.Fatalf("waybar timeout stderr: %s", output.String())
141 }142 }
142 }143 }
144+
145+func TestLayerAlreadyConstructed(t *testing.T) {
146+ for _, scenario := range []string{"role", "attached", "committed"} {
147+ t.Run(scenario, func(t *testing.T) {
148+ s, _, _, dir := lifecycleServer(t)
149+ c := protocolClient(t, s, dir)
150+ comp := bindProtocol(t, c, "wl_compositor")
151+ shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
152+ surf := c.AllocateID()
153+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
154+ registerProtocol(t, c, surf)
155+ switch scenario {
156+ case "role":
157+ sub := bindProtocol(t, c, "wl_subcompositor")
158+ parent := c.AllocateID()
159+ requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, parent)
160+ registerProtocol(t, c, parent)
161+ id := c.AllocateID()
162+ requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, id, surf, parent)
163+ case "attached":
164+ requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
165+ case "committed":
166+ requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
167+ }
168+ id := c.AllocateID()
169+ requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, id, surf, uint32(0), uint32(ports.LayerTop), "test")
170+ expectProtocolError(t, c, shell, uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed))
171+ })
172+ }
173+}
174+
175+type doneProxy struct {
176+ wlturbo.BaseProxy
177+ done int
178+ opcode uint16
179+}
180+
181+func (p *doneProxy) Dispatch(e *wlturbo.Event) {
182+ if e.Opcode == p.opcode {
183+ p.done++
184+ }
185+}
186+func TestXDGOutputDone(t *testing.T) {
187+ for _, version := range []uint32{1, 2} {
188+ t.Run(fmt.Sprintf("wl_output_v%d", version), func(t *testing.T) {
189+ s, _, _, dir := lifecycleServer(t)
190+ c := protocolClient(t, s, dir)
191+ global, ok := c.Registry().FindGlobal("wl_output")
192+ if !ok {
193+ t.Fatal("missing wl_output")
194+ }
195+ output, err := c.Registry().BindID(global.Name, global.Interface, version)
196+ if err != nil {
197+ t.Fatal(err)
198+ }
199+ op := &doneProxy{opcode: uint16(wayland.OutputEventDone)}
200+ op.SetID(output)
201+ c.Context().Register(op)
202+ global, ok = c.Registry().FindGlobal("zxdg_output_manager_v1")
203+ if !ok {
204+ t.Fatal("missing xdg output manager")
205+ }
206+ manager, err := c.Registry().BindID(global.Name, global.Interface, 3)
207+ if err != nil {
208+ t.Fatal(err)
209+ }
210+ registerProtocol(t, c, manager)
211+ if err := c.Roundtrip(); err != nil {
212+ t.Fatal(err)
213+ }
214+ op.done = 0
215+ xdg := c.AllocateID()
216+ xp := &doneProxy{opcode: 2}
217+ xp.SetID(xdg)
218+ c.Context().Register(xp)
219+ requestProtocol(t, c, manager, 1, xdg, output)
220+ if err := c.Roundtrip(); err != nil {
221+ t.Fatal(err)
222+ }
223+ if version == 1 && (xp.done != 1 || op.done != 0) || version == 2 && (xp.done != 0 || op.done != 1) {
224+ t.Fatalf("wl_output v%d: xdg done=%d, wl_output done=%d", version, xp.done, op.done)
225+ }
226+ })
227+ }
228+}
M internal/adapters/wayland/server.go +2 -1
@@ -43,6 +43,7 @@ type Server struct {
4343 serial uint32
4444 ctx context.Context
4545 windows map[ports.WindowID]*window
46+ layers map[ports.WindowID]*layerSurface
4647 nextWindow ports.WindowID
4748 focused ports.WindowID
4849 pointerFocus ports.WindowID
@@ -90,7 +91,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
9091 d.Close()
9192 return nil, err
9293 }
93- s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
94+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
9495 if opts.Keymap != "" {
9596 if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) {
9697 cleanup()
@@ -43,6 +43,7 @@ type Server struct {
43 serial uint3243 serial uint32
44 ctx context.Context44 ctx context.Context
45 windows map[ports.WindowID]*window45 windows map[ports.WindowID]*window
46+ layers map[ports.WindowID]*layerSurface
46 nextWindow ports.WindowID47 nextWindow ports.WindowID
47 focused ports.WindowID48 focused ports.WindowID
48 pointerFocus ports.WindowID49 pointerFocus ports.WindowID
@@ -90,7 +91,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
90 d.Close()91 d.Close()
91 return nil, err92 return nil, err
92 }93 }
93- s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}94+ s := &Server{display: d, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
94 if opts.Keymap != "" {95 if opts.Keymap != "" {
95 if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) {96 if uint64(len(opts.Keymap))+1 > uint64(^uint32(0)) {
96 cleanup()97 cleanup()
M internal/adapters/wayland/surface.go +2 -0
@@ -23,6 +23,7 @@ type surface struct {
2323 server *Server
2424 current, pending *wayland.Buffer
2525 attached bool
26+ committed bool
2627 callbacks []*wayland.Callback
2728 role func(bool)
2829 destroyed bool
@@ -51,6 +52,7 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) {
5152 }
5253 }
5354 func (s *surface) Commit(*wayland.Surface) {
55+ s.committed = true
5456 fresh := s.attached && s.pending != nil
5557 if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked {
5658 s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")
@@ -23,6 +23,7 @@ type surface struct {
23 server *Server23 server *Server
24 current, pending *wayland.Buffer24 current, pending *wayland.Buffer
25 attached bool25 attached bool
26+ committed bool
26 callbacks []*wayland.Callback27 callbacks []*wayland.Callback
27 role func(bool)28 role func(bool)
28 destroyed bool29 destroyed bool
@@ -51,6 +52,7 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) {
51 }52 }
52 }53 }
53 func (s *surface) Commit(*wayland.Surface) {54 func (s *surface) Commit(*wayland.Surface) {
55+ s.committed = true
54 fresh := s.attached && s.pending != nil56 fresh := s.attached && s.pending != nil
55 if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked {57 if s.layer != nil && s.attached && s.pending != nil && !s.layer.acked {
56 s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")58 s.layer.resource.PostError(uint32(wlrlayershell.ZwlrLayerSurfaceV1ErrorInvalidSurfaceState), "buffer before configure ack")
M internal/adapters/wayland/xdgoutput.go +2 -2
@@ -25,9 +25,9 @@ func (m xdgOutputManager) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint
2525 x.SendName("HEADLESS-1")
2626 x.SendDescription("NeferTTY headless output")
2727 }
28- if r.Version() < 3 {
28+ if r.Version() < 3 || output == nil || output.Version() < 2 {
2929 x.SendDone()
30- } else if output != nil && output.Version() >= 2 {
30+ } else {
3131 output.SendDone()
3232 }
3333 }
@@ -25,9 +25,9 @@ func (m xdgOutputManager) GetXdgOutput(r *xdgoutput.ZxdgOutputManagerV1, id uint
25 x.SendName("HEADLESS-1")25 x.SendName("HEADLESS-1")
26 x.SendDescription("NeferTTY headless output")26 x.SendDescription("NeferTTY headless output")
27 }27 }
28- if r.Version() < 3 {28+ if r.Version() < 3 || output == nil || output.Version() < 2 {
29 x.SendDone()29 x.SendDone()
30- } else if output != nil && output.Version() >= 2 {30+ } else {
31 output.SendDone()31 output.SendDone()
32 }32 }
33 }33 }

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 23

Tags 10

↑↓ moveenter openesc close