detached 78a0c3a

fix(wayland): enforce xdg-shell configure, role and destruction rules

commit
78a0c3a97c85ac334dc2024e5f37afedd73cc9ef
author
brice · · 2026-09-23 12:28 UTC
parent
a7ac56a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +199 -10
M go.mod +1 -0
@@ -5,6 +5,7 @@ go 1.27
55 require (
66 github.com/BurntSushi/toml v1.5.0
77 github.com/bnema/purego-libwayland v0.0.0
8+ github.com/bnema/wlturbo v0.2.0
89 github.com/bnema/zerowrap v1.4.1
910 github.com/rs/zerolog v1.35.1
1011 golang.org/x/sys v0.48.0
@@ -5,6 +5,7 @@ go 1.27
5 require (5 require (
6 github.com/BurntSushi/toml v1.5.06 github.com/BurntSushi/toml v1.5.0
7 github.com/bnema/purego-libwayland v0.0.07 github.com/bnema/purego-libwayland v0.0.0
8+ github.com/bnema/wlturbo v0.2.0
8 github.com/bnema/zerowrap v1.4.19 github.com/bnema/zerowrap v1.4.1
9 github.com/rs/zerolog v1.35.110 github.com/rs/zerolog v1.35.1
10 golang.org/x/sys v0.48.011 golang.org/x/sys v0.48.0
M go.sum +2 -0 generated file: collapsed
M internal/adapters/wayland/globals.go +2 -1
@@ -93,11 +93,12 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
9393 if state == nil {
9494 return
9595 }
96- if state.role != nil {
96+ if state.kind != roleNone {
9797 r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role")
9898 return
9999 }
100100 if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil {
101+ state.kind = roleSubsurface
101102 state.role = func(bool) {}
102103 sub.OnDestroy = func() { state.role = nil }
103104 }
@@ -93,11 +93,12 @@ func (c subcompositor) GetSubsurface(r *wayland.Subcompositor, id uint32, w, par
93 if state == nil {93 if state == nil {
94 return94 return
95 }95 }
96- if state.role != nil {96+ if state.kind != roleNone {
97 r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role")97 r.PostError(uint32(wayland.SubcompositorErrorBadSurface), "surface already has role")
98 return98 return
99 }99 }
100 if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil {100 if sub, err := wayland.NewSubsurface(r.Client(), 1, id, subsurface{state}); err == nil {
101+ state.kind = roleSubsurface
101 state.role = func(bool) {}102 state.role = func(bool) {}
102 sub.OnDestroy = func() { state.role = nil }103 sub.OnDestroy = func() { state.role = nil }
103 }104 }
A internal/adapters/wayland/protocol_test.go +140 -0
@@ -0,0 +1,140 @@
1+package wayland
2+
3+import (
4+ "errors"
5+ "path/filepath"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
11+ "github.com/bnema/wlturbo"
12+ "golang.org/x/sys/unix"
13+)
14+
15+func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display {
16+ t.Helper()
17+ c, err := wlturbo.Connect(filepath.Join(dir, s.SocketName()))
18+ if err != nil {
19+ t.Fatal(err)
20+ }
21+ t.Cleanup(func() { _ = c.Close() })
22+ if err := c.Roundtrip(); err != nil {
23+ t.Fatal(err)
24+ }
25+ return c
26+}
27+
28+func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 {
29+ t.Helper()
30+ g, ok := c.Registry().FindGlobal(iface)
31+ if !ok {
32+ t.Fatalf("missing %s", iface)
33+ }
34+ id, err := c.Registry().BindID(g.Name, g.Interface, 1)
35+ if err != nil {
36+ t.Fatal(err)
37+ }
38+ return id
39+}
40+
41+func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, args ...any) {
42+ t.Helper()
43+ if err := c.SendRequest(id, uint16(op), args...); err != nil {
44+ t.Fatal(err)
45+ }
46+}
47+
48+func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) {
49+ t.Helper()
50+ done := make(chan error, 1)
51+ go func() { done <- c.Roundtrip() }()
52+ select {
53+ case err := <-done:
54+ var displayErr *wlturbo.DisplayError
55+ if !errors.As(err, &displayErr) {
56+ t.Fatalf("expected display error, got %v", err)
57+ }
58+ if displayErr.ObjectID != object || displayErr.Code != code {
59+ t.Fatalf("protocol error = %v, want object=%d code=%d", displayErr, object, code)
60+ }
61+ case <-time.After(2 * time.Second):
62+ _ = c.Close()
63+ t.Fatal("protocol error timeout")
64+ }
65+}
66+
67+type protocolProxy struct{ wlturbo.BaseProxy }
68+
69+func (*protocolProxy) Dispatch(*wlturbo.Event) {}
70+
71+func registerProtocol(t *testing.T, c *wlturbo.Display, id uint32) {
72+ t.Helper()
73+ p := &protocolProxy{}
74+ p.SetID(id)
75+ c.Context().Register(p)
76+}
77+
78+func TestXDGProtocolErrors(t *testing.T) {
79+ for _, scenario := range []string{"bogus ack", "buffer before ack", "subsurface role", "defunct toplevel"} {
80+ t.Run(scenario, func(t *testing.T) {
81+ s, _, _, dir := lifecycleServer(t)
82+ c := protocolClient(t, s, dir)
83+ compositor := bindProtocol(t, c, "wl_compositor")
84+ wm := bindProtocol(t, c, "xdg_wm_base")
85+ surface := c.AllocateID()
86+ requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, surface)
87+ registerProtocol(t, c, surface)
88+ if scenario == "subsurface role" {
89+ parent := c.AllocateID()
90+ requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, parent)
91+ registerProtocol(t, c, parent)
92+ subcomp := bindProtocol(t, c, "wl_subcompositor")
93+ requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, c.AllocateID(), surface, parent)
94+ requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, c.AllocateID(), surface)
95+ expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorRole))
96+ return
97+ }
98+ xdg := c.AllocateID()
99+ requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surface)
100+ top := c.AllocateID()
101+ registerProtocol(t, c, xdg)
102+ registerProtocol(t, c, top)
103+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
104+ switch scenario {
105+ case "bogus ack":
106+ requestProtocol(t, c, surface, wayland.SurfaceRequestCommit)
107+ if err := c.Roundtrip(); err != nil {
108+ t.Fatal(err)
109+ }
110+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0))
111+ expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial))
112+ case "buffer before ack":
113+ shm := bindProtocol(t, c, "wl_shm")
114+ fd, err := unix.MemfdCreate("protocol-buffer", 0)
115+ if err != nil {
116+ t.Fatal(err)
117+ }
118+ defer unix.Close(fd)
119+ if err := unix.Ftruncate(fd, 4); err != nil {
120+ t.Fatal(err)
121+ }
122+ pool := c.AllocateID()
123+ registerProtocol(t, c, shm)
124+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil {
125+ t.Fatal(err)
126+ }
127+ buffer := c.AllocateID()
128+ registerProtocol(t, c, pool)
129+ registerProtocol(t, c, buffer)
130+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
131+ requestProtocol(t, c, surface, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
132+ requestProtocol(t, c, surface, wayland.SurfaceRequestCommit)
133+ expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorUnconfiguredBuffer))
134+ case "defunct toplevel":
135+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestDestroy)
136+ expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorDefunctRoleObject))
137+ }
138+ })
139+ }
140+}
@@ -0,0 +1,140 @@
1+package wayland
2+
3+import (
4+ "errors"
5+ "path/filepath"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
11+ "github.com/bnema/wlturbo"
12+ "golang.org/x/sys/unix"
13+)
14+
15+func protocolClient(t *testing.T, s *Server, dir string) *wlturbo.Display {
16+ t.Helper()
17+ c, err := wlturbo.Connect(filepath.Join(dir, s.SocketName()))
18+ if err != nil {
19+ t.Fatal(err)
20+ }
21+ t.Cleanup(func() { _ = c.Close() })
22+ if err := c.Roundtrip(); err != nil {
23+ t.Fatal(err)
24+ }
25+ return c
26+}
27+
28+func bindProtocol(t *testing.T, c *wlturbo.Display, iface string) uint32 {
29+ t.Helper()
30+ g, ok := c.Registry().FindGlobal(iface)
31+ if !ok {
32+ t.Fatalf("missing %s", iface)
33+ }
34+ id, err := c.Registry().BindID(g.Name, g.Interface, 1)
35+ if err != nil {
36+ t.Fatal(err)
37+ }
38+ return id
39+}
40+
41+func requestProtocol(t *testing.T, c *wlturbo.Display, id uint32, op uint32, args ...any) {
42+ t.Helper()
43+ if err := c.SendRequest(id, uint16(op), args...); err != nil {
44+ t.Fatal(err)
45+ }
46+}
47+
48+func expectProtocolError(t *testing.T, c *wlturbo.Display, object, code uint32) {
49+ t.Helper()
50+ done := make(chan error, 1)
51+ go func() { done <- c.Roundtrip() }()
52+ select {
53+ case err := <-done:
54+ var displayErr *wlturbo.DisplayError
55+ if !errors.As(err, &displayErr) {
56+ t.Fatalf("expected display error, got %v", err)
57+ }
58+ if displayErr.ObjectID != object || displayErr.Code != code {
59+ t.Fatalf("protocol error = %v, want object=%d code=%d", displayErr, object, code)
60+ }
61+ case <-time.After(2 * time.Second):
62+ _ = c.Close()
63+ t.Fatal("protocol error timeout")
64+ }
65+}
66+
67+type protocolProxy struct{ wlturbo.BaseProxy }
68+
69+func (*protocolProxy) Dispatch(*wlturbo.Event) {}
70+
71+func registerProtocol(t *testing.T, c *wlturbo.Display, id uint32) {
72+ t.Helper()
73+ p := &protocolProxy{}
74+ p.SetID(id)
75+ c.Context().Register(p)
76+}
77+
78+func TestXDGProtocolErrors(t *testing.T) {
79+ for _, scenario := range []string{"bogus ack", "buffer before ack", "subsurface role", "defunct toplevel"} {
80+ t.Run(scenario, func(t *testing.T) {
81+ s, _, _, dir := lifecycleServer(t)
82+ c := protocolClient(t, s, dir)
83+ compositor := bindProtocol(t, c, "wl_compositor")
84+ wm := bindProtocol(t, c, "xdg_wm_base")
85+ surface := c.AllocateID()
86+ requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, surface)
87+ registerProtocol(t, c, surface)
88+ if scenario == "subsurface role" {
89+ parent := c.AllocateID()
90+ requestProtocol(t, c, compositor, wayland.CompositorRequestCreateSurface, parent)
91+ registerProtocol(t, c, parent)
92+ subcomp := bindProtocol(t, c, "wl_subcompositor")
93+ requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, c.AllocateID(), surface, parent)
94+ requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, c.AllocateID(), surface)
95+ expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorRole))
96+ return
97+ }
98+ xdg := c.AllocateID()
99+ requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, xdg, surface)
100+ top := c.AllocateID()
101+ registerProtocol(t, c, xdg)
102+ registerProtocol(t, c, top)
103+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestGetToplevel, top)
104+ switch scenario {
105+ case "bogus ack":
106+ requestProtocol(t, c, surface, wayland.SurfaceRequestCommit)
107+ if err := c.Roundtrip(); err != nil {
108+ t.Fatal(err)
109+ }
110+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestAckConfigure, uint32(0))
111+ expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorInvalidSerial))
112+ case "buffer before ack":
113+ shm := bindProtocol(t, c, "wl_shm")
114+ fd, err := unix.MemfdCreate("protocol-buffer", 0)
115+ if err != nil {
116+ t.Fatal(err)
117+ }
118+ defer unix.Close(fd)
119+ if err := unix.Ftruncate(fd, 4); err != nil {
120+ t.Fatal(err)
121+ }
122+ pool := c.AllocateID()
123+ registerProtocol(t, c, shm)
124+ if err := c.SendRequestWithFDs(shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, uint32(0), int32(4)); err != nil {
125+ t.Fatal(err)
126+ }
127+ buffer := c.AllocateID()
128+ registerProtocol(t, c, pool)
129+ registerProtocol(t, c, buffer)
130+ requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(1), int32(1), int32(4), uint32(0))
131+ requestProtocol(t, c, surface, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
132+ requestProtocol(t, c, surface, wayland.SurfaceRequestCommit)
133+ expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorUnconfiguredBuffer))
134+ case "defunct toplevel":
135+ requestProtocol(t, c, xdg, xdgshell.SurfaceRequestDestroy)
136+ expectProtocolError(t, c, xdg, uint32(xdgshell.SurfaceErrorDefunctRoleObject))
137+ }
138+ })
139+ }
140+}
M internal/adapters/wayland/server.go +1 -0
@@ -209,6 +209,7 @@ func (s *Server) apply(cmd ports.ClientCommand) {
209209 w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states)
210210 s.serial++
211211 w.xdg.resource.SendConfigure(s.serial)
212+ w.xdg.serials = append(w.xdg.serials, s.serial)
212213 case ports.CloseWindow:
213214 w := s.windows[c.ID]
214215 if w == nil || !w.toplevel.Resource.Alive() {
@@ -209,6 +209,7 @@ func (s *Server) apply(cmd ports.ClientCommand) {
209 w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states)209 w.toplevel.SendConfigure(int32(c.Width), int32(c.Height), states)
210 s.serial++210 s.serial++
211 w.xdg.resource.SendConfigure(s.serial)211 w.xdg.resource.SendConfigure(s.serial)
212+ w.xdg.serials = append(w.xdg.serials, s.serial)
212 case ports.CloseWindow:213 case ports.CloseWindow:
213 w := s.windows[c.ID]214 w := s.windows[c.ID]
214 if w == nil || !w.toplevel.Resource.Alive() {215 if w == nil || !w.toplevel.Resource.Alive() {
M internal/adapters/wayland/surface.go +22 -1
@@ -1,8 +1,21 @@
11 package wayland
22
3-import "github.com/bnema/purego-libwayland/protocol/wayland"
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
6+)
7+
8+type roleKind uint8
9+
10+const (
11+ roleNone roleKind = iota
12+ roleXDG
13+ roleSubsurface
14+)
415
516 type surface struct {
17+ kind roleKind
18+ xdg *xdgSurface
619 server *Server
720 current, pending *wayland.Buffer
821 attached bool
@@ -13,7 +26,11 @@ type surface struct {
1326
1427 func (s *surface) Destroy(*wayland.Surface) {
1528 s.destroyed = true
29+ for _, cb := range s.callbacks {
30+ cb.Destroy()
31+ }
1632 s.callbacks = nil
33+ s.current, s.pending = nil, nil
1734 if s.role != nil {
1835 s.role(false)
1936 }
@@ -29,6 +46,10 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) {
2946 }
3047 }
3148 func (s *surface) Commit(*wayland.Surface) {
49+ if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {
50+ s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
51+ return
52+ }
3253 if s.attached {
3354 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
3455 s.current.SendRelease()
@@ -1,8 +1,21 @@
1 package wayland1 package wayland
2 2
3-import "github.com/bnema/purego-libwayland/protocol/wayland"3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/protocol/xdgshell"
6+)
7+
8+type roleKind uint8
9+
10+const (
11+ roleNone roleKind = iota
12+ roleXDG
13+ roleSubsurface
14+)
4 15
5 type surface struct {16 type surface struct {
17+ kind roleKind
18+ xdg *xdgSurface
6 server *Server19 server *Server
7 current, pending *wayland.Buffer20 current, pending *wayland.Buffer
8 attached bool21 attached bool
@@ -13,7 +26,11 @@ type surface struct {
13 26
14 func (s *surface) Destroy(*wayland.Surface) {27 func (s *surface) Destroy(*wayland.Surface) {
15 s.destroyed = true28 s.destroyed = true
29+ for _, cb := range s.callbacks {
30+ cb.Destroy()
31+ }
16 s.callbacks = nil32 s.callbacks = nil
33+ s.current, s.pending = nil, nil
17 if s.role != nil {34 if s.role != nil {
18 s.role(false)35 s.role(false)
19 }36 }
@@ -29,6 +46,10 @@ func (s *surface) Frame(r *wayland.Surface, id uint32) {
29 }46 }
30 }47 }
31 func (s *surface) Commit(*wayland.Surface) {48 func (s *surface) Commit(*wayland.Surface) {
49+ if s.xdg != nil && s.attached && s.pending != nil && !s.xdg.acked {
50+ s.xdg.resource.PostError(uint32(xdgshell.SurfaceErrorUnconfiguredBuffer), "buffer before initial configure ack")
51+ return
52+ }
32 if s.attached {53 if s.attached {
33 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {54 if s.current != nil && (s.pending == nil || s.current.Resource != s.pending.Resource) {
34 s.current.SendRelease()55 s.current.SendRelease()
M internal/adapters/wayland/xdg.go +31 -8
@@ -26,14 +26,17 @@ func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) {
2626 if state == nil {
2727 return
2828 }
29- if state.role != nil {
30- r.PostError(0, "surface already has role")
29+ if state.kind != roleNone && state.kind != roleXDG || state.xdg != nil {
30+ r.PostError(uint32(xdgshell.WmBaseErrorRole), "surface already has role")
3131 return
3232 }
3333 x := &xdgSurface{server: m.server, surface: state}
3434 resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x)
3535 if err == nil {
3636 x.resource = resource
37+ state.kind = roleXDG
38+ state.xdg = x
39+ resource.OnDestroy = func() { state.xdg = nil; state.role = nil }
3740 }
3841
3942 }
@@ -58,10 +61,14 @@ type xdgSurface struct {
5861 configured bool
5962 acked bool
6063 window *window
61- serial uint32
64+ serials []uint32
6265 }
6366
64-func (x *xdgSurface) Destroy(*xdgshell.Surface) {}
67+func (x *xdgSurface) Destroy(r *xdgshell.Surface) {
68+ if x.window != nil {
69+ r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "toplevel still exists")
70+ }
71+}
6572
6673 type window struct {
6774 id ports.WindowID
@@ -78,6 +85,7 @@ func (w *window) unmap() {
7885 w.mapped = false
7986 w.xdg.configured = false
8087 w.xdg.acked = false
88+ w.xdg.serials = nil
8189 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
8290 }
8391 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
@@ -104,6 +112,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
104112 x.server.serial++
105113 t.SendConfigure(0, 0, nil)
106114 r.SendConfigure(x.server.serial)
115+ x.serials = append(x.serials, x.server.serial)
107116 } else if buffer && !w.mapped && x.acked {
108117 w.mapped = true
109118 x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID})
@@ -112,7 +121,15 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
112121 w.unmap()
113122 }
114123 }
115- t.OnDestroy = func() { w.unmap(); delete(x.server.windows, w.id); x.window = nil; x.surface.role = nil }
124+ t.OnDestroy = func() {
125+ w.unmap()
126+ delete(x.server.windows, w.id)
127+ x.window = nil
128+ x.surface.role = nil
129+ x.configured = false
130+ x.acked = false
131+ x.serials = nil
132+ }
116133 }
117134 func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {
118135 if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil {
@@ -120,9 +137,15 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac
120137 }
121138 }
122139 func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {}
123-func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) {
124- x.serial = serial
125- x.acked = true
140+func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
141+ for i, issued := range x.serials {
142+ if issued == serial {
143+ x.serials = x.serials[i+1:]
144+ x.acked = true
145+ return
146+ }
147+ }
148+ r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial")
126149 }
127150
128151 type top struct{ w *window }
@@ -26,14 +26,17 @@ func (m wm) GetXdgSurface(r *xdgshell.WmBase, id uint32, w *wayland.Surface) {
26 if state == nil {26 if state == nil {
27 return27 return
28 }28 }
29- if state.role != nil {29+ if state.kind != roleNone && state.kind != roleXDG || state.xdg != nil {
30- r.PostError(0, "surface already has role")30+ r.PostError(uint32(xdgshell.WmBaseErrorRole), "surface already has role")
31 return31 return
32 }32 }
33 x := &xdgSurface{server: m.server, surface: state}33 x := &xdgSurface{server: m.server, surface: state}
34 resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x)34 resource, err := xdgshell.NewSurface(r.Client(), r.Version(), id, x)
35 if err == nil {35 if err == nil {
36 x.resource = resource36 x.resource = resource
37+ state.kind = roleXDG
38+ state.xdg = x
39+ resource.OnDestroy = func() { state.xdg = nil; state.role = nil }
37 }40 }
38 41
39 }42 }
@@ -58,10 +61,14 @@ type xdgSurface struct {
58 configured bool61 configured bool
59 acked bool62 acked bool
60 window *window63 window *window
61- serial uint3264+ serials []uint32
62 }65 }
63 66
64-func (x *xdgSurface) Destroy(*xdgshell.Surface) {}67+func (x *xdgSurface) Destroy(r *xdgshell.Surface) {
68+ if x.window != nil {
69+ r.PostError(uint32(xdgshell.SurfaceErrorDefunctRoleObject), "toplevel still exists")
70+ }
71+}
65 72
66 type window struct {73 type window struct {
67 id ports.WindowID74 id ports.WindowID
@@ -78,6 +85,7 @@ func (w *window) unmap() {
78 w.mapped = false85 w.mapped = false
79 w.xdg.configured = false86 w.xdg.configured = false
80 w.xdg.acked = false87 w.xdg.acked = false
88+ w.xdg.serials = nil
81 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})89 w.xdg.server.emit(ports.WindowUnmapped{ID: w.id})
82 }90 }
83 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {91 func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
@@ -104,6 +112,7 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
104 x.server.serial++112 x.server.serial++
105 t.SendConfigure(0, 0, nil)113 t.SendConfigure(0, 0, nil)
106 r.SendConfigure(x.server.serial)114 r.SendConfigure(x.server.serial)
115+ x.serials = append(x.serials, x.server.serial)
107 } else if buffer && !w.mapped && x.acked {116 } else if buffer && !w.mapped && x.acked {
108 w.mapped = true117 w.mapped = true
109 x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID})118 x.server.emit(ports.WindowMapped{ID: w.id, AppID: w.appID})
@@ -112,7 +121,15 @@ func (x *xdgSurface) GetToplevel(r *xdgshell.Surface, id uint32) {
112 w.unmap()121 w.unmap()
113 }122 }
114 }123 }
115- t.OnDestroy = func() { w.unmap(); delete(x.server.windows, w.id); x.window = nil; x.surface.role = nil }124+ t.OnDestroy = func() {
125+ w.unmap()
126+ delete(x.server.windows, w.id)
127+ x.window = nil
128+ x.surface.role = nil
129+ x.configured = false
130+ x.acked = false
131+ x.serials = nil
132+ }
116 }133 }
117 func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {134 func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surface, _ *xdgshell.Positioner) {
118 if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil {135 if p, e := xdgshell.NewPopup(r.Client(), r.Version(), id, popup{}); e == nil {
@@ -120,9 +137,15 @@ func (x *xdgSurface) GetPopup(r *xdgshell.Surface, id uint32, _ *xdgshell.Surfac
120 }137 }
121 }138 }
122 func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {}139 func (x *xdgSurface) SetWindowGeometry(*xdgshell.Surface, int32, int32, int32, int32) {}
123-func (x *xdgSurface) AckConfigure(_ *xdgshell.Surface, serial uint32) {140+func (x *xdgSurface) AckConfigure(r *xdgshell.Surface, serial uint32) {
124- x.serial = serial141+ for i, issued := range x.serials {
125- x.acked = true142+ if issued == serial {
143+ x.serials = x.serials[i+1:]
144+ x.acked = true
145+ return
146+ }
147+ }
148+ r.PostError(uint32(xdgshell.SurfaceErrorInvalidSerial), "unknown configure serial")
126 }149 }
127 150
128 type top struct{ w *window }151 type top struct{ w *window }

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 25

Tags 10

↑↓ moveenter openesc close