detached 2513cf9

merge: removable globals

commit
2513cf9195b2232093535c1a0cbd7d9a359144b1
author
brice · · 2026-09-24 18:40 UTC
merge of
cdeaf22 + be4f2d7 · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +61 -3
M internal/e2e/feasibility_test.go +32 -0
@@ -321,3 +321,35 @@ func TestClientPID(t *testing.T) {
321321 t.Fatalf("pid %d, want %d", got, os.Getpid())
322322 }
323323 }
324+
325+// A removed global disappears from the registry of new clients.
326+func TestGlobalRemove(t *testing.T) {
327+ socket, d, _ := startServer(t)
328+ var g *server.Global
329+ added := make(chan error, 1)
330+ if !d.Do(func() {
331+ var err error
332+ g, err = d.AddGlobal(wayland.OutputInterface, 4, func(server.Client, uint32, uint32) {})
333+ added <- err
334+ }) {
335+ t.Fatal("display stopped")
336+ }
337+ must(t, <-added)
338+ has := func() bool {
339+ c, err := wlturbo.Connect(socket)
340+ must(t, err)
341+ defer c.Close()
342+ must(t, c.Roundtrip())
343+ _, ok := c.Registry().FindGlobal("wl_output")
344+ return ok
345+ }
346+ if !has() {
347+ t.Fatal("wl_output not advertised")
348+ }
349+ if !d.Do(func() { g.Remove(); g.Remove() }) {
350+ t.Fatal("display stopped")
351+ }
352+ if has() {
353+ t.Fatal("wl_output still advertised after Remove")
354+ }
355+}
@@ -321,3 +321,35 @@ func TestClientPID(t *testing.T) {
321 t.Fatalf("pid %d, want %d", got, os.Getpid())321 t.Fatalf("pid %d, want %d", got, os.Getpid())
322 }322 }
323 }323 }
324+
325+// A removed global disappears from the registry of new clients.
326+func TestGlobalRemove(t *testing.T) {
327+ socket, d, _ := startServer(t)
328+ var g *server.Global
329+ added := make(chan error, 1)
330+ if !d.Do(func() {
331+ var err error
332+ g, err = d.AddGlobal(wayland.OutputInterface, 4, func(server.Client, uint32, uint32) {})
333+ added <- err
334+ }) {
335+ t.Fatal("display stopped")
336+ }
337+ must(t, <-added)
338+ has := func() bool {
339+ c, err := wlturbo.Connect(socket)
340+ must(t, err)
341+ defer c.Close()
342+ must(t, c.Roundtrip())
343+ _, ok := c.Registry().FindGlobal("wl_output")
344+ return ok
345+ }
346+ if !has() {
347+ t.Fatal("wl_output not advertised")
348+ }
349+ if !d.Do(func() { g.Remove(); g.Remove() }) {
350+ t.Fatal("display stopped")
351+ }
352+ if has() {
353+ t.Fatal("wl_output still advertised after Remove")
354+ }
355+}
M server/display.go +27 -3
@@ -89,14 +89,38 @@ func (d *Display) AddSocketFD(fd int) error {
8989 }
9090
9191 func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {
92+ _, err := d.AddGlobal(iface, version, bindFn)
93+ return err
94+}
95+
96+// Global is a global that can be withdrawn, such as a wl_output on hotplug.
97+type Global struct{ c uintptr }
98+
99+// AddGlobal advertises a global and returns a handle to remove it later.
100+func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
92101 live.nextBind++
93102 token := live.nextBind
94103 live.binds[token] = bindFn
95- if wlGlobalCreate(d.c, iface.c, version, token, cbBind) == 0 {
104+ c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
105+ if c == 0 {
96106 delete(live.binds, token)
97- return fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
107+ return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
98108 }
99- return nil
109+ return &Global{c: c}, nil
110+}
111+
112+// Remove withdraws the global: clients get wl_registry.global_remove.
113+// Existing resources stay valid. A client that binds before it sees the
114+// removal still gets its bind function called, so it always receives a
115+// resource (the protocol allows binding a just-removed global); the bind
116+// function should then send inert state. The global is freed with the
117+// display. Display goroutine only; idempotent.
118+func (g *Global) Remove() {
119+ if g == nil || g.c == 0 {
120+ return
121+ }
122+ wlGlobalRemove(g.c)
123+ g.c = 0
100124 }
101125
102126 // Close destroys a display that has not started Run. It is a no-op once Run
@@ -89,14 +89,38 @@ func (d *Display) AddSocketFD(fd int) error {
89 }89 }
90 90
91 func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {91 func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {
92+ _, err := d.AddGlobal(iface, version, bindFn)
93+ return err
94+}
95+
96+// Global is a global that can be withdrawn, such as a wl_output on hotplug.
97+type Global struct{ c uintptr }
98+
99+// AddGlobal advertises a global and returns a handle to remove it later.
100+func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
92 live.nextBind++101 live.nextBind++
93 token := live.nextBind102 token := live.nextBind
94 live.binds[token] = bindFn103 live.binds[token] = bindFn
95- if wlGlobalCreate(d.c, iface.c, version, token, cbBind) == 0 {104+ c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
105+ if c == 0 {
96 delete(live.binds, token)106 delete(live.binds, token)
97- return fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)107+ return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
98 }108 }
99- return nil109+ return &Global{c: c}, nil
110+}
111+
112+// Remove withdraws the global: clients get wl_registry.global_remove.
113+// Existing resources stay valid. A client that binds before it sees the
114+// removal still gets its bind function called, so it always receives a
115+// resource (the protocol allows binding a just-removed global); the bind
116+// function should then send inert state. The global is freed with the
117+// display. Display goroutine only; idempotent.
118+func (g *Global) Remove() {
119+ if g == nil || g.c == 0 {
120+ return
121+ }
122+ wlGlobalRemove(g.c)
123+ g.c = 0
100 }124 }
101 125
102 // Close destroys a display that has not started Run. It is a no-op once Run126 // Close destroys a display that has not started Run. It is a no-op once Run
M server/lib.go +2 -0
@@ -21,6 +21,7 @@ var (
2121 wlEventLoopAddFD func(loop uintptr, fd int32, mask uint32, fn, data uintptr) uintptr
2222 wlEventSourceRemove func(source uintptr) int32
2323 wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr
24+ wlGlobalRemove func(global uintptr)
2425 wlResourceCreate func(client, iface uintptr, version int32, id uint32) uintptr
2526 wlResourceSetDispatcher func(resource, dispatcher, impl, data, destroy uintptr)
2627 wlResourcePostEventArr func(resource uintptr, opcode uint32, args unsafe.Pointer)
@@ -68,6 +69,7 @@ func load() error {
6869 reg(&wlEventLoopAddFD, "wl_event_loop_add_fd")
6970 reg(&wlEventSourceRemove, "wl_event_source_remove")
7071 reg(&wlGlobalCreate, "wl_global_create")
72+ reg(&wlGlobalRemove, "wl_global_remove")
7173 reg(&wlResourceCreate, "wl_resource_create")
7274 reg(&wlResourceSetDispatcher, "wl_resource_set_dispatcher")
7375 reg(&wlResourcePostEventArr, "wl_resource_post_event_array")
@@ -21,6 +21,7 @@ var (
21 wlEventLoopAddFD func(loop uintptr, fd int32, mask uint32, fn, data uintptr) uintptr21 wlEventLoopAddFD func(loop uintptr, fd int32, mask uint32, fn, data uintptr) uintptr
22 wlEventSourceRemove func(source uintptr) int3222 wlEventSourceRemove func(source uintptr) int32
23 wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr23 wlGlobalCreate func(display, iface uintptr, version int32, data uintptr, bind uintptr) uintptr
24+ wlGlobalRemove func(global uintptr)
24 wlResourceCreate func(client, iface uintptr, version int32, id uint32) uintptr25 wlResourceCreate func(client, iface uintptr, version int32, id uint32) uintptr
25 wlResourceSetDispatcher func(resource, dispatcher, impl, data, destroy uintptr)26 wlResourceSetDispatcher func(resource, dispatcher, impl, data, destroy uintptr)
26 wlResourcePostEventArr func(resource uintptr, opcode uint32, args unsafe.Pointer)27 wlResourcePostEventArr func(resource uintptr, opcode uint32, args unsafe.Pointer)
@@ -68,6 +69,7 @@ func load() error {
68 reg(&wlEventLoopAddFD, "wl_event_loop_add_fd")69 reg(&wlEventLoopAddFD, "wl_event_loop_add_fd")
69 reg(&wlEventSourceRemove, "wl_event_source_remove")70 reg(&wlEventSourceRemove, "wl_event_source_remove")
70 reg(&wlGlobalCreate, "wl_global_create")71 reg(&wlGlobalCreate, "wl_global_create")
72+ reg(&wlGlobalRemove, "wl_global_remove")
71 reg(&wlResourceCreate, "wl_resource_create")73 reg(&wlResourceCreate, "wl_resource_create")
72 reg(&wlResourceSetDispatcher, "wl_resource_set_dispatcher")74 reg(&wlResourceSetDispatcher, "wl_resource_set_dispatcher")
73 reg(&wlResourcePostEventArr, "wl_resource_post_event_array")75 reg(&wlResourcePostEventArr, "wl_resource_post_event_array")

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 14

↑↓ moveenter openesc close