detached ef3605f

feat(registry): negotiate announced global versions for extension binds

commit
ef3605f0837bce55228ebf8bfd321f2b406c6841
author
brice · · 2026-09-28 08:04 UTC
parent
4bb8af0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +76 -54
M README.md +2 -0
@@ -21,6 +21,8 @@ Higher-level protocol implementations (virtual input devices, output management,
2121
2222 `protocol/core` provides core Wayland (including data-device); `protocol/xdgshell` provides xdg-shell; `protocol/linuxdmabuf` provides linux-dmabuf v4 feedback; `protocol/drmsyncobj` provides linux-drm-syncobj v1; `protocol/viewporter` provides viewporter; `protocol/fractionalscale` provides fractional-scale v1; and `protocol/textinput` provides text-input v3. Bindings handle wire messages and object lifecycle; applications decide compositor policy.
2323
24+After `display.Roundtrip()` discovers globals, call `display.Registry().BindNegotiated(interfaceName, supportedVersion, proxy)` to bind at the lesser of the advertised and supported versions. It returns the negotiated version; use `errors.Is(err, wlturbo.ErrGlobalNotFound)` to handle absent optional globals.
25+
2426 ## Features
2527
2628 ### Performance Optimizations
@@ -21,6 +21,8 @@ Higher-level protocol implementations (virtual input devices, output management,
21 21
22 `protocol/core` provides core Wayland (including data-device); `protocol/xdgshell` provides xdg-shell; `protocol/linuxdmabuf` provides linux-dmabuf v4 feedback; `protocol/drmsyncobj` provides linux-drm-syncobj v1; `protocol/viewporter` provides viewporter; `protocol/fractionalscale` provides fractional-scale v1; and `protocol/textinput` provides text-input v3. Bindings handle wire messages and object lifecycle; applications decide compositor policy.22 `protocol/core` provides core Wayland (including data-device); `protocol/xdgshell` provides xdg-shell; `protocol/linuxdmabuf` provides linux-dmabuf v4 feedback; `protocol/drmsyncobj` provides linux-drm-syncobj v1; `protocol/viewporter` provides viewporter; `protocol/fractionalscale` provides fractional-scale v1; and `protocol/textinput` provides text-input v3. Bindings handle wire messages and object lifecycle; applications decide compositor policy.
23 23
24+After `display.Roundtrip()` discovers globals, call `display.Registry().BindNegotiated(interfaceName, supportedVersion, proxy)` to bind at the lesser of the advertised and supported versions. It returns the negotiated version; use `errors.Is(err, wlturbo.ErrGlobalNotFound)` to handle absent optional globals.
25+
24 ## Features26 ## Features
25 27
26 ### Performance Optimizations28 ### Performance Optimizations
M client.go +22 -0
@@ -789,6 +789,28 @@ func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
789789 r.handlers[iface] = handler
790790 }
791791
792+// ErrGlobalNotFound reports that the compositor does not announce a global.
793+var ErrGlobalNotFound = errors.New("wlturbo: global not announced")
794+
795+// BindNegotiated binds the announced global for iface at
796+// min(announced version, supported) and returns that version. It returns
797+// ErrGlobalNotFound when the global is absent, so callers can decide whether
798+// the capability is optional.
799+func (r *Registry) BindNegotiated(iface string, supported uint32, proxy Proxy) (uint32, error) {
800+ if supported == 0 {
801+ return 0, fmt.Errorf("wlturbo: supported version for %s must be at least 1", iface)
802+ }
803+ g, ok := r.FindGlobal(iface)
804+ if !ok || g.Version == 0 {
805+ return 0, fmt.Errorf("%w: %s", ErrGlobalNotFound, iface)
806+ }
807+ version := min(g.Version, supported)
808+ if err := r.Bind(g.Name, iface, version, proxy); err != nil {
809+ return 0, err
810+ }
811+ return version, nil
812+}
813+
792814 // Bind binds to a global object and returns a typed proxy
793815 func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
794816 // Set the ID if not already set
@@ -789,6 +789,28 @@ func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
789 r.handlers[iface] = handler789 r.handlers[iface] = handler
790 }790 }
791 791
792+// ErrGlobalNotFound reports that the compositor does not announce a global.
793+var ErrGlobalNotFound = errors.New("wlturbo: global not announced")
794+
795+// BindNegotiated binds the announced global for iface at
796+// min(announced version, supported) and returns that version. It returns
797+// ErrGlobalNotFound when the global is absent, so callers can decide whether
798+// the capability is optional.
799+func (r *Registry) BindNegotiated(iface string, supported uint32, proxy Proxy) (uint32, error) {
800+ if supported == 0 {
801+ return 0, fmt.Errorf("wlturbo: supported version for %s must be at least 1", iface)
802+ }
803+ g, ok := r.FindGlobal(iface)
804+ if !ok || g.Version == 0 {
805+ return 0, fmt.Errorf("%w: %s", ErrGlobalNotFound, iface)
806+ }
807+ version := min(g.Version, supported)
808+ if err := r.Bind(g.Name, iface, version, proxy); err != nil {
809+ return 0, err
810+ }
811+ return version, nil
812+}
813+
792 // Bind binds to a global object and returns a typed proxy814 // Bind binds to a global object and returns a typed proxy
793 func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {815 func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
794 // Set the ID if not already set816 // Set the ID if not already set
M consumer_test.go +8 -1
@@ -47,7 +47,14 @@ func TestExternalConsumer(t *testing.T) {
4747 if err := os.WriteFile(filepath.Join(temp, "extensions", "extensions_test.go"), extension, 0600); err != nil {
4848 t.Fatal(err)
4949 }
50- cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair", "./...")
50+ negotiation, err := os.ReadFile(filepath.Join(root, "protocol/negotiation_test.go"))
51+ if err != nil {
52+ t.Fatal(err)
53+ }
54+ if err := os.WriteFile(filepath.Join(temp, "extensions", "negotiation_test.go"), negotiation, 0600); err != nil {
55+ t.Fatal(err)
56+ }
57+ cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair|TestAnnouncedVersionNegotiation", "./...")
5158 cmd.Dir = temp
5259 cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
5360 if output, err := cmd.CombinedOutput(); err != nil {
@@ -47,7 +47,14 @@ func TestExternalConsumer(t *testing.T) {
47 if err := os.WriteFile(filepath.Join(temp, "extensions", "extensions_test.go"), extension, 0600); err != nil {47 if err := os.WriteFile(filepath.Join(temp, "extensions", "extensions_test.go"), extension, 0600); err != nil {
48 t.Fatal(err)48 t.Fatal(err)
49 }49 }
50- cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair", "./...")50+ negotiation, err := os.ReadFile(filepath.Join(root, "protocol/negotiation_test.go"))
51+ if err != nil {
52+ t.Fatal(err)
53+ }
54+ if err := os.WriteFile(filepath.Join(temp, "extensions", "negotiation_test.go"), negotiation, 0600); err != nil {
55+ t.Fatal(err)
56+ }
57+ cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair|TestAnnouncedVersionNegotiation", "./...")
51 cmd.Dir = temp58 cmd.Dir = temp
52 cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")59 cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
53 if output, err := cmd.CombinedOutput(); err != nil {60 if output, err := cmd.CombinedOutput(); err != nil {
M protocol/extensions_test.go +27 -16
@@ -94,16 +94,19 @@ func noerr(t *testing.T, e error) {
9494 t.Fatal(e)
9595 }
9696 }
97-func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, supported uint32, proxy wlturbo.Proxy) {
97+func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, announced uint32, supported uint32, proxy wlturbo.Proxy) {
9898 t.Helper()
99- v := supported
100- if v > 4 {
101- v = 4
102- }
103- noerr(t, d.Registry().Bind(name, iface, v, proxy))
99+ // A fake server must advertise a global before clients can negotiate it.
100+ announcement := append(msg(0, 0, name)[8:], str(iface)...)
101+ announcement = append(announcement, msg(0, 0, announced)[8:]...)
102+ send(t, p, payload(d.Registry().ID(), 0, announcement...))
103+ noerr(t, d.Dispatch())
104+ v, err := d.Registry().BindNegotiated(iface, supported, proxy)
105+ noerr(t, err)
106+ want := min(announced, supported)
104107 _, op, b := request(t, p)
105- if op != 0 || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != v || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
106- t.Fatalf("bad bind %s version=%d %x", iface, v, b)
108+ if op != 0 || v != want || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != want || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
109+ t.Fatalf("bad bind %s announced=%d supported=%d negotiated=%d wire=%x", iface, announced, supported, v, b)
107110 }
108111 }
109112 func TestExtensionsOverSocketpair(t *testing.T) {
@@ -121,6 +124,10 @@ func TestExtensionsOverSocketpair(t *testing.T) {
121124 if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
122125 t.Fatal("absent text input")
123126 }
127+ // Remove the discovery-only announcement before the bind matrix assigns
128+ // its own global name to this interface.
129+ send(t, p, msg(d.Registry().ID(), 1, 11))
130+ noerr(t, d.Dispatch())
124131 surface := core.NewSurface(d.Context())
125132 surface.SetID(d.AllocateID())
126133 d.Context().Register(surface)
@@ -128,7 +135,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
128135 seat.SetID(d.AllocateID())
129136 d.Context().Register(seat)
130137 wm := xdgshell.NewXdgWmBase(d.Context())
131- bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 7, wm)
138+ bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 6, 7, wm)
132139 xs, e := wm.GetXdgSurface(surface)
133140 noerr(t, e)
134141 request(t, p)
@@ -158,7 +165,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
158165 noerr(t, wm.Destroy())
159166 request(t, p)
160167 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
161- bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, dm)
168+ bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, 4, dm)
162169 fb, e := dm.GetDefaultFeedback()
163170 noerr(t, e)
164171 request(t, p)
@@ -270,7 +277,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
270277 noerr(t, dm.Destroy())
271278 request(t, p)
272279 sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
273- bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, sync)
280+ bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, sync)
274281 ss, e := sync.GetSurface(surface)
275282 noerr(t, e)
276283 request(t, p)
@@ -300,7 +307,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
300307 noerr(t, sync.Destroy())
301308 request(t, p)
302309 vp := viewporter.NewWpViewporter(d.Context())
303- bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, vp)
310+ bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, 1, vp)
304311 view, e := vp.GetViewport(surface)
305312 noerr(t, e)
306313 request(t, p)
@@ -309,7 +316,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
309316 noerr(t, vp.Destroy())
310317 request(t, p)
311318 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
312- bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, fs)
319+ bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, 1, fs)
313320 scale, e := fs.GetFractionalScale(surface)
314321 noerr(t, e)
315322 request(t, p)
@@ -325,7 +332,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
325332 noerr(t, fs.Destroy())
326333 request(t, p)
327334 ti := textinput.NewTextInputManagerV3(d.Context())
328- bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 1, ti)
335+ bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 2, 1, ti)
329336 input, e := ti.GetTextInput(seat)
330337 noerr(t, e)
331338 request(t, p)
@@ -362,7 +369,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
362369 // Core data-device remains in protocol/core; verify its typed child and
363370 // both directions of clipboard descriptor ownership.
364371 mgr := core.NewDataDeviceManager(d.Context())
365- bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, mgr)
372+ bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, 4, mgr)
366373 device, e := mgr.GetDataDevice(seat)
367374 noerr(t, e)
368375 request(t, p)
@@ -442,7 +449,11 @@ func TestLiveFeedback(t *testing.T) {
442449 t.Fatal("dmabuf feedback requires v4")
443450 }
444451 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
445- noerr(t, d.Registry().Bind(g.Name, iface, v, dm))
452+ negotiated, err := d.Registry().BindNegotiated(iface, 4, dm)
453+ noerr(t, err)
454+ if negotiated != v {
455+ t.Fatalf("dmabuf negotiated=%d want=%d", negotiated, v)
456+ }
446457 fb, e := dm.GetDefaultFeedback()
447458 noerr(t, e)
448459 done := false
@@ -94,16 +94,19 @@ func noerr(t *testing.T, e error) {
94 t.Fatal(e)94 t.Fatal(e)
95 }95 }
96 }96 }
97-func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, supported uint32, proxy wlturbo.Proxy) {97+func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, announced uint32, supported uint32, proxy wlturbo.Proxy) {
98 t.Helper()98 t.Helper()
99- v := supported99+ // A fake server must advertise a global before clients can negotiate it.
100- if v > 4 {100+ announcement := append(msg(0, 0, name)[8:], str(iface)...)
101- v = 4101+ announcement = append(announcement, msg(0, 0, announced)[8:]...)
102- }102+ send(t, p, payload(d.Registry().ID(), 0, announcement...))
103- noerr(t, d.Registry().Bind(name, iface, v, proxy))103+ noerr(t, d.Dispatch())
104+ v, err := d.Registry().BindNegotiated(iface, supported, proxy)
105+ noerr(t, err)
106+ want := min(announced, supported)
104 _, op, b := request(t, p)107 _, op, b := request(t, p)
105- if op != 0 || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != v || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {108+ if op != 0 || v != want || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != want || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() {
106- t.Fatalf("bad bind %s version=%d %x", iface, v, b)109+ t.Fatalf("bad bind %s announced=%d supported=%d negotiated=%d wire=%x", iface, announced, supported, v, b)
107 }110 }
108 }111 }
109 func TestExtensionsOverSocketpair(t *testing.T) {112 func TestExtensionsOverSocketpair(t *testing.T) {
@@ -121,6 +124,10 @@ func TestExtensionsOverSocketpair(t *testing.T) {
121 if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {124 if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok {
122 t.Fatal("absent text input")125 t.Fatal("absent text input")
123 }126 }
127+ // Remove the discovery-only announcement before the bind matrix assigns
128+ // its own global name to this interface.
129+ send(t, p, msg(d.Registry().ID(), 1, 11))
130+ noerr(t, d.Dispatch())
124 surface := core.NewSurface(d.Context())131 surface := core.NewSurface(d.Context())
125 surface.SetID(d.AllocateID())132 surface.SetID(d.AllocateID())
126 d.Context().Register(surface)133 d.Context().Register(surface)
@@ -128,7 +135,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
128 seat.SetID(d.AllocateID())135 seat.SetID(d.AllocateID())
129 d.Context().Register(seat)136 d.Context().Register(seat)
130 wm := xdgshell.NewXdgWmBase(d.Context())137 wm := xdgshell.NewXdgWmBase(d.Context())
131- bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 7, wm)138+ bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 6, 7, wm)
132 xs, e := wm.GetXdgSurface(surface)139 xs, e := wm.GetXdgSurface(surface)
133 noerr(t, e)140 noerr(t, e)
134 request(t, p)141 request(t, p)
@@ -158,7 +165,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
158 noerr(t, wm.Destroy())165 noerr(t, wm.Destroy())
159 request(t, p)166 request(t, p)
160 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())167 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
161- bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, dm)168+ bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, 4, dm)
162 fb, e := dm.GetDefaultFeedback()169 fb, e := dm.GetDefaultFeedback()
163 noerr(t, e)170 noerr(t, e)
164 request(t, p)171 request(t, p)
@@ -270,7 +277,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
270 noerr(t, dm.Destroy())277 noerr(t, dm.Destroy())
271 request(t, p)278 request(t, p)
272 sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())279 sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context())
273- bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, sync)280+ bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, sync)
274 ss, e := sync.GetSurface(surface)281 ss, e := sync.GetSurface(surface)
275 noerr(t, e)282 noerr(t, e)
276 request(t, p)283 request(t, p)
@@ -300,7 +307,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
300 noerr(t, sync.Destroy())307 noerr(t, sync.Destroy())
301 request(t, p)308 request(t, p)
302 vp := viewporter.NewWpViewporter(d.Context())309 vp := viewporter.NewWpViewporter(d.Context())
303- bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, vp)310+ bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, 1, vp)
304 view, e := vp.GetViewport(surface)311 view, e := vp.GetViewport(surface)
305 noerr(t, e)312 noerr(t, e)
306 request(t, p)313 request(t, p)
@@ -309,7 +316,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
309 noerr(t, vp.Destroy())316 noerr(t, vp.Destroy())
310 request(t, p)317 request(t, p)
311 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())318 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
312- bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, fs)319+ bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, 1, fs)
313 scale, e := fs.GetFractionalScale(surface)320 scale, e := fs.GetFractionalScale(surface)
314 noerr(t, e)321 noerr(t, e)
315 request(t, p)322 request(t, p)
@@ -325,7 +332,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
325 noerr(t, fs.Destroy())332 noerr(t, fs.Destroy())
326 request(t, p)333 request(t, p)
327 ti := textinput.NewTextInputManagerV3(d.Context())334 ti := textinput.NewTextInputManagerV3(d.Context())
328- bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 1, ti)335+ bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 2, 1, ti)
329 input, e := ti.GetTextInput(seat)336 input, e := ti.GetTextInput(seat)
330 noerr(t, e)337 noerr(t, e)
331 request(t, p)338 request(t, p)
@@ -362,7 +369,7 @@ func TestExtensionsOverSocketpair(t *testing.T) {
362 // Core data-device remains in protocol/core; verify its typed child and369 // Core data-device remains in protocol/core; verify its typed child and
363 // both directions of clipboard descriptor ownership.370 // both directions of clipboard descriptor ownership.
364 mgr := core.NewDataDeviceManager(d.Context())371 mgr := core.NewDataDeviceManager(d.Context())
365- bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, mgr)372+ bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, 4, mgr)
366 device, e := mgr.GetDataDevice(seat)373 device, e := mgr.GetDataDevice(seat)
367 noerr(t, e)374 noerr(t, e)
368 request(t, p)375 request(t, p)
@@ -442,7 +449,11 @@ func TestLiveFeedback(t *testing.T) {
442 t.Fatal("dmabuf feedback requires v4")449 t.Fatal("dmabuf feedback requires v4")
443 }450 }
444 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())451 dm := linuxdmabuf.NewLinuxDmabuf(d.Context())
445- noerr(t, d.Registry().Bind(g.Name, iface, v, dm))452+ negotiated, err := d.Registry().BindNegotiated(iface, 4, dm)
453+ noerr(t, err)
454+ if negotiated != v {
455+ t.Fatalf("dmabuf negotiated=%d want=%d", negotiated, v)
456+ }
446 fb, e := dm.GetDefaultFeedback()457 fb, e := dm.GetDefaultFeedback()
447 noerr(t, e)458 noerr(t, e)
448 done := false459 done := false
M protocol/headless_test.go +6 -19
@@ -73,23 +73,12 @@ func TestHeadlessNeferWL(t *testing.T) {
7373 stop := context.AfterFunc(ctx, func() { _ = d.Close() })
7474 defer stop()
7575 noerr(t, d.Roundtrip())
76- require := func(iface string) wlturbo.Global {
77- t.Helper()
78- g, ok := d.Registry().FindGlobal(iface)
79- if !ok {
80- t.Fatalf("required global %s absent\n%s", iface, headlessLog(log))
81- }
82- t.Logf("%s server=%d", iface, g.Version)
83- return g
84- }
8576 bind := func(iface string, supported uint32, proxy wlturbo.Proxy) uint32 {
8677 t.Helper()
87- g := require(iface)
88- v := g.Version
89- if v > supported {
90- v = supported
78+ v, err := d.Registry().BindNegotiated(iface, supported, proxy)
79+ if err != nil {
80+ t.Fatalf("bind %s: %v\n%s", iface, err, headlessLog(log))
9181 }
92- noerr(t, d.Registry().Bind(g.Name, iface, v, proxy))
9382 t.Logf("%s negotiated=%d", iface, v)
9483 return v
9584 }
@@ -109,12 +98,10 @@ func TestHeadlessNeferWL(t *testing.T) {
10998 bind(viewporter.WpViewporterInterface, 1, vp)
11099 if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); ok {
111100 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
112- v := g.Version
113- if v > 1 {
114- v = 1
101+ v := bind(g.Interface, 1, fs)
102+ if v != 1 {
103+ t.Fatalf("fractional scale version %d", v)
115104 }
116- noerr(t, d.Registry().Bind(g.Name, g.Interface, v, fs))
117- t.Logf("%s negotiated=%d", g.Interface, v)
118105 noerr(t, fs.Destroy())
119106 } else {
120107 t.Log("fractional scale absent")
@@ -73,23 +73,12 @@ func TestHeadlessNeferWL(t *testing.T) {
73 stop := context.AfterFunc(ctx, func() { _ = d.Close() })73 stop := context.AfterFunc(ctx, func() { _ = d.Close() })
74 defer stop()74 defer stop()
75 noerr(t, d.Roundtrip())75 noerr(t, d.Roundtrip())
76- require := func(iface string) wlturbo.Global {
77- t.Helper()
78- g, ok := d.Registry().FindGlobal(iface)
79- if !ok {
80- t.Fatalf("required global %s absent\n%s", iface, headlessLog(log))
81- }
82- t.Logf("%s server=%d", iface, g.Version)
83- return g
84- }
85 bind := func(iface string, supported uint32, proxy wlturbo.Proxy) uint32 {76 bind := func(iface string, supported uint32, proxy wlturbo.Proxy) uint32 {
86 t.Helper()77 t.Helper()
87- g := require(iface)78+ v, err := d.Registry().BindNegotiated(iface, supported, proxy)
88- v := g.Version79+ if err != nil {
89- if v > supported {80+ t.Fatalf("bind %s: %v\n%s", iface, err, headlessLog(log))
90- v = supported
91 }81 }
92- noerr(t, d.Registry().Bind(g.Name, iface, v, proxy))
93 t.Logf("%s negotiated=%d", iface, v)82 t.Logf("%s negotiated=%d", iface, v)
94 return v83 return v
95 }84 }
@@ -109,12 +98,10 @@ func TestHeadlessNeferWL(t *testing.T) {
109 bind(viewporter.WpViewporterInterface, 1, vp)98 bind(viewporter.WpViewporterInterface, 1, vp)
110 if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); ok {99 if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); ok {
111 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())100 fs := fractionalscale.NewWpFractionalScaleManager(d.Context())
112- v := g.Version101+ v := bind(g.Interface, 1, fs)
113- if v > 1 {102+ if v != 1 {
114- v = 1103+ t.Fatalf("fractional scale version %d", v)
115 }104 }
116- noerr(t, d.Registry().Bind(g.Name, g.Interface, v, fs))
117- t.Logf("%s negotiated=%d", g.Interface, v)
118 noerr(t, fs.Destroy())105 noerr(t, fs.Destroy())
119 } else {106 } else {
120 t.Log("fractional scale absent")107 t.Log("fractional scale absent")
M protocol/negotiation_test.go +11 -18
@@ -4,26 +4,13 @@ package protocol_test
44
55 import (
66 "encoding/binary"
7+ "errors"
8+ "testing"
9+
710 "github.com/bnema/wlturbo"
811 "github.com/bnema/wlturbo/protocol/xdgshell"
9- "testing"
1012 )
1113
12-// bindAnnounced selects a supported version using the registry's announcement.
13-func bindAnnounced(d *wlturbo.Display, iface string, supported uint32, proxy wlturbo.Proxy) (uint32, error) {
14- g, ok := d.Registry().FindGlobal(iface)
15- if !ok {
16- return 0, nil
17- }
18- version := g.Version
19- if version > supported {
20- version = supported
21- }
22- if version == 0 {
23- return 0, nil
24- }
25- return version, d.Registry().Bind(g.Name, iface, version, proxy)
26-}
2714 func TestAnnouncedVersionNegotiation(t *testing.T) {
2815 c, p := pair(t)
2916 d, e := wlturbo.ConnectFromConn(c)
@@ -31,9 +18,12 @@ func TestAnnouncedVersionNegotiation(t *testing.T) {
3118 defer d.Close()
3219 request(t, p)
3320 iface := xdgshell.XdgWmBaseInterface
34- if v, e := bindAnnounced(d, iface, 6, xdgshell.NewXdgWmBase(d.Context())); e != nil || v != 0 {
21+ if v, e := d.Registry().BindNegotiated(iface, 6, xdgshell.NewXdgWmBase(d.Context())); v != 0 || !errors.Is(e, wlturbo.ErrGlobalNotFound) {
3522 t.Fatalf("missing: %d %v", v, e)
3623 }
24+ if v, e := d.Registry().BindNegotiated(iface, 0, xdgshell.NewXdgWmBase(d.Context())); v != 0 || e == nil || errors.Is(e, wlturbo.ErrGlobalNotFound) {
25+ t.Fatalf("unsupported zero: %d %v", v, e)
26+ }
3727 for i, version := range []uint32{2, 9} {
3828 name := uint32(30 + i)
3929 announcement := append(msg(0, 0, name)[8:], str(iface)...)
@@ -41,7 +31,10 @@ func TestAnnouncedVersionNegotiation(t *testing.T) {
4131 send(t, p, payload(d.Registry().ID(), 0, announcement...))
4232 noerr(t, d.Dispatch())
4333 proxy := xdgshell.NewXdgWmBase(d.Context())
44- negotiated, e := bindAnnounced(d, iface, 6, proxy)
34+ if v, e := d.Registry().BindNegotiated(iface, 0, proxy); v != 0 || e == nil || errors.Is(e, wlturbo.ErrGlobalNotFound) {
35+ t.Fatalf("supported zero with global: %d %v", v, e)
36+ }
37+ negotiated, e := d.Registry().BindNegotiated(iface, 6, proxy)
4538 noerr(t, e)
4639 want := version
4740 if want > 6 {
@@ -4,26 +4,13 @@ package protocol_test
4 4
5 import (5 import (
6 "encoding/binary"6 "encoding/binary"
7+ "errors"
8+ "testing"
9+
7 "github.com/bnema/wlturbo"10 "github.com/bnema/wlturbo"
8 "github.com/bnema/wlturbo/protocol/xdgshell"11 "github.com/bnema/wlturbo/protocol/xdgshell"
9- "testing"
10 )12 )
11 13
12-// bindAnnounced selects a supported version using the registry's announcement.
13-func bindAnnounced(d *wlturbo.Display, iface string, supported uint32, proxy wlturbo.Proxy) (uint32, error) {
14- g, ok := d.Registry().FindGlobal(iface)
15- if !ok {
16- return 0, nil
17- }
18- version := g.Version
19- if version > supported {
20- version = supported
21- }
22- if version == 0 {
23- return 0, nil
24- }
25- return version, d.Registry().Bind(g.Name, iface, version, proxy)
26-}
27 func TestAnnouncedVersionNegotiation(t *testing.T) {14 func TestAnnouncedVersionNegotiation(t *testing.T) {
28 c, p := pair(t)15 c, p := pair(t)
29 d, e := wlturbo.ConnectFromConn(c)16 d, e := wlturbo.ConnectFromConn(c)
@@ -31,9 +18,12 @@ func TestAnnouncedVersionNegotiation(t *testing.T) {
31 defer d.Close()18 defer d.Close()
32 request(t, p)19 request(t, p)
33 iface := xdgshell.XdgWmBaseInterface20 iface := xdgshell.XdgWmBaseInterface
34- if v, e := bindAnnounced(d, iface, 6, xdgshell.NewXdgWmBase(d.Context())); e != nil || v != 0 {21+ if v, e := d.Registry().BindNegotiated(iface, 6, xdgshell.NewXdgWmBase(d.Context())); v != 0 || !errors.Is(e, wlturbo.ErrGlobalNotFound) {
35 t.Fatalf("missing: %d %v", v, e)22 t.Fatalf("missing: %d %v", v, e)
36 }23 }
24+ if v, e := d.Registry().BindNegotiated(iface, 0, xdgshell.NewXdgWmBase(d.Context())); v != 0 || e == nil || errors.Is(e, wlturbo.ErrGlobalNotFound) {
25+ t.Fatalf("unsupported zero: %d %v", v, e)
26+ }
37 for i, version := range []uint32{2, 9} {27 for i, version := range []uint32{2, 9} {
38 name := uint32(30 + i)28 name := uint32(30 + i)
39 announcement := append(msg(0, 0, name)[8:], str(iface)...)29 announcement := append(msg(0, 0, name)[8:], str(iface)...)
@@ -41,7 +31,10 @@ func TestAnnouncedVersionNegotiation(t *testing.T) {
41 send(t, p, payload(d.Registry().ID(), 0, announcement...))31 send(t, p, payload(d.Registry().ID(), 0, announcement...))
42 noerr(t, d.Dispatch())32 noerr(t, d.Dispatch())
43 proxy := xdgshell.NewXdgWmBase(d.Context())33 proxy := xdgshell.NewXdgWmBase(d.Context())
44- negotiated, e := bindAnnounced(d, iface, 6, proxy)34+ if v, e := d.Registry().BindNegotiated(iface, 0, proxy); v != 0 || e == nil || errors.Is(e, wlturbo.ErrGlobalNotFound) {
35+ t.Fatalf("supported zero with global: %d %v", v, e)
36+ }
37+ negotiated, e := d.Registry().BindNegotiated(iface, 6, proxy)
45 noerr(t, e)38 noerr(t, e)
46 want := version39 want := version
47 if want > 6 {40 if want > 6 {

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 8

↑↓ moveenter openesc close