detached 20a3136

feat(gen): port client generator from wlturbo scanner

commit
20a31360f3698ba20900f972592fe275bb25522e
author
brice · · 2026-10-01 03:28 UTC
parent
891299a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +2047 -0
A internal/gen/client/equivalence_test.go +123 -0
@@ -0,0 +1,123 @@
1+package client
2+
3+import (
4+ "bytes"
5+ "os"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+// oldWlturboEnv names the environment variable holding a checkout of the
12+// wlturbo repository whose checked-in bindings are the reference output.
13+const oldWlturboEnv = "WGB_OLD_WLTURBO"
14+
15+// TestEquivalenceWithWlturboScanner regenerates every binding that the old
16+// wlturbo repository checks in, using the same XML, package name and -import
17+// mappings as its //go:generate lines, and requires byte-identical output.
18+// It is skipped unless WGB_OLD_WLTURBO is set.
19+func TestEquivalenceWithWlturboScanner(t *testing.T) {
20+ oldRoot := os.Getenv(oldWlturboEnv)
21+ if oldRoot == "" {
22+ t.Skipf("%s not set", oldWlturboEnv)
23+ }
24+ root := filepath.Join(oldRoot, "protocol")
25+ checked := 0
26+ err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
27+ if err != nil {
28+ return err
29+ }
30+ if entry.IsDir() || entry.Name() != "generate.go" {
31+ return nil
32+ }
33+ content, err := os.ReadFile(path)
34+ if err != nil {
35+ return err
36+ }
37+ for _, line := range strings.Split(string(content), "\n") {
38+ if !strings.HasPrefix(line, "//go:generate go run ") {
39+ continue
40+ }
41+ fields := strings.Fields(line)
42+ if len(fields) < 5 || !strings.HasSuffix(fields[3], "/cmd/wlturbo-scanner") {
43+ t.Fatalf("%s: expected direct go run of cmd/wlturbo-scanner", path)
44+ }
45+ cmd := parseGenerateArgs(t, path, fields[4:])
46+ dir := filepath.Dir(path)
47+ checked++
48+ t.Run(cmd.pkg, func(t *testing.T) {
49+ xmlPath := filepath.Join(dir, cmd.input)
50+ data, err := os.ReadFile(xmlPath)
51+ if err != nil {
52+ t.Fatal(err)
53+ }
54+ s := NewScanner()
55+ s.Tool = "wlturbo-scanner"
56+ s.CrossPackage = cmd.imports
57+ if err := s.Load(data, filepath.Base(xmlPath)); err != nil {
58+ t.Fatal(err)
59+ }
60+ generated, err := s.Generate(cmd.pkg)
61+ if err != nil {
62+ t.Fatal(err)
63+ }
64+ checkedIn, err := os.ReadFile(filepath.Join(dir, cmd.output))
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ if !bytes.Equal(generated, checkedIn) {
69+ t.Fatalf("output differs from %s", filepath.Join(dir, cmd.output))
70+ }
71+ })
72+ }
73+ return nil
74+ })
75+ if err != nil {
76+ t.Fatal(err)
77+ }
78+ if checked == 0 {
79+ t.Fatalf("no //go:generate lines found under %s", root)
80+ }
81+ t.Logf("compared %d generated files", checked)
82+}
83+
84+type generateCommand struct {
85+ pkg, output, input string
86+ imports map[string]string
87+}
88+
89+func parseGenerateArgs(t *testing.T, path string, args []string) generateCommand {
90+ t.Helper()
91+ cmd := generateCommand{imports: map[string]string{}}
92+ for i := 0; i < len(args); i++ {
93+ switch args[i] {
94+ case "-p", "-o", "-import":
95+ if i+1 == len(args) {
96+ t.Fatalf("%s: missing value for %s", path, args[i])
97+ }
98+ flag, value := args[i], args[i+1]
99+ i++
100+ switch flag {
101+ case "-p":
102+ cmd.pkg = value
103+ case "-o":
104+ cmd.output = value
105+ case "-import":
106+ iface, target, ok := strings.Cut(value, "=")
107+ if !ok {
108+ t.Fatalf("%s: invalid import %q", path, value)
109+ }
110+ cmd.imports[iface] = target
111+ }
112+ default:
113+ if strings.HasPrefix(args[i], "-") || cmd.input != "" {
114+ t.Fatalf("%s: unexpected generation argument %q", path, args[i])
115+ }
116+ cmd.input = args[i]
117+ }
118+ }
119+ if cmd.pkg == "" || cmd.output == "" || cmd.input == "" {
120+ t.Fatalf("%s: incomplete generation command", path)
121+ }
122+ return cmd
123+}
@@ -0,0 +1,123 @@
1+package client
2+
3+import (
4+ "bytes"
5+ "os"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+// oldWlturboEnv names the environment variable holding a checkout of the
12+// wlturbo repository whose checked-in bindings are the reference output.
13+const oldWlturboEnv = "WGB_OLD_WLTURBO"
14+
15+// TestEquivalenceWithWlturboScanner regenerates every binding that the old
16+// wlturbo repository checks in, using the same XML, package name and -import
17+// mappings as its //go:generate lines, and requires byte-identical output.
18+// It is skipped unless WGB_OLD_WLTURBO is set.
19+func TestEquivalenceWithWlturboScanner(t *testing.T) {
20+ oldRoot := os.Getenv(oldWlturboEnv)
21+ if oldRoot == "" {
22+ t.Skipf("%s not set", oldWlturboEnv)
23+ }
24+ root := filepath.Join(oldRoot, "protocol")
25+ checked := 0
26+ err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
27+ if err != nil {
28+ return err
29+ }
30+ if entry.IsDir() || entry.Name() != "generate.go" {
31+ return nil
32+ }
33+ content, err := os.ReadFile(path)
34+ if err != nil {
35+ return err
36+ }
37+ for _, line := range strings.Split(string(content), "\n") {
38+ if !strings.HasPrefix(line, "//go:generate go run ") {
39+ continue
40+ }
41+ fields := strings.Fields(line)
42+ if len(fields) < 5 || !strings.HasSuffix(fields[3], "/cmd/wlturbo-scanner") {
43+ t.Fatalf("%s: expected direct go run of cmd/wlturbo-scanner", path)
44+ }
45+ cmd := parseGenerateArgs(t, path, fields[4:])
46+ dir := filepath.Dir(path)
47+ checked++
48+ t.Run(cmd.pkg, func(t *testing.T) {
49+ xmlPath := filepath.Join(dir, cmd.input)
50+ data, err := os.ReadFile(xmlPath)
51+ if err != nil {
52+ t.Fatal(err)
53+ }
54+ s := NewScanner()
55+ s.Tool = "wlturbo-scanner"
56+ s.CrossPackage = cmd.imports
57+ if err := s.Load(data, filepath.Base(xmlPath)); err != nil {
58+ t.Fatal(err)
59+ }
60+ generated, err := s.Generate(cmd.pkg)
61+ if err != nil {
62+ t.Fatal(err)
63+ }
64+ checkedIn, err := os.ReadFile(filepath.Join(dir, cmd.output))
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ if !bytes.Equal(generated, checkedIn) {
69+ t.Fatalf("output differs from %s", filepath.Join(dir, cmd.output))
70+ }
71+ })
72+ }
73+ return nil
74+ })
75+ if err != nil {
76+ t.Fatal(err)
77+ }
78+ if checked == 0 {
79+ t.Fatalf("no //go:generate lines found under %s", root)
80+ }
81+ t.Logf("compared %d generated files", checked)
82+}
83+
84+type generateCommand struct {
85+ pkg, output, input string
86+ imports map[string]string
87+}
88+
89+func parseGenerateArgs(t *testing.T, path string, args []string) generateCommand {
90+ t.Helper()
91+ cmd := generateCommand{imports: map[string]string{}}
92+ for i := 0; i < len(args); i++ {
93+ switch args[i] {
94+ case "-p", "-o", "-import":
95+ if i+1 == len(args) {
96+ t.Fatalf("%s: missing value for %s", path, args[i])
97+ }
98+ flag, value := args[i], args[i+1]
99+ i++
100+ switch flag {
101+ case "-p":
102+ cmd.pkg = value
103+ case "-o":
104+ cmd.output = value
105+ case "-import":
106+ iface, target, ok := strings.Cut(value, "=")
107+ if !ok {
108+ t.Fatalf("%s: invalid import %q", path, value)
109+ }
110+ cmd.imports[iface] = target
111+ }
112+ default:
113+ if strings.HasPrefix(args[i], "-") || cmd.input != "" {
114+ t.Fatalf("%s: unexpected generation argument %q", path, args[i])
115+ }
116+ cmd.input = args[i]
117+ }
118+ }
119+ if cmd.pkg == "" || cmd.output == "" || cmd.input == "" {
120+ t.Fatalf("%s: incomplete generation command", path)
121+ }
122+ return cmd
123+}
A internal/gen/client/license_test.go +53 -0
@@ -0,0 +1,53 @@
1+package client
2+
3+import (
4+ "crypto/sha256"
5+ "fmt"
6+ "os"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestIncludedLGPLLicense(t *testing.T) {
12+ content, err := os.ReadFile("../../../LICENSES/LGPL-2.1-or-later.txt")
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ const expected = "5749785c8bdefafcb5d798270ed0a967036fe2ca63dcedade1627565dfef81d2"
17+ if got := fmt.Sprintf("%x", sha256.Sum256(content)); got != expected {
18+ t.Fatalf("LGPL text checksum %s, want %s", got, expected)
19+ }
20+}
21+
22+func TestGeneratedSourceRetainsProtocolLicense(t *testing.T) {
23+ for _, source := range []string{
24+ "testdata/server-decoration.xml",
25+ "testdata/wlr-layer-shell-unstable-v1.xml",
26+ "../../../spec/testdata/wayland.xml",
27+ } {
28+ t.Run(source, func(t *testing.T) {
29+ s := NewScanner()
30+ if err := s.ParseXML(source); err != nil {
31+ t.Fatal(err)
32+ }
33+ s.CrossPackage = map[string]string{
34+ "wl_surface": "github.com/bnema/go-wayland-bindings/client/wayland",
35+ "wl_output": "github.com/bnema/go-wayland-bindings/client/wayland",
36+ "xdg_popup": "github.com/bnema/go-wayland-bindings/client/xdgshell",
37+ }
38+ generated, err := s.Generate("wayland")
39+ if err != nil {
40+ t.Fatal(err)
41+ }
42+ header, _, ok := strings.Cut(string(generated), "\npackage wayland")
43+ if !ok {
44+ t.Fatal("package declaration missing")
45+ }
46+ for _, line := range strings.Split(strings.TrimSpace(s.protocol.Copyright), "\n") {
47+ if text := strings.TrimSpace(line); text != "" && !strings.Contains(header, text) {
48+ t.Errorf("upstream license line missing from header: %q", text)
49+ }
50+ }
51+ })
52+ }
53+}
@@ -0,0 +1,53 @@
1+package client
2+
3+import (
4+ "crypto/sha256"
5+ "fmt"
6+ "os"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestIncludedLGPLLicense(t *testing.T) {
12+ content, err := os.ReadFile("../../../LICENSES/LGPL-2.1-or-later.txt")
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ const expected = "5749785c8bdefafcb5d798270ed0a967036fe2ca63dcedade1627565dfef81d2"
17+ if got := fmt.Sprintf("%x", sha256.Sum256(content)); got != expected {
18+ t.Fatalf("LGPL text checksum %s, want %s", got, expected)
19+ }
20+}
21+
22+func TestGeneratedSourceRetainsProtocolLicense(t *testing.T) {
23+ for _, source := range []string{
24+ "testdata/server-decoration.xml",
25+ "testdata/wlr-layer-shell-unstable-v1.xml",
26+ "../../../spec/testdata/wayland.xml",
27+ } {
28+ t.Run(source, func(t *testing.T) {
29+ s := NewScanner()
30+ if err := s.ParseXML(source); err != nil {
31+ t.Fatal(err)
32+ }
33+ s.CrossPackage = map[string]string{
34+ "wl_surface": "github.com/bnema/go-wayland-bindings/client/wayland",
35+ "wl_output": "github.com/bnema/go-wayland-bindings/client/wayland",
36+ "xdg_popup": "github.com/bnema/go-wayland-bindings/client/xdgshell",
37+ }
38+ generated, err := s.Generate("wayland")
39+ if err != nil {
40+ t.Fatal(err)
41+ }
42+ header, _, ok := strings.Cut(string(generated), "\npackage wayland")
43+ if !ok {
44+ t.Fatal("package declaration missing")
45+ }
46+ for _, line := range strings.Split(strings.TrimSpace(s.protocol.Copyright), "\n") {
47+ if text := strings.TrimSpace(line); text != "" && !strings.Contains(header, text) {
48+ t.Errorf("upstream license line missing from header: %q", text)
49+ }
50+ }
51+ })
52+ }
53+}
A internal/gen/client/options_test.go +83 -0
@@ -0,0 +1,83 @@
1+package client
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+const optionsXML = `<protocol name="opt"><copyright>Copyright test
9+SPDX-License-Identifier: MIT</copyright><interface name="opt_thing" version="1"/></protocol>`
10+
11+func generateOptions(t *testing.T, mutate func(*Scanner)) string {
12+ t.Helper()
13+ s := NewScanner()
14+ if err := s.Load([]byte(optionsXML), "opt.xml"); err != nil {
15+ t.Fatal(err)
16+ }
17+ if mutate != nil {
18+ mutate(s)
19+ }
20+ b, err := s.Generate("opt")
21+ if err != nil {
22+ t.Fatal(err)
23+ }
24+ return string(b)
25+}
26+
27+func TestToolNameInHeader(t *testing.T) {
28+ if got := generateOptions(t, nil); !strings.HasPrefix(got, "// Code generated by wlbgen. DO NOT EDIT.\n") {
29+ t.Fatalf("default header: %q", strings.SplitN(got, "\n", 2)[0])
30+ }
31+ got := generateOptions(t, func(s *Scanner) { s.Tool = "wlturbo-scanner" })
32+ if !strings.HasPrefix(got, "// Code generated by wlturbo-scanner. DO NOT EDIT.\n") {
33+ t.Fatalf("custom header: %q", strings.SplitN(got, "\n", 2)[0])
34+ }
35+}
36+
37+func TestSPDXLineOnlyWhenRequested(t *testing.T) {
38+ const line = "// SPDX-License-Identifier: LGPL-2.1-or-later\n"
39+ if got := generateOptions(t, nil); strings.Contains(got, line) {
40+ t.Fatal("SPDX line emitted without request")
41+ }
42+ got := generateOptions(t, func(s *Scanner) { s.SPDXLicense = "LGPL-2.1-or-later" })
43+ header, _, _ := strings.Cut(got, "\npackage opt")
44+ if strings.Count(header, line) != 1 || !strings.Contains(header, "// Copyright test") {
45+ t.Fatalf("header: %s", header)
46+ }
47+}
48+
49+func TestSPDXNotDuplicatedWhenCopyrightHasIt(t *testing.T) {
50+ got := generateOptions(t, func(s *Scanner) { s.SPDXLicense = "MIT" })
51+ if n := strings.Count(got, "SPDX-License-Identifier: MIT"); n != 1 {
52+ t.Fatalf("SPDX line count = %d", n)
53+ }
54+}
55+
56+func TestCoreDetectionFollowsWlDisplay(t *testing.T) {
57+ const xml = `<protocol name="x"><interface name="wl_display" version="1"/><interface name="wl_registry" version="1"/><interface name="wl_thing" version="1"/></protocol>`
58+ for _, pkg := range []string{"wayland", "core", "anything"} {
59+ s := NewScanner()
60+ if err := s.Load([]byte(xml), "x.xml"); err != nil {
61+ t.Fatal(err)
62+ }
63+ b, err := s.Generate(pkg)
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") || !strings.Contains(string(b), "type Thing struct") {
68+ t.Fatalf("package %s: bootstrap types not skipped", pkg)
69+ }
70+ }
71+ // A package named "core" that does not define wl_display is not core.
72+ s := NewScanner()
73+ if err := s.Load([]byte(`<protocol name="x"><interface name="wl_registry" version="1"/></protocol>`), "x.xml"); err != nil {
74+ t.Fatal(err)
75+ }
76+ b, err := s.Generate("core")
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ if !strings.Contains(string(b), "type Registry struct") {
81+ t.Fatal("core name alone must not trigger core handling")
82+ }
83+}
@@ -0,0 +1,83 @@
1+package client
2+
3+import (
4+ "strings"
5+ "testing"
6+)
7+
8+const optionsXML = `<protocol name="opt"><copyright>Copyright test
9+SPDX-License-Identifier: MIT</copyright><interface name="opt_thing" version="1"/></protocol>`
10+
11+func generateOptions(t *testing.T, mutate func(*Scanner)) string {
12+ t.Helper()
13+ s := NewScanner()
14+ if err := s.Load([]byte(optionsXML), "opt.xml"); err != nil {
15+ t.Fatal(err)
16+ }
17+ if mutate != nil {
18+ mutate(s)
19+ }
20+ b, err := s.Generate("opt")
21+ if err != nil {
22+ t.Fatal(err)
23+ }
24+ return string(b)
25+}
26+
27+func TestToolNameInHeader(t *testing.T) {
28+ if got := generateOptions(t, nil); !strings.HasPrefix(got, "// Code generated by wlbgen. DO NOT EDIT.\n") {
29+ t.Fatalf("default header: %q", strings.SplitN(got, "\n", 2)[0])
30+ }
31+ got := generateOptions(t, func(s *Scanner) { s.Tool = "wlturbo-scanner" })
32+ if !strings.HasPrefix(got, "// Code generated by wlturbo-scanner. DO NOT EDIT.\n") {
33+ t.Fatalf("custom header: %q", strings.SplitN(got, "\n", 2)[0])
34+ }
35+}
36+
37+func TestSPDXLineOnlyWhenRequested(t *testing.T) {
38+ const line = "// SPDX-License-Identifier: LGPL-2.1-or-later\n"
39+ if got := generateOptions(t, nil); strings.Contains(got, line) {
40+ t.Fatal("SPDX line emitted without request")
41+ }
42+ got := generateOptions(t, func(s *Scanner) { s.SPDXLicense = "LGPL-2.1-or-later" })
43+ header, _, _ := strings.Cut(got, "\npackage opt")
44+ if strings.Count(header, line) != 1 || !strings.Contains(header, "// Copyright test") {
45+ t.Fatalf("header: %s", header)
46+ }
47+}
48+
49+func TestSPDXNotDuplicatedWhenCopyrightHasIt(t *testing.T) {
50+ got := generateOptions(t, func(s *Scanner) { s.SPDXLicense = "MIT" })
51+ if n := strings.Count(got, "SPDX-License-Identifier: MIT"); n != 1 {
52+ t.Fatalf("SPDX line count = %d", n)
53+ }
54+}
55+
56+func TestCoreDetectionFollowsWlDisplay(t *testing.T) {
57+ const xml = `<protocol name="x"><interface name="wl_display" version="1"/><interface name="wl_registry" version="1"/><interface name="wl_thing" version="1"/></protocol>`
58+ for _, pkg := range []string{"wayland", "core", "anything"} {
59+ s := NewScanner()
60+ if err := s.Load([]byte(xml), "x.xml"); err != nil {
61+ t.Fatal(err)
62+ }
63+ b, err := s.Generate(pkg)
64+ if err != nil {
65+ t.Fatal(err)
66+ }
67+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") || !strings.Contains(string(b), "type Thing struct") {
68+ t.Fatalf("package %s: bootstrap types not skipped", pkg)
69+ }
70+ }
71+ // A package named "core" that does not define wl_display is not core.
72+ s := NewScanner()
73+ if err := s.Load([]byte(`<protocol name="x"><interface name="wl_registry" version="1"/></protocol>`), "x.xml"); err != nil {
74+ t.Fatal(err)
75+ }
76+ b, err := s.Generate("core")
77+ if err != nil {
78+ t.Fatal(err)
79+ }
80+ if !strings.Contains(string(b), "type Registry struct") {
81+ t.Fatal("core name alone must not trigger core handling")
82+ }
83+}
A internal/gen/client/requestargs_test.go +239 -0
@@ -0,0 +1,239 @@
1+package client
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+const requestArgsXML = `<protocol name="argsfix">
12+<interface name="args_obj" version="1">
13+ <request name="mixed">
14+ <arg name="serial" type="uint"/><arg name="dx" type="int"/><arg name="fx" type="fixed"/>
15+ <arg name="name" type="string"/><arg name="blob" type="array"/>
16+ <arg name="peer" type="object" interface="args_obj" allow-null="true"/>
17+ </request>
18+ <request name="make"><arg name="id" type="new_id" interface="args_obj"/><arg name="n" type="uint"/></request>
19+ <request name="give"><arg name="tag" type="string"/><arg name="fd" type="fd"/><arg name="size" type="uint"/></request>
20+ <request name="destroy" type="destructor"/>
21+</interface></protocol>`
22+
23+func generateArgsFixture(t *testing.T) string {
24+ t.Helper()
25+ path := filepath.Join(t.TempDir(), "argsfix.xml")
26+ if err := os.WriteFile(path, []byte(requestArgsXML), 0600); err != nil {
27+ t.Fatal(err)
28+ }
29+ s := NewScanner()
30+ if err := s.ParseXML(path); err != nil {
31+ t.Fatal(err)
32+ }
33+ b, err := s.Generate("argsfix")
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ return string(b)
38+}
39+
40+// Generated requests must use typed arguments only: no interface{} boxing
41+// path and no uintptr placeholder for descriptors.
42+func TestGeneratedRequestsUseTypedArgs(t *testing.T) {
43+ text := generateArgsFixture(t)
44+ for _, want := range []string{
45+ "RequestArgs(wl.Request{Proxy: o, Opcode: 0, Name: \"args_obj.mixed\"}, wl.ArgUint(serial), wl.ArgInt(dx), wl.ArgFixed(fx), wl.ArgString(name), wl.ArgArray(blob), wl.ArgObject(arg5))",
46+ "Child: child}, wl.ArgObject(child), wl.ArgUint(n))",
47+ "FDs: []int{fd}}, wl.ArgString(tag), wl.ArgFD(), wl.ArgUint(size))",
48+ "Destructor: true})",
49+ } {
50+ if !strings.Contains(text, want) {
51+ t.Errorf("generated code missing %q\n%s", want, text)
52+ }
53+ }
54+ for _, bad := range []string{".Request(wl.Request", "uintptr("} {
55+ if strings.Contains(text, bad) {
56+ t.Errorf("generated code still contains %q", bad)
57+ }
58+ }
59+}
60+
61+// The generated requests compile against the transport, put the exact wire
62+// bytes on the socket, and do not allocate for realistic numeric values.
63+func TestGeneratedRequestsWireAndAllocations(t *testing.T) {
64+ goBin, err := exec.LookPath("go")
65+ if err != nil {
66+ t.Skip("go unavailable")
67+ }
68+ dir := t.TempDir()
69+ write := func(name, content string) {
70+ if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0600); err != nil {
71+ t.Fatal(err)
72+ }
73+ }
74+ write("args.go", generateArgsFixture(t))
75+ writeTempModule(t, dir, "example.com/argsfix")
76+ write("args_test.go", `package argsfix
77+
78+import (
79+ "bytes"
80+ "net"
81+ "os"
82+ "testing"
83+ "time"
84+
85+ "github.com/bnema/wlturbo"
86+ "golang.org/x/sys/unix"
87+)
88+
89+type sink struct{ last []byte }
90+
91+func (c *sink) Read([]byte) (int, error) { return 0, net.ErrClosed }
92+func (c *sink) Write(p []byte) (int, error) {
93+ c.last = append(c.last[:0], p...)
94+ return len(p), nil
95+}
96+func (c *sink) Close() error { return nil }
97+func (c *sink) LocalAddr() net.Addr { return a{} }
98+func (c *sink) RemoteAddr() net.Addr { return a{} }
99+func (c *sink) SetDeadline(time.Time) error { return nil }
100+func (c *sink) SetReadDeadline(time.Time) error { return nil }
101+func (c *sink) SetWriteDeadline(time.Time) error { return nil }
102+
103+type a struct{}
104+
105+func (a) Network() string { return "t" }
106+func (a) String() string { return "t" }
107+
108+func setup(t testing.TB) (*wlturbo.Display, *sink, *ArgsObj) {
109+ c := &sink{}
110+ d, err := wlturbo.ConnectFromConn(c)
111+ if err != nil {
112+ t.Fatal(err)
113+ }
114+ o := NewArgsObj(d.Context())
115+ o.SetID(d.AllocateID())
116+ d.Context().Register(o)
117+ return d, c, o
118+}
119+
120+func TestMixedWire(t *testing.T) {
121+ _, c, o := setup(t)
122+ peer := NewArgsObj(o.Context())
123+ peer.SetID(0x1234)
124+ if err := o.Mixed(1000, -2, wlturbo.Fixed(512), "ab", []byte{9, 8, 7}, peer); err != nil {
125+ t.Fatal(err)
126+ }
127+ want := []byte{
128+ byte(o.ID()), 0, 0, 0, 0, 0, 40, 0,
129+ 0xe8, 3, 0, 0,
130+ 0xfe, 0xff, 0xff, 0xff,
131+ 0, 2, 0, 0,
132+ 3, 0, 0, 0, 'a', 'b', 0, 0,
133+ 3, 0, 0, 0, 9, 8, 7, 0,
134+ 0x34, 0x12, 0, 0,
135+ }
136+ if !bytes.Equal(c.last, want) {
137+ t.Fatalf("got %x want %x", c.last, want)
138+ }
139+ if err := o.Mixed(1, 2, 3, "", nil, nil); err != nil {
140+ t.Fatal(err)
141+ }
142+ if got := c.last[len(c.last)-4:]; !bytes.Equal(got, []byte{0, 0, 0, 0}) {
143+ t.Fatalf("nil object = %x", got)
144+ }
145+}
146+
147+func TestMixedAllocFree(t *testing.T) {
148+ _, _, o := setup(t)
149+ blob := []byte("blob")
150+ n := testing.AllocsPerRun(200, func() {
151+ if err := o.Mixed(123456, -70000, wlturbo.Fixed(1<<20), "hello", blob, nil); err != nil {
152+ t.Fatal(err)
153+ }
154+ })
155+ if n != 0 {
156+ t.Fatalf("Mixed: %v allocs/op, want 0", n)
157+ }
158+}
159+
160+func TestChildAndDestructor(t *testing.T) {
161+ d, c, o := setup(t)
162+ kid, err := o.Make(77777)
163+ if err != nil {
164+ t.Fatal(err)
165+ }
166+ if kid.ID() == 0 || c.last[8] != byte(kid.ID()) {
167+ t.Fatalf("child id %d not on the wire: %x", kid.ID(), c.last)
168+ }
169+ if err := o.Destroy(); err != nil {
170+ t.Fatal(err)
171+ }
172+ n := len(c.last)
173+ c.last = c.last[:0]
174+ if err := o.Destroy(); err == nil {
175+ t.Fatal("second destructor succeeded")
176+ }
177+ if len(c.last) != 0 || n == 0 {
178+ t.Fatal("wrote after destructor")
179+ }
180+ _ = d
181+}
182+
183+func TestFDRequest(t *testing.T) {
184+ sp, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
185+ if err != nil {
186+ t.Fatal(err)
187+ }
188+ fa, fb := os.NewFile(uintptr(sp[0]), "a"), os.NewFile(uintptr(sp[1]), "b")
189+ ca, err := net.FileConn(fa)
190+ fa.Close()
191+ if err != nil {
192+ fb.Close()
193+ t.Fatal(err)
194+ }
195+ defer ca.Close()
196+ cb, err := net.FileConn(fb)
197+ fb.Close()
198+ if err != nil {
199+ t.Fatal(err)
200+ }
201+ defer cb.Close()
202+ d, err := wlturbo.ConnectFromConn(ca)
203+ if err != nil {
204+ t.Fatal(err)
205+ }
206+ defer d.Close()
207+ o := NewArgsObj(d.Context())
208+ o.SetID(d.AllocateID())
209+ d.Context().Register(o)
210+ fd, err := unix.Open(os.DevNull, unix.O_RDONLY|unix.O_CLOEXEC, 0)
211+ if err != nil {
212+ t.Fatal(err)
213+ }
214+ if err := o.Give("t", fd, 5); err != nil {
215+ t.Fatal(err)
216+ }
217+ if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err == nil {
218+ t.Fatal("fd not closed after send")
219+ }
220+ // The connection first sends wl_display.get_registry (12 bytes).
221+ buf := make([]byte, 12+8+8+4)
222+ oob := make([]byte, unix.CmsgSpace(4))
223+ n, oobn, _, _, err := cb.(*net.UnixConn).ReadMsgUnix(buf, oob)
224+ if err != nil || n != len(buf) || oobn == 0 {
225+ t.Fatalf("n=%d oobn=%d err=%v", n, oobn, err)
226+ }
227+ // The descriptor has no body word: string(8) + uint(4) only.
228+ if buf[12+4] != 2 || buf[12+8+8] != 5 || buf[12+6] != 20 {
229+ t.Fatalf("message = %x", buf[12:])
230+ }
231+}
232+`)
233+ cmd := exec.Command(goBin, "test", "./...")
234+ cmd.Dir = dir
235+ cmd.Env = tempModuleEnv()
236+ if out, err := cmd.CombinedOutput(); err != nil {
237+ t.Fatalf("generated request test: %v\n%s", err, out)
238+ }
239+}
@@ -0,0 +1,239 @@
1+package client
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+const requestArgsXML = `<protocol name="argsfix">
12+<interface name="args_obj" version="1">
13+ <request name="mixed">
14+ <arg name="serial" type="uint"/><arg name="dx" type="int"/><arg name="fx" type="fixed"/>
15+ <arg name="name" type="string"/><arg name="blob" type="array"/>
16+ <arg name="peer" type="object" interface="args_obj" allow-null="true"/>
17+ </request>
18+ <request name="make"><arg name="id" type="new_id" interface="args_obj"/><arg name="n" type="uint"/></request>
19+ <request name="give"><arg name="tag" type="string"/><arg name="fd" type="fd"/><arg name="size" type="uint"/></request>
20+ <request name="destroy" type="destructor"/>
21+</interface></protocol>`
22+
23+func generateArgsFixture(t *testing.T) string {
24+ t.Helper()
25+ path := filepath.Join(t.TempDir(), "argsfix.xml")
26+ if err := os.WriteFile(path, []byte(requestArgsXML), 0600); err != nil {
27+ t.Fatal(err)
28+ }
29+ s := NewScanner()
30+ if err := s.ParseXML(path); err != nil {
31+ t.Fatal(err)
32+ }
33+ b, err := s.Generate("argsfix")
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ return string(b)
38+}
39+
40+// Generated requests must use typed arguments only: no interface{} boxing
41+// path and no uintptr placeholder for descriptors.
42+func TestGeneratedRequestsUseTypedArgs(t *testing.T) {
43+ text := generateArgsFixture(t)
44+ for _, want := range []string{
45+ "RequestArgs(wl.Request{Proxy: o, Opcode: 0, Name: \"args_obj.mixed\"}, wl.ArgUint(serial), wl.ArgInt(dx), wl.ArgFixed(fx), wl.ArgString(name), wl.ArgArray(blob), wl.ArgObject(arg5))",
46+ "Child: child}, wl.ArgObject(child), wl.ArgUint(n))",
47+ "FDs: []int{fd}}, wl.ArgString(tag), wl.ArgFD(), wl.ArgUint(size))",
48+ "Destructor: true})",
49+ } {
50+ if !strings.Contains(text, want) {
51+ t.Errorf("generated code missing %q\n%s", want, text)
52+ }
53+ }
54+ for _, bad := range []string{".Request(wl.Request", "uintptr("} {
55+ if strings.Contains(text, bad) {
56+ t.Errorf("generated code still contains %q", bad)
57+ }
58+ }
59+}
60+
61+// The generated requests compile against the transport, put the exact wire
62+// bytes on the socket, and do not allocate for realistic numeric values.
63+func TestGeneratedRequestsWireAndAllocations(t *testing.T) {
64+ goBin, err := exec.LookPath("go")
65+ if err != nil {
66+ t.Skip("go unavailable")
67+ }
68+ dir := t.TempDir()
69+ write := func(name, content string) {
70+ if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0600); err != nil {
71+ t.Fatal(err)
72+ }
73+ }
74+ write("args.go", generateArgsFixture(t))
75+ writeTempModule(t, dir, "example.com/argsfix")
76+ write("args_test.go", `package argsfix
77+
78+import (
79+ "bytes"
80+ "net"
81+ "os"
82+ "testing"
83+ "time"
84+
85+ "github.com/bnema/wlturbo"
86+ "golang.org/x/sys/unix"
87+)
88+
89+type sink struct{ last []byte }
90+
91+func (c *sink) Read([]byte) (int, error) { return 0, net.ErrClosed }
92+func (c *sink) Write(p []byte) (int, error) {
93+ c.last = append(c.last[:0], p...)
94+ return len(p), nil
95+}
96+func (c *sink) Close() error { return nil }
97+func (c *sink) LocalAddr() net.Addr { return a{} }
98+func (c *sink) RemoteAddr() net.Addr { return a{} }
99+func (c *sink) SetDeadline(time.Time) error { return nil }
100+func (c *sink) SetReadDeadline(time.Time) error { return nil }
101+func (c *sink) SetWriteDeadline(time.Time) error { return nil }
102+
103+type a struct{}
104+
105+func (a) Network() string { return "t" }
106+func (a) String() string { return "t" }
107+
108+func setup(t testing.TB) (*wlturbo.Display, *sink, *ArgsObj) {
109+ c := &sink{}
110+ d, err := wlturbo.ConnectFromConn(c)
111+ if err != nil {
112+ t.Fatal(err)
113+ }
114+ o := NewArgsObj(d.Context())
115+ o.SetID(d.AllocateID())
116+ d.Context().Register(o)
117+ return d, c, o
118+}
119+
120+func TestMixedWire(t *testing.T) {
121+ _, c, o := setup(t)
122+ peer := NewArgsObj(o.Context())
123+ peer.SetID(0x1234)
124+ if err := o.Mixed(1000, -2, wlturbo.Fixed(512), "ab", []byte{9, 8, 7}, peer); err != nil {
125+ t.Fatal(err)
126+ }
127+ want := []byte{
128+ byte(o.ID()), 0, 0, 0, 0, 0, 40, 0,
129+ 0xe8, 3, 0, 0,
130+ 0xfe, 0xff, 0xff, 0xff,
131+ 0, 2, 0, 0,
132+ 3, 0, 0, 0, 'a', 'b', 0, 0,
133+ 3, 0, 0, 0, 9, 8, 7, 0,
134+ 0x34, 0x12, 0, 0,
135+ }
136+ if !bytes.Equal(c.last, want) {
137+ t.Fatalf("got %x want %x", c.last, want)
138+ }
139+ if err := o.Mixed(1, 2, 3, "", nil, nil); err != nil {
140+ t.Fatal(err)
141+ }
142+ if got := c.last[len(c.last)-4:]; !bytes.Equal(got, []byte{0, 0, 0, 0}) {
143+ t.Fatalf("nil object = %x", got)
144+ }
145+}
146+
147+func TestMixedAllocFree(t *testing.T) {
148+ _, _, o := setup(t)
149+ blob := []byte("blob")
150+ n := testing.AllocsPerRun(200, func() {
151+ if err := o.Mixed(123456, -70000, wlturbo.Fixed(1<<20), "hello", blob, nil); err != nil {
152+ t.Fatal(err)
153+ }
154+ })
155+ if n != 0 {
156+ t.Fatalf("Mixed: %v allocs/op, want 0", n)
157+ }
158+}
159+
160+func TestChildAndDestructor(t *testing.T) {
161+ d, c, o := setup(t)
162+ kid, err := o.Make(77777)
163+ if err != nil {
164+ t.Fatal(err)
165+ }
166+ if kid.ID() == 0 || c.last[8] != byte(kid.ID()) {
167+ t.Fatalf("child id %d not on the wire: %x", kid.ID(), c.last)
168+ }
169+ if err := o.Destroy(); err != nil {
170+ t.Fatal(err)
171+ }
172+ n := len(c.last)
173+ c.last = c.last[:0]
174+ if err := o.Destroy(); err == nil {
175+ t.Fatal("second destructor succeeded")
176+ }
177+ if len(c.last) != 0 || n == 0 {
178+ t.Fatal("wrote after destructor")
179+ }
180+ _ = d
181+}
182+
183+func TestFDRequest(t *testing.T) {
184+ sp, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
185+ if err != nil {
186+ t.Fatal(err)
187+ }
188+ fa, fb := os.NewFile(uintptr(sp[0]), "a"), os.NewFile(uintptr(sp[1]), "b")
189+ ca, err := net.FileConn(fa)
190+ fa.Close()
191+ if err != nil {
192+ fb.Close()
193+ t.Fatal(err)
194+ }
195+ defer ca.Close()
196+ cb, err := net.FileConn(fb)
197+ fb.Close()
198+ if err != nil {
199+ t.Fatal(err)
200+ }
201+ defer cb.Close()
202+ d, err := wlturbo.ConnectFromConn(ca)
203+ if err != nil {
204+ t.Fatal(err)
205+ }
206+ defer d.Close()
207+ o := NewArgsObj(d.Context())
208+ o.SetID(d.AllocateID())
209+ d.Context().Register(o)
210+ fd, err := unix.Open(os.DevNull, unix.O_RDONLY|unix.O_CLOEXEC, 0)
211+ if err != nil {
212+ t.Fatal(err)
213+ }
214+ if err := o.Give("t", fd, 5); err != nil {
215+ t.Fatal(err)
216+ }
217+ if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err == nil {
218+ t.Fatal("fd not closed after send")
219+ }
220+ // The connection first sends wl_display.get_registry (12 bytes).
221+ buf := make([]byte, 12+8+8+4)
222+ oob := make([]byte, unix.CmsgSpace(4))
223+ n, oobn, _, _, err := cb.(*net.UnixConn).ReadMsgUnix(buf, oob)
224+ if err != nil || n != len(buf) || oobn == 0 {
225+ t.Fatalf("n=%d oobn=%d err=%v", n, oobn, err)
226+ }
227+ // The descriptor has no body word: string(8) + uint(4) only.
228+ if buf[12+4] != 2 || buf[12+8+8] != 5 || buf[12+6] != 20 {
229+ t.Fatalf("message = %x", buf[12:])
230+ }
231+}
232+`)
233+ cmd := exec.Command(goBin, "test", "./...")
234+ cmd.Dir = dir
235+ cmd.Env = tempModuleEnv()
236+ if out, err := cmd.CombinedOutput(); err != nil {
237+ t.Fatalf("generated request test: %v\n%s", err, out)
238+ }
239+}
A internal/gen/client/scanner.go +723 -0
@@ -0,0 +1,723 @@
1+// Package client generates WLTurbo-backed Go bindings from Wayland protocol
2+// XML files.
3+//
4+// The generated code owns protocol semantics only: it embeds wl.BaseProxy,
5+// allocates and registers child objects through the wl.Context, sends requests
6+// through that context, and dispatches typed events to registered handlers.
7+// Message framing, descriptor passing and connection lifetime stay in the
8+// transport, so a generated binding never opens a socket or writes a header.
9+package client
10+
11+import (
12+ "bytes"
13+ "fmt"
14+ "go/format"
15+ "os"
16+ "path/filepath"
17+ "sort"
18+ "strings"
19+ "text/template"
20+
21+ "github.com/bnema/go-wayland-bindings/spec"
22+)
23+
24+// TransportImport is the import path generated bindings use for the Wayland
25+// transport.
26+const TransportImport = "github.com/bnema/wlturbo/wl"
27+
28+// DefaultTool is the generator name written to the "Code generated" header
29+// when Scanner.Tool is empty.
30+const DefaultTool = "wlbgen"
31+
32+// Scanner generates Go bindings from Wayland protocol XML
33+type Scanner struct {
34+ protocol *spec.Protocol
35+ source string
36+ // CrossPackage maps interface names to Go package import paths. Local wins.
37+ CrossPackage map[string]string
38+ // Tool is the generator name used in the header line
39+ // "// Code generated by <Tool>. DO NOT EDIT." Empty means DefaultTool.
40+ Tool string
41+ // SPDXLicense, when non-empty, makes the header carry a
42+ // "// SPDX-License-Identifier: <SPDXLicense>" line, unless the upstream
43+ // copyright text (which is always retained) already holds that exact line.
44+ // Empty emits no such line.
45+ SPDXLicense string
46+}
47+
48+// NewScanner creates a new protocol scanner
49+func NewScanner() *Scanner {
50+ return &Scanner{Tool: DefaultTool}
51+}
52+
53+// ParseXML parses a Wayland protocol XML file
54+func (s *Scanner) ParseXML(path string) error {
55+ data, err := os.ReadFile(path)
56+ if err != nil {
57+ return fmt.Errorf("failed to read XML file: %w", err)
58+ }
59+ // Only the base name is recorded: generated output must not depend on the
60+ // path the XML happened to be read from.
61+ return s.Load(data, filepath.Base(path))
62+}
63+
64+// Load parses Wayland protocol XML held in memory. sourceName is recorded in
65+// the generated header as the "Source:" line; use the XML file's base name.
66+func (s *Scanner) Load(data []byte, sourceName string) error {
67+ protocol, err := spec.Parse(data)
68+ if err != nil {
69+ return fmt.Errorf("failed to parse XML: %w", err)
70+ }
71+ return s.LoadProtocol(protocol, sourceName)
72+}
73+
74+// LoadProtocol uses an already parsed protocol.
75+func (s *Scanner) LoadProtocol(protocol *spec.Protocol, sourceName string) error {
76+ if protocol == nil {
77+ return fmt.Errorf("nil protocol")
78+ }
79+ s.protocol = protocol
80+ s.source = sourceName
81+ return nil
82+}
83+
84+// Generate creates Go source code for the parsed protocol
85+func (s *Scanner) Generate(packageName string) ([]byte, error) {
86+ if s.protocol == nil {
87+ return nil, fmt.Errorf("no protocol parsed")
88+ }
89+
90+ data, err := s.prepareTemplateData(packageName)
91+ if err != nil {
92+ return nil, err
93+ }
94+
95+ var buf bytes.Buffer
96+ tmpl := template.Must(template.New("protocol").Parse(protocolTemplate))
97+ if err := tmpl.Execute(&buf, data); err != nil {
98+ return nil, fmt.Errorf("failed to execute template: %w", err)
99+ }
100+
101+ formatted, err := format.Source(buf.Bytes())
102+ if err != nil {
103+ return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err)
104+ }
105+ return formatted, nil
106+}
107+
108+// templateData holds data for template generation
109+type templateData struct {
110+ Tool string
111+ SPDX string
112+ Package string
113+ Protocol string
114+ Source string
115+ Copyright string
116+ ImportBlock string
117+ Constants []constantData
118+ Interfaces []interfaceData
119+}
120+
121+type interfaceData struct {
122+ Name string
123+ GoName string
124+ Version int
125+ HasEvents bool
126+ SignatureCases []signatureCase
127+ Requests []requestData
128+ Events []eventData
129+}
130+
131+type signatureCase struct {
132+ Opcode int
133+ Signature string
134+}
135+type requestData struct {
136+ Name string
137+ GoName string
138+ Doc string
139+ Opcode int
140+ Destructor bool
141+ Since int
142+ Params string
143+ Results string
144+ ArgPreparations []string
145+ CreatesChild bool
146+ ChildVar string
147+ ChildType string
148+ ChildFactory string
149+ ErrorReturn string
150+ HasFDs bool
151+ FDList string
152+ ArgList string
153+}
154+
155+// argSlot is one positional argument of a request, in declaration order. A
156+// new_id slot is filled in once the child object has been created.
157+type argSlot struct {
158+ newID bool
159+ expr string
160+}
161+
162+type eventData struct {
163+ HasFD bool
164+ ServerDestroy bool
165+ FDNames []string
166+ Name string
167+ GoName string
168+ Opcode int
169+ HandlerField string
170+ HandlerAccessor string
171+ HandlerParams string
172+ HandlerArgs string
173+ DecodeLines []string
174+}
175+
176+type constantData struct {
177+ Name string
178+ Value string
179+ Type string
180+ Enum string
181+ Entry string
182+}
183+
184+// Only display and registry have handwritten bootstrap bindings.
185+var bootstrapTypes = map[string]bool{
186+ "Display": true,
187+ "Registry": true,
188+}
189+
190+// goKeywords are avoided as parameter names because generated identifiers
191+// must not collide with the language.
192+var goKeywords = map[string]bool{
193+ "break": true, "case": true, "chan": true, "const": true, "continue": true,
194+ "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
195+ "func": true, "go": true, "goto": true, "if": true, "import": true,
196+ "interface": true, "map": true, "package": true, "range": true, "return": true,
197+ "select": true, "struct": true, "switch": true, "type": true, "var": true,
198+}
199+
200+func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
201+ data := templateData{
202+ Tool: s.Tool,
203+ Package: packageName,
204+ Protocol: s.protocol.Name,
205+ Source: s.source,
206+ }
207+ if data.Tool == "" {
208+ data.Tool = DefaultTool
209+ }
210+ if notice := strings.TrimSpace(s.protocol.Copyright); notice != "" {
211+ var comment strings.Builder
212+ for _, line := range strings.Split(notice, "\n") {
213+ comment.WriteString("// ")
214+ comment.WriteString(strings.TrimSpace(line))
215+ comment.WriteByte('\n')
216+ }
217+ data.Copyright = comment.String()
218+ }
219+ if id := strings.TrimSpace(s.SPDXLicense); id != "" {
220+ line := "SPDX-License-Identifier: " + id
221+ retained := false
222+ for _, l := range strings.Split(s.protocol.Copyright, "\n") {
223+ if strings.TrimSpace(l) == line {
224+ retained = true
225+ }
226+ }
227+ if !retained {
228+ data.SPDX = id
229+ }
230+ }
231+
232+ imports := []string{TransportImport}
233+ usedCross := make(map[string]bool)
234+ for _, candidate := range s.protocol.Interfaces {
235+ for _, req := range candidate.Requests {
236+ for _, arg := range req.Args {
237+ if arg.Type == "object" || arg.Type == "new_id" {
238+ usedCross[arg.Interface] = true
239+ }
240+ }
241+ }
242+ for _, ev := range candidate.Events {
243+ for _, arg := range ev.Args {
244+ // Event object references are decoded as IDs, not imported proxies.
245+ if arg.Type == "new_id" {
246+ usedCross[arg.Interface] = true
247+ }
248+ }
249+ }
250+ }
251+ for iface, path := range s.CrossPackage {
252+ local := false
253+ for _, candidate := range s.protocol.Interfaces {
254+ if candidate.Name == iface {
255+ local = true
256+ break
257+ }
258+ }
259+ if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
260+ continue
261+ }
262+ alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
263+ imports = append(imports, alias+" "+path)
264+ }
265+ sort.Strings(imports)
266+ stdImports := []string{}
267+ needsSync := false
268+
269+ // Enum constants are package-level declarations, so two interfaces that
270+ // declare the same enum entry (for example xdg_surface.error.invalid_size
271+ // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
272+ // occurs in more than one interface are qualified with their interface
273+ // name below; unique names keep their historical, unprefixed form.
274+ enumEntryCounts := map[string]int{}
275+ for _, iface := range s.protocol.Interfaces {
276+ for _, enum := range iface.Enums {
277+ for _, entry := range enum.Entries {
278+ enumEntryCounts[constantName(enum.Name, entry.Name)]++
279+ }
280+ }
281+ }
282+
283+ // The protocol that defines wl_display is the core protocol: display and
284+ // registry have handwritten bootstrap bindings in the transport.
285+ definesCore := false
286+ for _, iface := range s.protocol.Interfaces {
287+ if iface.Name == "wl_display" {
288+ definesCore = true
289+ }
290+ }
291+
292+ for _, iface := range s.protocol.Interfaces {
293+ if definesCore && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
294+ continue
295+ }
296+ ifaceData, err := s.processInterface(iface)
297+ if err != nil {
298+ return data, err
299+ }
300+ data.Interfaces = append(data.Interfaces, ifaceData)
301+ if len(iface.Events) > 0 {
302+ needsSync = true
303+ }
304+
305+ for _, enum := range iface.Enums {
306+ for _, entry := range enum.Entries {
307+ name := constantName(enum.Name, entry.Name)
308+ if enumEntryCounts[name] > 1 {
309+ name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
310+ }
311+ // Enum values are emitted untyped: the same enum travels as a uint
312+ // in one protocol and as an int in another, and an untyped constant
313+ // assigns to either without casts at the call site.
314+ data.Constants = append(data.Constants, constantData{
315+ Name: name,
316+ Value: entry.Value,
317+ Enum: enum.Name,
318+ Entry: entry.Name,
319+ })
320+ }
321+ }
322+ }
323+
324+ // Handlers are registered and dispatched from different goroutines, so an
325+ // interface with events needs a mutex.
326+ if needsSync {
327+ stdImports = append(stdImports, "sync")
328+ }
329+
330+ var block strings.Builder
331+ block.WriteString("import (\n")
332+ for _, imp := range stdImports {
333+ fmt.Fprintf(&block, "\t%q\n", imp)
334+ }
335+ if len(stdImports) > 0 && len(imports) > 0 {
336+ block.WriteString("\n")
337+ }
338+ for _, imp := range imports {
339+ if strings.Contains(imp, " ") {
340+ parts := strings.SplitN(imp, " ", 2)
341+ fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
342+ } else {
343+ fmt.Fprintf(&block, "\t%q\n", imp)
344+ }
345+ }
346+ block.WriteString(")")
347+ data.ImportBlock = block.String()
348+
349+ return data, nil
350+}
351+
352+func (s *Scanner) processInterface(iface spec.Interface) (interfaceData, error) {
353+ data := interfaceData{
354+ Name: iface.Name,
355+ GoName: s.toGoName(iface.Name),
356+ Version: iface.Version,
357+ HasEvents: len(iface.Events) > 0,
358+ }
359+
360+ for i, req := range iface.Requests {
361+ request, err := s.processRequest(iface, req, i)
362+ if err != nil {
363+ return data, err
364+ }
365+ data.Requests = append(data.Requests, request)
366+ }
367+
368+ for i, event := range iface.Events {
369+ var sig strings.Builder
370+ for _, arg := range event.Args {
371+ sig.WriteString(arg.Type)
372+ sig.WriteByte(',')
373+ }
374+ data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
375+ eventData, err := s.processEvent(event, i)
376+ if iface.Name == "wl_callback" && event.Name == "done" {
377+ eventData.ServerDestroy = true
378+ }
379+ if err != nil {
380+ return data, err
381+ }
382+ data.Events = append(data.Events, eventData)
383+ }
384+
385+ return data, nil
386+}
387+
388+func (s *Scanner) processRequest(iface spec.Interface, req spec.Message, opcode int) (requestData, error) {
389+ data := requestData{
390+ Name: req.Name,
391+ GoName: s.toGoName(req.Name),
392+ Doc: s.formatDescription(req.Description),
393+ Opcode: opcode,
394+ Destructor: req.Type == "destructor",
395+ Since: req.Since,
396+ ChildVar: "child",
397+ }
398+
399+ var (
400+ params []string
401+ argSlots []argSlot
402+ argExprs []string
403+ fdExprs []string
404+ newIDArg *spec.Arg
405+ )
406+
407+ for i, arg := range req.Args {
408+ if arg.Type == "new_id" {
409+ if newIDArg != nil {
410+ return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
411+ }
412+ newIDArg = &arg
413+ // The child object is created later, but it keeps its declared
414+ // position: the wire order is the argument order.
415+ argSlots = append(argSlots, argSlot{newID: true})
416+ continue
417+ }
418+
419+ name := s.paramName(arg.Name, i)
420+ goType, err := s.goTypeForArg(arg)
421+ if err != nil {
422+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
423+ }
424+
425+ expr := name
426+ switch arg.Type {
427+ case "object":
428+ // Objects are pointer types, and travel as wl.Object so a nil
429+ // pointer is never sent as a non-nil interface holding a typed nil.
430+ params = append(params, name+" *"+goType)
431+ holder := fmt.Sprintf("arg%d", i)
432+ data.ArgPreparations = append(data.ArgPreparations,
433+ fmt.Sprintf("var %s wl.Object", holder),
434+ fmt.Sprintf("if %s != nil {", name),
435+ fmt.Sprintf("\t%s = %s", holder, name),
436+ "}")
437+ expr = "wl.ArgObject(" + holder + ")"
438+
439+ case "fd":
440+ // Descriptors travel out of band: the body carries no value for
441+ // them, and the descriptor list is attached to the request.
442+ params = append(params, name+" int")
443+ fdExprs = append(fdExprs, name)
444+ expr = "wl.ArgFD()"
445+
446+ default:
447+ params = append(params, name+" "+goType)
448+ switch arg.Type {
449+ case "int":
450+ expr = "wl.ArgInt(" + name + ")"
451+ case "uint":
452+ expr = "wl.ArgUint(" + name + ")"
453+ case "fixed":
454+ expr = "wl.ArgFixed(" + name + ")"
455+ case "string":
456+ expr = "wl.ArgString(" + name + ")"
457+ case "array":
458+ expr = "wl.ArgArray(" + name + ")"
459+ }
460+ }
461+
462+ argSlots = append(argSlots, argSlot{expr: expr})
463+ }
464+
465+ if newIDArg != nil {
466+ childType, err := s.goTypeForArg(*newIDArg)
467+ if err != nil {
468+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
469+ }
470+ data.CreatesChild = true
471+ data.ChildType = childType
472+ if strings.Contains(childType, ".") {
473+ data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface)
474+ }
475+ }
476+
477+ for _, slot := range argSlots {
478+ if slot.newID {
479+ argExprs = append(argExprs, "wl.ArgObject("+data.ChildVar+")")
480+ continue
481+ }
482+ argExprs = append(argExprs, slot.expr)
483+ }
484+
485+ data.Params = strings.Join(params, ", ")
486+ args := ""
487+ if len(argExprs) > 0 {
488+ args = ", " + strings.Join(argExprs, ", ")
489+ }
490+ // Context.RequestArgs owns the lifecycle: destructors claim the proxy
491+ // atomically, children are registered before the send and descriptors are
492+ // closed only after a successful send.
493+ data.ArgList = args
494+ data.FDList = strings.Join(fdExprs, ", ")
495+ data.HasFDs = len(fdExprs) > 0
496+ if data.CreatesChild {
497+ data.Results = "(*" + data.ChildType + ", error)"
498+ data.ErrorReturn = "nil, err"
499+ } else {
500+ data.Results = "error"
501+ data.ErrorReturn = "err"
502+ }
503+
504+ return data, nil
505+}
506+
507+func (s *Scanner) processEvent(event spec.Message, opcode int) (eventData, error) {
508+ goName := s.toGoName(event.Name)
509+ data := eventData{
510+ ServerDestroy: event.Type == "destructor",
511+ Name: event.Name,
512+ GoName: goName,
513+ Opcode: opcode,
514+ HandlerField: "on" + goName,
515+ HandlerAccessor: "handlersFor" + goName,
516+ }
517+
518+ var params, args []string
519+
520+ for i, arg := range event.Args {
521+ name := s.paramName(arg.Name, i)
522+
523+ switch arg.Type {
524+ case "new_id":
525+ childType, err := s.goTypeForArg(arg)
526+ if err != nil {
527+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
528+ }
529+ child := name + "Object"
530+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
531+ if strings.Contains(childType, ".") {
532+ factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface)
533+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory))
534+ } else {
535+ data.DecodeLines = append(data.DecodeLines,
536+ fmt.Sprintf("%s := &%s{}", child, childType),
537+ fmt.Sprintf("%s.SetContext(o.Context())", child),
538+ )
539+ }
540+ data.DecodeLines = append(data.DecodeLines,
541+ fmt.Sprintf("%s.SetID(%sID)", child, name),
542+ fmt.Sprintf("%s.SetVersion(o.Version())", child),
543+ fmt.Sprintf("o.Context().Register(%s)", child),
544+ )
545+ params = append(params, child+" *"+childType)
546+ args = append(args, child)
547+
548+ case "object":
549+ // Object references are reported as raw object IDs: the generated
550+ // layer does not own a registry of foreign proxies.
551+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
552+ params = append(params, name+"ID uint32")
553+ args = append(args, name+"ID")
554+
555+ default:
556+ if arg.Type == "fd" {
557+ data.HasFD = true
558+ data.FDNames = append(data.FDNames, name)
559+ }
560+ goType, decoder, err := s.scalarDecoder(arg)
561+ if err != nil {
562+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
563+ }
564+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
565+ params = append(params, name+" "+goType)
566+ args = append(args, name)
567+ }
568+ }
569+
570+ data.HandlerParams = strings.Join(params, ", ")
571+ data.HandlerArgs = strings.Join(args, ", ")
572+ return data, nil
573+}
574+
575+// scalarDecoder maps a simple argument to its Go type and event decoder.
576+func (s *Scanner) scalarDecoder(arg spec.Arg) (goType, decoder string, err error) {
577+ switch arg.Type {
578+ case "int":
579+ return "int32", "Int32", nil
580+ case "uint":
581+ return "uint32", "Uint32", nil
582+ case "fixed":
583+ return "wl.Fixed", "Fixed", nil
584+ case "string":
585+ return "string", "String", nil
586+ case "array":
587+ return "[]byte", "Array", nil
588+ case "fd":
589+ return "*wl.OwnedFD", "FD", nil
590+ }
591+ return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
592+}
593+
594+// goTypeForArg returns the Go type of an object-like argument. Protocol-local
595+// interfaces map to generated types, explicit external references to their
596+// mapped package, and bootstrap display/registry to transport wrappers.
597+func (s *Scanner) goTypeForArg(arg spec.Arg) (string, error) {
598+ switch arg.Type {
599+ case "int":
600+ return "int32", nil
601+ case "uint":
602+ return "uint32", nil
603+ case "fixed":
604+ return "wl.Fixed", nil
605+ case "string":
606+ return "string", nil
607+ case "array":
608+ return "[]byte", nil
609+ case "fd":
610+ return "int", nil
611+ case "object", "new_id":
612+ return s.goTypeForInterface(arg.Interface)
613+ }
614+ return "", fmt.Errorf("unsupported argument type %q", arg.Type)
615+}
616+
617+func (s *Scanner) goTypeForInterface(iface string) (string, error) {
618+ if iface == "" {
619+ return "wl.BaseProxy", nil
620+ }
621+ // The XML package owns its own definitions even when the name starts wl_.
622+ for _, local := range s.protocol.Interfaces {
623+ if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
624+ return s.toGoName(iface), nil
625+ }
626+ }
627+ if bootstrapTypes[s.toGoName(iface)] {
628+ return "wl." + s.toGoName(iface), nil
629+ }
630+ if _, ok := s.CrossPackage[iface]; ok {
631+ return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil
632+ }
633+ return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface)
634+}
635+
636+// paramName turns a protocol argument name into a Go parameter name.
637+func (s *Scanner) paramName(name string, index int) string {
638+ if name == "" {
639+ return fmt.Sprintf("arg%d", index)
640+ }
641+
642+ parts := strings.Split(name, "_")
643+ for i, part := range parts {
644+ if part == "" {
645+ continue
646+ }
647+ if i == 0 {
648+ parts[i] = strings.ToLower(part[:1]) + part[1:]
649+ continue
650+ }
651+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
652+ }
653+ result := strings.Join(parts, "")
654+ if result == "" {
655+ return fmt.Sprintf("arg%d", index)
656+ }
657+ if goKeywords[result] {
658+ return result + "Arg"
659+ }
660+ return result
661+}
662+
663+func (s *Scanner) toGoName(name string) string {
664+ name = strings.TrimPrefix(name, "zwlr_")
665+ name = strings.TrimPrefix(name, "zwp_")
666+ name = strings.TrimPrefix(name, "wl_")
667+ name = strings.TrimSuffix(name, "_v1")
668+ name = strings.TrimSuffix(name, "_v2")
669+
670+ parts := strings.Split(name, "_")
671+ for i, part := range parts {
672+ if part != "" {
673+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
674+ }
675+ }
676+ return strings.Join(parts, "")
677+}
678+
679+// constantName renders an enum entry as the unprefixed constant name.
680+func constantName(enum, entry string) string {
681+ return strings.Join(append(joinName(enum), joinName(entry)...), "_")
682+}
683+
684+// qualifiedConstantName prefixes an enum entry constant with its interface
685+// name. It is used for entries that several interfaces declare, which would
686+// otherwise collide in the generated package.
687+func qualifiedConstantName(iface, enum, entry string) string {
688+ return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
689+}
690+
691+// joinName upper-cases the underscore-separated parts of a protocol name.
692+func joinName(value string) []string {
693+ parts := strings.Split(value, "_")
694+ out := make([]string, 0, len(parts))
695+ for _, part := range parts {
696+ if part != "" {
697+ out = append(out, strings.ToUpper(part))
698+ }
699+ }
700+ return out
701+}
702+
703+// formatDescription renders an argument or request description as a single
704+// documentation line.
705+func (s *Scanner) formatDescription(desc *spec.Description) string {
706+ if desc == nil {
707+ return ""
708+ }
709+
710+ text := strings.TrimSpace(desc.Text)
711+ if text == "" {
712+ return desc.Summary
713+ }
714+
715+ lines := strings.Split(text, "\n")
716+ formatted := make([]string, 0, len(lines))
717+ for _, line := range lines {
718+ if line = strings.TrimSpace(line); line != "" {
719+ formatted = append(formatted, line)
720+ }
721+ }
722+ return strings.Join(formatted, " ")
723+}
@@ -0,0 +1,723 @@
1+// Package client generates WLTurbo-backed Go bindings from Wayland protocol
2+// XML files.
3+//
4+// The generated code owns protocol semantics only: it embeds wl.BaseProxy,
5+// allocates and registers child objects through the wl.Context, sends requests
6+// through that context, and dispatches typed events to registered handlers.
7+// Message framing, descriptor passing and connection lifetime stay in the
8+// transport, so a generated binding never opens a socket or writes a header.
9+package client
10+
11+import (
12+ "bytes"
13+ "fmt"
14+ "go/format"
15+ "os"
16+ "path/filepath"
17+ "sort"
18+ "strings"
19+ "text/template"
20+
21+ "github.com/bnema/go-wayland-bindings/spec"
22+)
23+
24+// TransportImport is the import path generated bindings use for the Wayland
25+// transport.
26+const TransportImport = "github.com/bnema/wlturbo/wl"
27+
28+// DefaultTool is the generator name written to the "Code generated" header
29+// when Scanner.Tool is empty.
30+const DefaultTool = "wlbgen"
31+
32+// Scanner generates Go bindings from Wayland protocol XML
33+type Scanner struct {
34+ protocol *spec.Protocol
35+ source string
36+ // CrossPackage maps interface names to Go package import paths. Local wins.
37+ CrossPackage map[string]string
38+ // Tool is the generator name used in the header line
39+ // "// Code generated by <Tool>. DO NOT EDIT." Empty means DefaultTool.
40+ Tool string
41+ // SPDXLicense, when non-empty, makes the header carry a
42+ // "// SPDX-License-Identifier: <SPDXLicense>" line, unless the upstream
43+ // copyright text (which is always retained) already holds that exact line.
44+ // Empty emits no such line.
45+ SPDXLicense string
46+}
47+
48+// NewScanner creates a new protocol scanner
49+func NewScanner() *Scanner {
50+ return &Scanner{Tool: DefaultTool}
51+}
52+
53+// ParseXML parses a Wayland protocol XML file
54+func (s *Scanner) ParseXML(path string) error {
55+ data, err := os.ReadFile(path)
56+ if err != nil {
57+ return fmt.Errorf("failed to read XML file: %w", err)
58+ }
59+ // Only the base name is recorded: generated output must not depend on the
60+ // path the XML happened to be read from.
61+ return s.Load(data, filepath.Base(path))
62+}
63+
64+// Load parses Wayland protocol XML held in memory. sourceName is recorded in
65+// the generated header as the "Source:" line; use the XML file's base name.
66+func (s *Scanner) Load(data []byte, sourceName string) error {
67+ protocol, err := spec.Parse(data)
68+ if err != nil {
69+ return fmt.Errorf("failed to parse XML: %w", err)
70+ }
71+ return s.LoadProtocol(protocol, sourceName)
72+}
73+
74+// LoadProtocol uses an already parsed protocol.
75+func (s *Scanner) LoadProtocol(protocol *spec.Protocol, sourceName string) error {
76+ if protocol == nil {
77+ return fmt.Errorf("nil protocol")
78+ }
79+ s.protocol = protocol
80+ s.source = sourceName
81+ return nil
82+}
83+
84+// Generate creates Go source code for the parsed protocol
85+func (s *Scanner) Generate(packageName string) ([]byte, error) {
86+ if s.protocol == nil {
87+ return nil, fmt.Errorf("no protocol parsed")
88+ }
89+
90+ data, err := s.prepareTemplateData(packageName)
91+ if err != nil {
92+ return nil, err
93+ }
94+
95+ var buf bytes.Buffer
96+ tmpl := template.Must(template.New("protocol").Parse(protocolTemplate))
97+ if err := tmpl.Execute(&buf, data); err != nil {
98+ return nil, fmt.Errorf("failed to execute template: %w", err)
99+ }
100+
101+ formatted, err := format.Source(buf.Bytes())
102+ if err != nil {
103+ return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err)
104+ }
105+ return formatted, nil
106+}
107+
108+// templateData holds data for template generation
109+type templateData struct {
110+ Tool string
111+ SPDX string
112+ Package string
113+ Protocol string
114+ Source string
115+ Copyright string
116+ ImportBlock string
117+ Constants []constantData
118+ Interfaces []interfaceData
119+}
120+
121+type interfaceData struct {
122+ Name string
123+ GoName string
124+ Version int
125+ HasEvents bool
126+ SignatureCases []signatureCase
127+ Requests []requestData
128+ Events []eventData
129+}
130+
131+type signatureCase struct {
132+ Opcode int
133+ Signature string
134+}
135+type requestData struct {
136+ Name string
137+ GoName string
138+ Doc string
139+ Opcode int
140+ Destructor bool
141+ Since int
142+ Params string
143+ Results string
144+ ArgPreparations []string
145+ CreatesChild bool
146+ ChildVar string
147+ ChildType string
148+ ChildFactory string
149+ ErrorReturn string
150+ HasFDs bool
151+ FDList string
152+ ArgList string
153+}
154+
155+// argSlot is one positional argument of a request, in declaration order. A
156+// new_id slot is filled in once the child object has been created.
157+type argSlot struct {
158+ newID bool
159+ expr string
160+}
161+
162+type eventData struct {
163+ HasFD bool
164+ ServerDestroy bool
165+ FDNames []string
166+ Name string
167+ GoName string
168+ Opcode int
169+ HandlerField string
170+ HandlerAccessor string
171+ HandlerParams string
172+ HandlerArgs string
173+ DecodeLines []string
174+}
175+
176+type constantData struct {
177+ Name string
178+ Value string
179+ Type string
180+ Enum string
181+ Entry string
182+}
183+
184+// Only display and registry have handwritten bootstrap bindings.
185+var bootstrapTypes = map[string]bool{
186+ "Display": true,
187+ "Registry": true,
188+}
189+
190+// goKeywords are avoided as parameter names because generated identifiers
191+// must not collide with the language.
192+var goKeywords = map[string]bool{
193+ "break": true, "case": true, "chan": true, "const": true, "continue": true,
194+ "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
195+ "func": true, "go": true, "goto": true, "if": true, "import": true,
196+ "interface": true, "map": true, "package": true, "range": true, "return": true,
197+ "select": true, "struct": true, "switch": true, "type": true, "var": true,
198+}
199+
200+func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
201+ data := templateData{
202+ Tool: s.Tool,
203+ Package: packageName,
204+ Protocol: s.protocol.Name,
205+ Source: s.source,
206+ }
207+ if data.Tool == "" {
208+ data.Tool = DefaultTool
209+ }
210+ if notice := strings.TrimSpace(s.protocol.Copyright); notice != "" {
211+ var comment strings.Builder
212+ for _, line := range strings.Split(notice, "\n") {
213+ comment.WriteString("// ")
214+ comment.WriteString(strings.TrimSpace(line))
215+ comment.WriteByte('\n')
216+ }
217+ data.Copyright = comment.String()
218+ }
219+ if id := strings.TrimSpace(s.SPDXLicense); id != "" {
220+ line := "SPDX-License-Identifier: " + id
221+ retained := false
222+ for _, l := range strings.Split(s.protocol.Copyright, "\n") {
223+ if strings.TrimSpace(l) == line {
224+ retained = true
225+ }
226+ }
227+ if !retained {
228+ data.SPDX = id
229+ }
230+ }
231+
232+ imports := []string{TransportImport}
233+ usedCross := make(map[string]bool)
234+ for _, candidate := range s.protocol.Interfaces {
235+ for _, req := range candidate.Requests {
236+ for _, arg := range req.Args {
237+ if arg.Type == "object" || arg.Type == "new_id" {
238+ usedCross[arg.Interface] = true
239+ }
240+ }
241+ }
242+ for _, ev := range candidate.Events {
243+ for _, arg := range ev.Args {
244+ // Event object references are decoded as IDs, not imported proxies.
245+ if arg.Type == "new_id" {
246+ usedCross[arg.Interface] = true
247+ }
248+ }
249+ }
250+ }
251+ for iface, path := range s.CrossPackage {
252+ local := false
253+ for _, candidate := range s.protocol.Interfaces {
254+ if candidate.Name == iface {
255+ local = true
256+ break
257+ }
258+ }
259+ if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
260+ continue
261+ }
262+ alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
263+ imports = append(imports, alias+" "+path)
264+ }
265+ sort.Strings(imports)
266+ stdImports := []string{}
267+ needsSync := false
268+
269+ // Enum constants are package-level declarations, so two interfaces that
270+ // declare the same enum entry (for example xdg_surface.error.invalid_size
271+ // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
272+ // occurs in more than one interface are qualified with their interface
273+ // name below; unique names keep their historical, unprefixed form.
274+ enumEntryCounts := map[string]int{}
275+ for _, iface := range s.protocol.Interfaces {
276+ for _, enum := range iface.Enums {
277+ for _, entry := range enum.Entries {
278+ enumEntryCounts[constantName(enum.Name, entry.Name)]++
279+ }
280+ }
281+ }
282+
283+ // The protocol that defines wl_display is the core protocol: display and
284+ // registry have handwritten bootstrap bindings in the transport.
285+ definesCore := false
286+ for _, iface := range s.protocol.Interfaces {
287+ if iface.Name == "wl_display" {
288+ definesCore = true
289+ }
290+ }
291+
292+ for _, iface := range s.protocol.Interfaces {
293+ if definesCore && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
294+ continue
295+ }
296+ ifaceData, err := s.processInterface(iface)
297+ if err != nil {
298+ return data, err
299+ }
300+ data.Interfaces = append(data.Interfaces, ifaceData)
301+ if len(iface.Events) > 0 {
302+ needsSync = true
303+ }
304+
305+ for _, enum := range iface.Enums {
306+ for _, entry := range enum.Entries {
307+ name := constantName(enum.Name, entry.Name)
308+ if enumEntryCounts[name] > 1 {
309+ name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
310+ }
311+ // Enum values are emitted untyped: the same enum travels as a uint
312+ // in one protocol and as an int in another, and an untyped constant
313+ // assigns to either without casts at the call site.
314+ data.Constants = append(data.Constants, constantData{
315+ Name: name,
316+ Value: entry.Value,
317+ Enum: enum.Name,
318+ Entry: entry.Name,
319+ })
320+ }
321+ }
322+ }
323+
324+ // Handlers are registered and dispatched from different goroutines, so an
325+ // interface with events needs a mutex.
326+ if needsSync {
327+ stdImports = append(stdImports, "sync")
328+ }
329+
330+ var block strings.Builder
331+ block.WriteString("import (\n")
332+ for _, imp := range stdImports {
333+ fmt.Fprintf(&block, "\t%q\n", imp)
334+ }
335+ if len(stdImports) > 0 && len(imports) > 0 {
336+ block.WriteString("\n")
337+ }
338+ for _, imp := range imports {
339+ if strings.Contains(imp, " ") {
340+ parts := strings.SplitN(imp, " ", 2)
341+ fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
342+ } else {
343+ fmt.Fprintf(&block, "\t%q\n", imp)
344+ }
345+ }
346+ block.WriteString(")")
347+ data.ImportBlock = block.String()
348+
349+ return data, nil
350+}
351+
352+func (s *Scanner) processInterface(iface spec.Interface) (interfaceData, error) {
353+ data := interfaceData{
354+ Name: iface.Name,
355+ GoName: s.toGoName(iface.Name),
356+ Version: iface.Version,
357+ HasEvents: len(iface.Events) > 0,
358+ }
359+
360+ for i, req := range iface.Requests {
361+ request, err := s.processRequest(iface, req, i)
362+ if err != nil {
363+ return data, err
364+ }
365+ data.Requests = append(data.Requests, request)
366+ }
367+
368+ for i, event := range iface.Events {
369+ var sig strings.Builder
370+ for _, arg := range event.Args {
371+ sig.WriteString(arg.Type)
372+ sig.WriteByte(',')
373+ }
374+ data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
375+ eventData, err := s.processEvent(event, i)
376+ if iface.Name == "wl_callback" && event.Name == "done" {
377+ eventData.ServerDestroy = true
378+ }
379+ if err != nil {
380+ return data, err
381+ }
382+ data.Events = append(data.Events, eventData)
383+ }
384+
385+ return data, nil
386+}
387+
388+func (s *Scanner) processRequest(iface spec.Interface, req spec.Message, opcode int) (requestData, error) {
389+ data := requestData{
390+ Name: req.Name,
391+ GoName: s.toGoName(req.Name),
392+ Doc: s.formatDescription(req.Description),
393+ Opcode: opcode,
394+ Destructor: req.Type == "destructor",
395+ Since: req.Since,
396+ ChildVar: "child",
397+ }
398+
399+ var (
400+ params []string
401+ argSlots []argSlot
402+ argExprs []string
403+ fdExprs []string
404+ newIDArg *spec.Arg
405+ )
406+
407+ for i, arg := range req.Args {
408+ if arg.Type == "new_id" {
409+ if newIDArg != nil {
410+ return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
411+ }
412+ newIDArg = &arg
413+ // The child object is created later, but it keeps its declared
414+ // position: the wire order is the argument order.
415+ argSlots = append(argSlots, argSlot{newID: true})
416+ continue
417+ }
418+
419+ name := s.paramName(arg.Name, i)
420+ goType, err := s.goTypeForArg(arg)
421+ if err != nil {
422+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
423+ }
424+
425+ expr := name
426+ switch arg.Type {
427+ case "object":
428+ // Objects are pointer types, and travel as wl.Object so a nil
429+ // pointer is never sent as a non-nil interface holding a typed nil.
430+ params = append(params, name+" *"+goType)
431+ holder := fmt.Sprintf("arg%d", i)
432+ data.ArgPreparations = append(data.ArgPreparations,
433+ fmt.Sprintf("var %s wl.Object", holder),
434+ fmt.Sprintf("if %s != nil {", name),
435+ fmt.Sprintf("\t%s = %s", holder, name),
436+ "}")
437+ expr = "wl.ArgObject(" + holder + ")"
438+
439+ case "fd":
440+ // Descriptors travel out of band: the body carries no value for
441+ // them, and the descriptor list is attached to the request.
442+ params = append(params, name+" int")
443+ fdExprs = append(fdExprs, name)
444+ expr = "wl.ArgFD()"
445+
446+ default:
447+ params = append(params, name+" "+goType)
448+ switch arg.Type {
449+ case "int":
450+ expr = "wl.ArgInt(" + name + ")"
451+ case "uint":
452+ expr = "wl.ArgUint(" + name + ")"
453+ case "fixed":
454+ expr = "wl.ArgFixed(" + name + ")"
455+ case "string":
456+ expr = "wl.ArgString(" + name + ")"
457+ case "array":
458+ expr = "wl.ArgArray(" + name + ")"
459+ }
460+ }
461+
462+ argSlots = append(argSlots, argSlot{expr: expr})
463+ }
464+
465+ if newIDArg != nil {
466+ childType, err := s.goTypeForArg(*newIDArg)
467+ if err != nil {
468+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
469+ }
470+ data.CreatesChild = true
471+ data.ChildType = childType
472+ if strings.Contains(childType, ".") {
473+ data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface)
474+ }
475+ }
476+
477+ for _, slot := range argSlots {
478+ if slot.newID {
479+ argExprs = append(argExprs, "wl.ArgObject("+data.ChildVar+")")
480+ continue
481+ }
482+ argExprs = append(argExprs, slot.expr)
483+ }
484+
485+ data.Params = strings.Join(params, ", ")
486+ args := ""
487+ if len(argExprs) > 0 {
488+ args = ", " + strings.Join(argExprs, ", ")
489+ }
490+ // Context.RequestArgs owns the lifecycle: destructors claim the proxy
491+ // atomically, children are registered before the send and descriptors are
492+ // closed only after a successful send.
493+ data.ArgList = args
494+ data.FDList = strings.Join(fdExprs, ", ")
495+ data.HasFDs = len(fdExprs) > 0
496+ if data.CreatesChild {
497+ data.Results = "(*" + data.ChildType + ", error)"
498+ data.ErrorReturn = "nil, err"
499+ } else {
500+ data.Results = "error"
501+ data.ErrorReturn = "err"
502+ }
503+
504+ return data, nil
505+}
506+
507+func (s *Scanner) processEvent(event spec.Message, opcode int) (eventData, error) {
508+ goName := s.toGoName(event.Name)
509+ data := eventData{
510+ ServerDestroy: event.Type == "destructor",
511+ Name: event.Name,
512+ GoName: goName,
513+ Opcode: opcode,
514+ HandlerField: "on" + goName,
515+ HandlerAccessor: "handlersFor" + goName,
516+ }
517+
518+ var params, args []string
519+
520+ for i, arg := range event.Args {
521+ name := s.paramName(arg.Name, i)
522+
523+ switch arg.Type {
524+ case "new_id":
525+ childType, err := s.goTypeForArg(arg)
526+ if err != nil {
527+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
528+ }
529+ child := name + "Object"
530+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
531+ if strings.Contains(childType, ".") {
532+ factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface)
533+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory))
534+ } else {
535+ data.DecodeLines = append(data.DecodeLines,
536+ fmt.Sprintf("%s := &%s{}", child, childType),
537+ fmt.Sprintf("%s.SetContext(o.Context())", child),
538+ )
539+ }
540+ data.DecodeLines = append(data.DecodeLines,
541+ fmt.Sprintf("%s.SetID(%sID)", child, name),
542+ fmt.Sprintf("%s.SetVersion(o.Version())", child),
543+ fmt.Sprintf("o.Context().Register(%s)", child),
544+ )
545+ params = append(params, child+" *"+childType)
546+ args = append(args, child)
547+
548+ case "object":
549+ // Object references are reported as raw object IDs: the generated
550+ // layer does not own a registry of foreign proxies.
551+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
552+ params = append(params, name+"ID uint32")
553+ args = append(args, name+"ID")
554+
555+ default:
556+ if arg.Type == "fd" {
557+ data.HasFD = true
558+ data.FDNames = append(data.FDNames, name)
559+ }
560+ goType, decoder, err := s.scalarDecoder(arg)
561+ if err != nil {
562+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
563+ }
564+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
565+ params = append(params, name+" "+goType)
566+ args = append(args, name)
567+ }
568+ }
569+
570+ data.HandlerParams = strings.Join(params, ", ")
571+ data.HandlerArgs = strings.Join(args, ", ")
572+ return data, nil
573+}
574+
575+// scalarDecoder maps a simple argument to its Go type and event decoder.
576+func (s *Scanner) scalarDecoder(arg spec.Arg) (goType, decoder string, err error) {
577+ switch arg.Type {
578+ case "int":
579+ return "int32", "Int32", nil
580+ case "uint":
581+ return "uint32", "Uint32", nil
582+ case "fixed":
583+ return "wl.Fixed", "Fixed", nil
584+ case "string":
585+ return "string", "String", nil
586+ case "array":
587+ return "[]byte", "Array", nil
588+ case "fd":
589+ return "*wl.OwnedFD", "FD", nil
590+ }
591+ return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
592+}
593+
594+// goTypeForArg returns the Go type of an object-like argument. Protocol-local
595+// interfaces map to generated types, explicit external references to their
596+// mapped package, and bootstrap display/registry to transport wrappers.
597+func (s *Scanner) goTypeForArg(arg spec.Arg) (string, error) {
598+ switch arg.Type {
599+ case "int":
600+ return "int32", nil
601+ case "uint":
602+ return "uint32", nil
603+ case "fixed":
604+ return "wl.Fixed", nil
605+ case "string":
606+ return "string", nil
607+ case "array":
608+ return "[]byte", nil
609+ case "fd":
610+ return "int", nil
611+ case "object", "new_id":
612+ return s.goTypeForInterface(arg.Interface)
613+ }
614+ return "", fmt.Errorf("unsupported argument type %q", arg.Type)
615+}
616+
617+func (s *Scanner) goTypeForInterface(iface string) (string, error) {
618+ if iface == "" {
619+ return "wl.BaseProxy", nil
620+ }
621+ // The XML package owns its own definitions even when the name starts wl_.
622+ for _, local := range s.protocol.Interfaces {
623+ if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
624+ return s.toGoName(iface), nil
625+ }
626+ }
627+ if bootstrapTypes[s.toGoName(iface)] {
628+ return "wl." + s.toGoName(iface), nil
629+ }
630+ if _, ok := s.CrossPackage[iface]; ok {
631+ return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil
632+ }
633+ return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface)
634+}
635+
636+// paramName turns a protocol argument name into a Go parameter name.
637+func (s *Scanner) paramName(name string, index int) string {
638+ if name == "" {
639+ return fmt.Sprintf("arg%d", index)
640+ }
641+
642+ parts := strings.Split(name, "_")
643+ for i, part := range parts {
644+ if part == "" {
645+ continue
646+ }
647+ if i == 0 {
648+ parts[i] = strings.ToLower(part[:1]) + part[1:]
649+ continue
650+ }
651+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
652+ }
653+ result := strings.Join(parts, "")
654+ if result == "" {
655+ return fmt.Sprintf("arg%d", index)
656+ }
657+ if goKeywords[result] {
658+ return result + "Arg"
659+ }
660+ return result
661+}
662+
663+func (s *Scanner) toGoName(name string) string {
664+ name = strings.TrimPrefix(name, "zwlr_")
665+ name = strings.TrimPrefix(name, "zwp_")
666+ name = strings.TrimPrefix(name, "wl_")
667+ name = strings.TrimSuffix(name, "_v1")
668+ name = strings.TrimSuffix(name, "_v2")
669+
670+ parts := strings.Split(name, "_")
671+ for i, part := range parts {
672+ if part != "" {
673+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
674+ }
675+ }
676+ return strings.Join(parts, "")
677+}
678+
679+// constantName renders an enum entry as the unprefixed constant name.
680+func constantName(enum, entry string) string {
681+ return strings.Join(append(joinName(enum), joinName(entry)...), "_")
682+}
683+
684+// qualifiedConstantName prefixes an enum entry constant with its interface
685+// name. It is used for entries that several interfaces declare, which would
686+// otherwise collide in the generated package.
687+func qualifiedConstantName(iface, enum, entry string) string {
688+ return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
689+}
690+
691+// joinName upper-cases the underscore-separated parts of a protocol name.
692+func joinName(value string) []string {
693+ parts := strings.Split(value, "_")
694+ out := make([]string, 0, len(parts))
695+ for _, part := range parts {
696+ if part != "" {
697+ out = append(out, strings.ToUpper(part))
698+ }
699+ }
700+ return out
701+}
702+
703+// formatDescription renders an argument or request description as a single
704+// documentation line.
705+func (s *Scanner) formatDescription(desc *spec.Description) string {
706+ if desc == nil {
707+ return ""
708+ }
709+
710+ text := strings.TrimSpace(desc.Text)
711+ if text == "" {
712+ return desc.Summary
713+ }
714+
715+ lines := strings.Split(text, "\n")
716+ formatted := make([]string, 0, len(lines))
717+ for _, line := range lines {
718+ if line = strings.TrimSpace(line); line != "" {
719+ formatted = append(formatted, line)
720+ }
721+ }
722+ return strings.Join(formatted, " ")
723+}
A internal/gen/client/scanner_test.go +156 -0
@@ -0,0 +1,156 @@
1+package client
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestResolution(t *testing.T) {
12+ p := filepath.Join(t.TempDir(), "fixture.xml")
13+ xml := `<protocol name="sample"><interface name="wl_surface" version="1"><event name="done"><arg name="value" type="uint"/></event></interface><interface name="custom_thing" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/><arg name="output" type="object" interface="wl_output"/></request></interface></protocol>`
14+ if err := os.WriteFile(p, []byte(xml), 0600); err != nil {
15+ t.Fatal(err)
16+ }
17+ s := NewScanner()
18+ if err := s.ParseXML(p); err != nil {
19+ t.Fatal(err)
20+ }
21+ s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/go-wayland-bindings/client/wayland", "wl_output": "github.com/bnema/go-wayland-bindings/client/wayland"}
22+ b, err := s.Generate("fixture")
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ text := string(b)
27+ if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") {
28+ t.Fatalf("wrong resolution: %s", text)
29+ }
30+}
31+func TestCoreGeneration(t *testing.T) {
32+ s := NewScanner()
33+ if err := s.ParseXML("../../../spec/testdata/wayland.xml"); err != nil {
34+ t.Fatal(err)
35+ }
36+ b, err := s.Generate("wayland")
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} {
41+ if !strings.Contains(string(b), want) {
42+ t.Errorf("missing %s", want)
43+ }
44+ }
45+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") {
46+ t.Fatal("bootstrap generated twice")
47+ }
48+}
49+
50+// Extension protocols use wl_buffer as both an event-created child and a request-created child.
51+// Verify the generated code uses the public constructor instead of embedding
52+// an inaccessible BaseProxy context field across package boundaries.
53+func TestExternalChildFactory(t *testing.T) {
54+ path := filepath.Join(t.TempDir(), "external.xml")
55+ xml := `<protocol name="fixture"><interface name="ext_params" version="1"><request name="create"><arg name="id" type="new_id" interface="wl_buffer"/></request><event name="created"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
56+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
57+ t.Fatal(err)
58+ }
59+ s := NewScanner()
60+ if err := s.ParseXML(path); err != nil {
61+ t.Fatal(err)
62+ }
63+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/go-wayland-bindings/client/wayland"}
64+ generated, err := s.Generate("ext")
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ for _, part := range []string{"cross_wl_buffer.NewBuffer(o.Context())", "(*cross_wl_buffer.Buffer, error)", "idObject.SetID(idID)"} {
69+ if !strings.Contains(string(generated), part) {
70+ t.Errorf("generated code missing %q", part)
71+ }
72+ }
73+ if strings.Contains(string(generated), "&cross_wl_buffer.Buffer{}") {
74+ t.Fatal("foreign child was constructed without its package factory")
75+ }
76+}
77+
78+func TestUnresolvedExternalInterface(t *testing.T) {
79+ path := filepath.Join(t.TempDir(), "extension.xml")
80+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/></request></interface></protocol>`
81+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
82+ t.Fatal(err)
83+ }
84+ s := NewScanner()
85+ if err := s.ParseXML(path); err != nil {
86+ t.Fatal(err)
87+ }
88+ if _, err := s.Generate("ext"); err == nil || !strings.Contains(err.Error(), "wl_surface") {
89+ t.Fatalf("unmapped external reference: %v", err)
90+ }
91+}
92+
93+func TestMappedExtensionCompilesAndDispatchesChild(t *testing.T) {
94+ goBin, err := exec.LookPath("go")
95+ if err != nil {
96+ t.Skip("go unavailable: generated extension compilation requires go")
97+ }
98+ dir := t.TempDir()
99+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><event name="child"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
100+ path := filepath.Join(dir, "ext.xml")
101+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
102+ t.Fatal(err)
103+ }
104+ s := NewScanner()
105+ if err := s.ParseXML(path); err != nil {
106+ t.Fatal(err)
107+ }
108+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"}
109+ generated, err := s.Generate("ext")
110+ if err != nil {
111+ t.Fatal(err)
112+ }
113+ if err := os.WriteFile(filepath.Join(dir, "ext.go"), generated, 0600); err != nil {
114+ t.Fatal(err)
115+ }
116+ writeTempModule(t, dir, "example.com/ext")
117+ test := `package ext
118+import (
119+ "encoding/binary"
120+ "net"
121+ "os"
122+ "testing"
123+ "github.com/bnema/wlturbo"
124+ "github.com/bnema/wlturbo/protocol/core"
125+ "golang.org/x/sys/unix"
126+)
127+func TestTypedChild(t *testing.T) {
128+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0); if err != nil { t.Fatal(err) }
129+ a, b := os.NewFile(uintptr(f[0]), "a"), os.NewFile(uintptr(f[1]), "b")
130+ c, err := net.FileConn(a); a.Close(); if err != nil { t.Fatal(err) }; defer c.Close()
131+ p, err := net.FileConn(b); b.Close(); if err != nil { t.Fatal(err) }; defer p.Close()
132+ d, err := wlturbo.ConnectFromConn(c); if err != nil { t.Fatal(err) }; defer d.Close()
133+ parent := NewExtParent(d.Context()); parent.SetID(d.AllocateID()); d.Context().Register(parent)
134+ var child *core.Buffer
135+ parent.OnChild(func(b *core.Buffer) { child = b })
136+ frame := make([]byte, 12); binary.LittleEndian.PutUint32(frame, parent.ID()); binary.LittleEndian.PutUint32(frame[4:], 12<<16); binary.LittleEndian.PutUint32(frame[8:], 0xff000001)
137+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
138+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
139+ if child == nil || child.ID() != 0xff000001 { t.Fatalf("child: %v", child) }
140+ called := false; child.OnRelease(func(){ called = true })
141+ frame = frame[:8]; binary.LittleEndian.PutUint32(frame, child.ID()); binary.LittleEndian.PutUint32(frame[4:], 8<<16)
142+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
143+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
144+ if !called { t.Fatal("typed child did not dispatch release") }
145+}
146+`
147+ if err := os.WriteFile(filepath.Join(dir, "ext_test.go"), []byte(test), 0600); err != nil {
148+ t.Fatal(err)
149+ }
150+ cmd := exec.Command(goBin, "test", "./...")
151+ cmd.Dir = dir
152+ cmd.Env = tempModuleEnv()
153+ if out, err := cmd.CombinedOutput(); err != nil {
154+ t.Fatalf("generated extension test: %v\n%s", err, out)
155+ }
156+}
@@ -0,0 +1,156 @@
1+package client
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestResolution(t *testing.T) {
12+ p := filepath.Join(t.TempDir(), "fixture.xml")
13+ xml := `<protocol name="sample"><interface name="wl_surface" version="1"><event name="done"><arg name="value" type="uint"/></event></interface><interface name="custom_thing" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/><arg name="output" type="object" interface="wl_output"/></request></interface></protocol>`
14+ if err := os.WriteFile(p, []byte(xml), 0600); err != nil {
15+ t.Fatal(err)
16+ }
17+ s := NewScanner()
18+ if err := s.ParseXML(p); err != nil {
19+ t.Fatal(err)
20+ }
21+ s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/go-wayland-bindings/client/wayland", "wl_output": "github.com/bnema/go-wayland-bindings/client/wayland"}
22+ b, err := s.Generate("fixture")
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ text := string(b)
27+ if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") {
28+ t.Fatalf("wrong resolution: %s", text)
29+ }
30+}
31+func TestCoreGeneration(t *testing.T) {
32+ s := NewScanner()
33+ if err := s.ParseXML("../../../spec/testdata/wayland.xml"); err != nil {
34+ t.Fatal(err)
35+ }
36+ b, err := s.Generate("wayland")
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} {
41+ if !strings.Contains(string(b), want) {
42+ t.Errorf("missing %s", want)
43+ }
44+ }
45+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") {
46+ t.Fatal("bootstrap generated twice")
47+ }
48+}
49+
50+// Extension protocols use wl_buffer as both an event-created child and a request-created child.
51+// Verify the generated code uses the public constructor instead of embedding
52+// an inaccessible BaseProxy context field across package boundaries.
53+func TestExternalChildFactory(t *testing.T) {
54+ path := filepath.Join(t.TempDir(), "external.xml")
55+ xml := `<protocol name="fixture"><interface name="ext_params" version="1"><request name="create"><arg name="id" type="new_id" interface="wl_buffer"/></request><event name="created"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
56+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
57+ t.Fatal(err)
58+ }
59+ s := NewScanner()
60+ if err := s.ParseXML(path); err != nil {
61+ t.Fatal(err)
62+ }
63+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/go-wayland-bindings/client/wayland"}
64+ generated, err := s.Generate("ext")
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ for _, part := range []string{"cross_wl_buffer.NewBuffer(o.Context())", "(*cross_wl_buffer.Buffer, error)", "idObject.SetID(idID)"} {
69+ if !strings.Contains(string(generated), part) {
70+ t.Errorf("generated code missing %q", part)
71+ }
72+ }
73+ if strings.Contains(string(generated), "&cross_wl_buffer.Buffer{}") {
74+ t.Fatal("foreign child was constructed without its package factory")
75+ }
76+}
77+
78+func TestUnresolvedExternalInterface(t *testing.T) {
79+ path := filepath.Join(t.TempDir(), "extension.xml")
80+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/></request></interface></protocol>`
81+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
82+ t.Fatal(err)
83+ }
84+ s := NewScanner()
85+ if err := s.ParseXML(path); err != nil {
86+ t.Fatal(err)
87+ }
88+ if _, err := s.Generate("ext"); err == nil || !strings.Contains(err.Error(), "wl_surface") {
89+ t.Fatalf("unmapped external reference: %v", err)
90+ }
91+}
92+
93+func TestMappedExtensionCompilesAndDispatchesChild(t *testing.T) {
94+ goBin, err := exec.LookPath("go")
95+ if err != nil {
96+ t.Skip("go unavailable: generated extension compilation requires go")
97+ }
98+ dir := t.TempDir()
99+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><event name="child"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
100+ path := filepath.Join(dir, "ext.xml")
101+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
102+ t.Fatal(err)
103+ }
104+ s := NewScanner()
105+ if err := s.ParseXML(path); err != nil {
106+ t.Fatal(err)
107+ }
108+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"}
109+ generated, err := s.Generate("ext")
110+ if err != nil {
111+ t.Fatal(err)
112+ }
113+ if err := os.WriteFile(filepath.Join(dir, "ext.go"), generated, 0600); err != nil {
114+ t.Fatal(err)
115+ }
116+ writeTempModule(t, dir, "example.com/ext")
117+ test := `package ext
118+import (
119+ "encoding/binary"
120+ "net"
121+ "os"
122+ "testing"
123+ "github.com/bnema/wlturbo"
124+ "github.com/bnema/wlturbo/protocol/core"
125+ "golang.org/x/sys/unix"
126+)
127+func TestTypedChild(t *testing.T) {
128+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0); if err != nil { t.Fatal(err) }
129+ a, b := os.NewFile(uintptr(f[0]), "a"), os.NewFile(uintptr(f[1]), "b")
130+ c, err := net.FileConn(a); a.Close(); if err != nil { t.Fatal(err) }; defer c.Close()
131+ p, err := net.FileConn(b); b.Close(); if err != nil { t.Fatal(err) }; defer p.Close()
132+ d, err := wlturbo.ConnectFromConn(c); if err != nil { t.Fatal(err) }; defer d.Close()
133+ parent := NewExtParent(d.Context()); parent.SetID(d.AllocateID()); d.Context().Register(parent)
134+ var child *core.Buffer
135+ parent.OnChild(func(b *core.Buffer) { child = b })
136+ frame := make([]byte, 12); binary.LittleEndian.PutUint32(frame, parent.ID()); binary.LittleEndian.PutUint32(frame[4:], 12<<16); binary.LittleEndian.PutUint32(frame[8:], 0xff000001)
137+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
138+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
139+ if child == nil || child.ID() != 0xff000001 { t.Fatalf("child: %v", child) }
140+ called := false; child.OnRelease(func(){ called = true })
141+ frame = frame[:8]; binary.LittleEndian.PutUint32(frame, child.ID()); binary.LittleEndian.PutUint32(frame[4:], 8<<16)
142+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
143+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
144+ if !called { t.Fatal("typed child did not dispatch release") }
145+}
146+`
147+ if err := os.WriteFile(filepath.Join(dir, "ext_test.go"), []byte(test), 0600); err != nil {
148+ t.Fatal(err)
149+ }
150+ cmd := exec.Command(goBin, "test", "./...")
151+ cmd.Dir = dir
152+ cmd.Env = tempModuleEnv()
153+ if out, err := cmd.CombinedOutput(); err != nil {
154+ t.Fatalf("generated extension test: %v\n%s", err, out)
155+ }
156+}
A internal/gen/client/template.go +141 -0
@@ -0,0 +1,141 @@
1+package client
2+
3+// protocolTemplate is the template for generating WLTurbo-backed Go bindings.
4+//
5+// Generated objects embed wl.BaseProxy, send every request through
6+// wl.Context.RequestArgs (typed arguments, no interface boxing; it owns
7+// version checks, child registration and FD cleanup), and dispatch typed
8+// events to registered handlers. Nothing in the generated code opens a
9+// socket or frames messages itself: that is the transport's job.
10+const protocolTemplate = `// Code generated by {{.Tool}}. DO NOT EDIT.
11+// Protocol: {{.Protocol}}
12+// Source: {{.Source}}
13+{{if .SPDX}}// SPDX-License-Identifier: {{.SPDX}}
14+{{end}}{{if .Copyright}}
15+// Upstream protocol copyright and license:
16+{{.Copyright}}{{end}}
17+package {{.Package}}
18+
19+{{.ImportBlock}}
20+
21+{{range .Constants}}
22+// {{.Name}} is the {{.Enum}} {{.Entry}} value.
23+const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}}
24+{{- end}}
25+{{range .Interfaces}}
26+{{- $iface := .}}
27+
28+// {{$iface.GoName}}Interface is the protocol interface name.
29+const {{$iface.GoName}}Interface = "{{$iface.Name}}"
30+
31+// {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}.
32+type {{$iface.GoName}} struct {
33+ wl.BaseProxy
34+{{- if $iface.HasEvents}}
35+ // handlersMu guards the handler slices: a client may register or replace
36+ // handlers while another goroutine dispatches events.
37+ handlersMu sync.Mutex
38+{{- end}}
39+{{- range $iface.Events}}
40+ {{.HandlerField}} []func({{.HandlerParams}})
41+{{- end}}
42+}
43+
44+// New{{$iface.GoName}} returns an unbound {{$iface.GoName}}.
45+func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} {
46+ obj := &{{$iface.GoName}}{}
47+ obj.SetContext(ctx)
48+ ctx.Register(obj)
49+ return obj
50+}
51+{{- range $iface.Requests}}
52+
53+{{- if .Doc}}
54+// {{.Doc}}
55+{{- end}}
56+func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} {
57+{{- range .ArgPreparations}}
58+ {{.}}
59+{{- end}}
60+{{- if .CreatesChild}}
61+{{- if .ChildFactory}}
62+ {{.ChildVar}} := {{.ChildFactory}}(o.Context())
63+{{- else}}
64+ {{.ChildVar}} := &{{.ChildType}}{}
65+ {{.ChildVar}}.SetContext(o.Context())
66+{{- end}}
67+{{- end}}
68+ if err := o.Context().RequestArgs(wl.Request{Proxy: o, Opcode: {{.Opcode}}, Name: "{{$iface.Name}}.{{.Name}}"
69+ {{- if gt .Since 1}}, Since: {{.Since}}{{end}}
70+ {{- if .Destructor}}, Destructor: true{{end}}
71+ {{- if .CreatesChild}}, Child: {{.ChildVar}}{{end}}
72+ {{- if .HasFDs}}, FDs: []int{ {{- .FDList -}} }{{end}}}{{.ArgList}}); err != nil {
73+ return {{.ErrorReturn}}
74+ }
75+{{- if .CreatesChild}}
76+ return {{.ChildVar}}, nil
77+{{- else}}
78+ return nil
79+{{- end}}
80+}
81+{{- end}}
82+{{- range $iface.Events}}
83+
84+// On{{.GoName}} registers a handler for the {{.Name}} event.
85+//
86+// Handlers are appended, so a second call adds another handler rather than
87+// replacing the first. Registration is safe while another goroutine dispatches.
88+func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) {
89+ if handler == nil { return }
90+ o.handlersMu.Lock()
91+ defer o.handlersMu.Unlock()
92+ o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler)
93+}
94+
95+// {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken
96+// under the lock so a handler may register another handler while events are
97+// being dispatched.
98+func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) {
99+ o.handlersMu.Lock()
100+ defer o.handlersMu.Unlock()
101+ return o.{{.HandlerField}}
102+}
103+{{- end}}
104+
105+// EventSignature returns the declared event argument types in wire order.
106+func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) {
107+ switch opcode {
108+ {{- range $iface.SignatureCases}}
109+ case {{.Opcode}}: return "{{.Signature}}", true
110+ {{- end}}
111+ default: return "", false
112+ }
113+}
114+
115+// Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers.
116+func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
117+ switch event.Opcode {
118+{{- range $iface.Events}}
119+ case {{.Opcode}}: // {{.Name}}
120+{{- range .DecodeLines}}
121+ {{.}}
122+{{- end}}
123+ {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close()
124+ {{end}}{{end}}
125+ for i, handler := range o.{{.HandlerAccessor}}() {
126+ {{- if .HasFD}}
127+ if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
128+ {{- else}}
129+ _ = i
130+ {{- end}}
131+ handler({{.HandlerArgs}})
132+ }
133+{{- if .ServerDestroy}}
134+ o.Context().Unregister(o)
135+{{- end}}
136+{{- end}}
137+ default:
138+ }
139+}
140+{{end}}
141+`
@@ -0,0 +1,141 @@
1+package client
2+
3+// protocolTemplate is the template for generating WLTurbo-backed Go bindings.
4+//
5+// Generated objects embed wl.BaseProxy, send every request through
6+// wl.Context.RequestArgs (typed arguments, no interface boxing; it owns
7+// version checks, child registration and FD cleanup), and dispatch typed
8+// events to registered handlers. Nothing in the generated code opens a
9+// socket or frames messages itself: that is the transport's job.
10+const protocolTemplate = `// Code generated by {{.Tool}}. DO NOT EDIT.
11+// Protocol: {{.Protocol}}
12+// Source: {{.Source}}
13+{{if .SPDX}}// SPDX-License-Identifier: {{.SPDX}}
14+{{end}}{{if .Copyright}}
15+// Upstream protocol copyright and license:
16+{{.Copyright}}{{end}}
17+package {{.Package}}
18+
19+{{.ImportBlock}}
20+
21+{{range .Constants}}
22+// {{.Name}} is the {{.Enum}} {{.Entry}} value.
23+const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}}
24+{{- end}}
25+{{range .Interfaces}}
26+{{- $iface := .}}
27+
28+// {{$iface.GoName}}Interface is the protocol interface name.
29+const {{$iface.GoName}}Interface = "{{$iface.Name}}"
30+
31+// {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}.
32+type {{$iface.GoName}} struct {
33+ wl.BaseProxy
34+{{- if $iface.HasEvents}}
35+ // handlersMu guards the handler slices: a client may register or replace
36+ // handlers while another goroutine dispatches events.
37+ handlersMu sync.Mutex
38+{{- end}}
39+{{- range $iface.Events}}
40+ {{.HandlerField}} []func({{.HandlerParams}})
41+{{- end}}
42+}
43+
44+// New{{$iface.GoName}} returns an unbound {{$iface.GoName}}.
45+func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} {
46+ obj := &{{$iface.GoName}}{}
47+ obj.SetContext(ctx)
48+ ctx.Register(obj)
49+ return obj
50+}
51+{{- range $iface.Requests}}
52+
53+{{- if .Doc}}
54+// {{.Doc}}
55+{{- end}}
56+func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} {
57+{{- range .ArgPreparations}}
58+ {{.}}
59+{{- end}}
60+{{- if .CreatesChild}}
61+{{- if .ChildFactory}}
62+ {{.ChildVar}} := {{.ChildFactory}}(o.Context())
63+{{- else}}
64+ {{.ChildVar}} := &{{.ChildType}}{}
65+ {{.ChildVar}}.SetContext(o.Context())
66+{{- end}}
67+{{- end}}
68+ if err := o.Context().RequestArgs(wl.Request{Proxy: o, Opcode: {{.Opcode}}, Name: "{{$iface.Name}}.{{.Name}}"
69+ {{- if gt .Since 1}}, Since: {{.Since}}{{end}}
70+ {{- if .Destructor}}, Destructor: true{{end}}
71+ {{- if .CreatesChild}}, Child: {{.ChildVar}}{{end}}
72+ {{- if .HasFDs}}, FDs: []int{ {{- .FDList -}} }{{end}}}{{.ArgList}}); err != nil {
73+ return {{.ErrorReturn}}
74+ }
75+{{- if .CreatesChild}}
76+ return {{.ChildVar}}, nil
77+{{- else}}
78+ return nil
79+{{- end}}
80+}
81+{{- end}}
82+{{- range $iface.Events}}
83+
84+// On{{.GoName}} registers a handler for the {{.Name}} event.
85+//
86+// Handlers are appended, so a second call adds another handler rather than
87+// replacing the first. Registration is safe while another goroutine dispatches.
88+func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) {
89+ if handler == nil { return }
90+ o.handlersMu.Lock()
91+ defer o.handlersMu.Unlock()
92+ o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler)
93+}
94+
95+// {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken
96+// under the lock so a handler may register another handler while events are
97+// being dispatched.
98+func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) {
99+ o.handlersMu.Lock()
100+ defer o.handlersMu.Unlock()
101+ return o.{{.HandlerField}}
102+}
103+{{- end}}
104+
105+// EventSignature returns the declared event argument types in wire order.
106+func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) {
107+ switch opcode {
108+ {{- range $iface.SignatureCases}}
109+ case {{.Opcode}}: return "{{.Signature}}", true
110+ {{- end}}
111+ default: return "", false
112+ }
113+}
114+
115+// Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers.
116+func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
117+ switch event.Opcode {
118+{{- range $iface.Events}}
119+ case {{.Opcode}}: // {{.Name}}
120+{{- range .DecodeLines}}
121+ {{.}}
122+{{- end}}
123+ {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close()
124+ {{end}}{{end}}
125+ for i, handler := range o.{{.HandlerAccessor}}() {
126+ {{- if .HasFD}}
127+ if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
128+ {{- else}}
129+ _ = i
130+ {{- end}}
131+ handler({{.HandlerArgs}})
132+ }
133+{{- if .ServerDestroy}}
134+ o.Context().Unregister(o)
135+{{- end}}
136+{{- end}}
137+ default:
138+ }
139+}
140+{{end}}
141+`
A internal/gen/client/tempmodule_test.go +37 -0
@@ -0,0 +1,37 @@
1+package client
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+)
8+
9+// runtimeVersion is the wlturbo release that temp-module tests compile the
10+// generated bindings against. It is resolved from the module cache.
11+const runtimeVersion = "v0.5.0"
12+
13+// writeTempModule writes go.mod and go.sum for a throwaway module that
14+// requires the published wlturbo runtime. go.sum starts from the repository's
15+// own go.sum; tempModuleEnv lets the go command complete it offline from the
16+// module cache.
17+func writeTempModule(t *testing.T, dir, module string) {
18+ t.Helper()
19+ mod := "module " + module + "\n\ngo 1.27\n\n" +
20+ "require (\n\tgithub.com/bnema/wlturbo " + runtimeVersion + "\n\tgolang.org/x/sys v0.48.0\n)\n"
21+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0o600); err != nil {
22+ t.Fatal(err)
23+ }
24+ sum, err := os.ReadFile(filepath.Join("..", "..", "..", "go.sum"))
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ if err := os.WriteFile(filepath.Join(dir, "go.sum"), sum, 0o600); err != nil {
29+ t.Fatal(err)
30+ }
31+}
32+
33+// tempModuleEnv resolves modules from the local module cache only: no network
34+// access and no checksum database lookups.
35+func tempModuleEnv() []string {
36+ return append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod", "GOPROXY=off", "GONOSUMDB=*", "GOSUMDB=off")
37+}
@@ -0,0 +1,37 @@
1+package client
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+)
8+
9+// runtimeVersion is the wlturbo release that temp-module tests compile the
10+// generated bindings against. It is resolved from the module cache.
11+const runtimeVersion = "v0.5.0"
12+
13+// writeTempModule writes go.mod and go.sum for a throwaway module that
14+// requires the published wlturbo runtime. go.sum starts from the repository's
15+// own go.sum; tempModuleEnv lets the go command complete it offline from the
16+// module cache.
17+func writeTempModule(t *testing.T, dir, module string) {
18+ t.Helper()
19+ mod := "module " + module + "\n\ngo 1.27\n\n" +
20+ "require (\n\tgithub.com/bnema/wlturbo " + runtimeVersion + "\n\tgolang.org/x/sys v0.48.0\n)\n"
21+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0o600); err != nil {
22+ t.Fatal(err)
23+ }
24+ sum, err := os.ReadFile(filepath.Join("..", "..", "..", "go.sum"))
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ if err := os.WriteFile(filepath.Join(dir, "go.sum"), sum, 0o600); err != nil {
29+ t.Fatal(err)
30+ }
31+}
32+
33+// tempModuleEnv resolves modules from the local module cache only: no network
34+// access and no checksum database lookups.
35+func tempModuleEnv() []string {
36+ return append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod", "GOPROXY=off", "GONOSUMDB=*", "GOSUMDB=off")
37+}
A internal/gen/client/testdata/server-decoration.xml +85 -0
@@ -0,0 +1,85 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="server_decoration">
3+ <copyright><![CDATA[
4+ SPDX-FileCopyrightText: 2015 Martin Gräßlin
5+
6+ SPDX-License-Identifier: LGPL-2.1-or-later
7+ ]]></copyright>
8+ <interface name="org_kde_kwin_server_decoration_manager" version="1">
9+ <description summary="Server side window decoration manager">
10+ This interface allows to coordinate whether the server should create
11+ a server-side window decoration around a wl_surface representing a
12+ shell surface (wl_shell_surface or similar). By announcing support
13+ for this interface the server indicates that it supports server
14+ side decorations.
15+
16+ Use in conjunction with zxdg_decoration_manager_v1 is undefined.
17+ </description>
18+ <request name="create">
19+ <description summary="Create a server-side decoration object for a given surface">
20+ When a client creates a server-side decoration object it indicates
21+ that it supports the protocol. The client is supposed to tell the
22+ server whether it wants server-side decorations or will provide
23+ client-side decorations.
24+
25+ If the client does not create a server-side decoration object for
26+ a surface the server interprets this as lack of support for this
27+ protocol and considers it as client-side decorated. Nevertheless a
28+ client-side decorated surface should use this protocol to indicate
29+ to the server that it does not want a server-side deco.
30+ </description>
31+ <arg name="id" type="new_id" interface="org_kde_kwin_server_decoration"/>
32+ <arg name="surface" type="object" interface="wl_surface"/>
33+ </request>
34+ <enum name="mode">
35+ <description summary="Possible values to use in request_mode and the event mode."/>
36+ <entry name="None" value="0" summary="Undecorated: The surface is not decorated at all, neither server nor client-side. An example is a popup surface which should not be decorated."/>
37+ <entry name="Client" value="1" summary="Client-side decoration: The decoration is part of the surface and the client."/>
38+ <entry name="Server" value="2" summary="Server-side decoration: The server embeds the surface into a decoration frame."/>
39+ </enum>
40+ <event name="default_mode">
41+ <description summary="The default mode used on the server">
42+ This event is emitted directly after binding the interface. It contains
43+ the default mode for the decoration. When a new server decoration object
44+ is created this new object will be in the default mode until the first
45+ request_mode is requested.
46+
47+ The server may change the default mode at any time.
48+ </description>
49+ <arg name="mode" type="uint" summary="The default decoration mode applied to newly created server decorations."/>
50+ </event>
51+ </interface>
52+ <interface name="org_kde_kwin_server_decoration" version="1">
53+ <request name="release" type="destructor">
54+ <description summary="release the server decoration object"/>
55+ </request>
56+ <enum name="mode">
57+ <description summary="Possible values to use in request_mode and the event mode."/>
58+ <entry name="None" value="0" summary="Undecorated: The surface is not decorated at all, neither server nor client-side. An example is a popup surface which should not be decorated."/>
59+ <entry name="Client" value="1" summary="Client-side decoration: The decoration is part of the surface and the client."/>
60+ <entry name="Server" value="2" summary="Server-side decoration: The server embeds the surface into a decoration frame."/>
61+ </enum>
62+ <request name="request_mode">
63+ <description summary="The decoration mode the surface wants to use."/>
64+ <arg name="mode" type="uint" summary="The mode this surface wants to use."/>
65+ </request>
66+ <event name="mode">
67+ <description summary="The new decoration mode applied by the server">
68+ This event is emitted directly after the decoration is created and
69+ represents the base decoration policy by the server. E.g. a server
70+ which wants all surfaces to be client-side decorated will send Client,
71+ a server which wants server-side decoration will send Server.
72+
73+ The client can request a different mode through the decoration request.
74+ The server will acknowledge this by another event with the same mode. So
75+ even if a server prefers server-side decoration it's possible to force a
76+ client-side decoration.
77+
78+ The server may emit this event at any time. In this case the client can
79+ again request a different mode. It's the responsibility of the server to
80+ prevent a feedback loop.
81+ </description>
82+ <arg name="mode" type="uint" summary="The decoration mode applied to the surface by the server."/>
83+ </event>
84+ </interface>
85+</protocol>
@@ -0,0 +1,85 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="server_decoration">
3+ <copyright><![CDATA[
4+ SPDX-FileCopyrightText: 2015 Martin Gräßlin
5+
6+ SPDX-License-Identifier: LGPL-2.1-or-later
7+ ]]></copyright>
8+ <interface name="org_kde_kwin_server_decoration_manager" version="1">
9+ <description summary="Server side window decoration manager">
10+ This interface allows to coordinate whether the server should create
11+ a server-side window decoration around a wl_surface representing a
12+ shell surface (wl_shell_surface or similar). By announcing support
13+ for this interface the server indicates that it supports server
14+ side decorations.
15+
16+ Use in conjunction with zxdg_decoration_manager_v1 is undefined.
17+ </description>
18+ <request name="create">
19+ <description summary="Create a server-side decoration object for a given surface">
20+ When a client creates a server-side decoration object it indicates
21+ that it supports the protocol. The client is supposed to tell the
22+ server whether it wants server-side decorations or will provide
23+ client-side decorations.
24+
25+ If the client does not create a server-side decoration object for
26+ a surface the server interprets this as lack of support for this
27+ protocol and considers it as client-side decorated. Nevertheless a
28+ client-side decorated surface should use this protocol to indicate
29+ to the server that it does not want a server-side deco.
30+ </description>
31+ <arg name="id" type="new_id" interface="org_kde_kwin_server_decoration"/>
32+ <arg name="surface" type="object" interface="wl_surface"/>
33+ </request>
34+ <enum name="mode">
35+ <description summary="Possible values to use in request_mode and the event mode."/>
36+ <entry name="None" value="0" summary="Undecorated: The surface is not decorated at all, neither server nor client-side. An example is a popup surface which should not be decorated."/>
37+ <entry name="Client" value="1" summary="Client-side decoration: The decoration is part of the surface and the client."/>
38+ <entry name="Server" value="2" summary="Server-side decoration: The server embeds the surface into a decoration frame."/>
39+ </enum>
40+ <event name="default_mode">
41+ <description summary="The default mode used on the server">
42+ This event is emitted directly after binding the interface. It contains
43+ the default mode for the decoration. When a new server decoration object
44+ is created this new object will be in the default mode until the first
45+ request_mode is requested.
46+
47+ The server may change the default mode at any time.
48+ </description>
49+ <arg name="mode" type="uint" summary="The default decoration mode applied to newly created server decorations."/>
50+ </event>
51+ </interface>
52+ <interface name="org_kde_kwin_server_decoration" version="1">
53+ <request name="release" type="destructor">
54+ <description summary="release the server decoration object"/>
55+ </request>
56+ <enum name="mode">
57+ <description summary="Possible values to use in request_mode and the event mode."/>
58+ <entry name="None" value="0" summary="Undecorated: The surface is not decorated at all, neither server nor client-side. An example is a popup surface which should not be decorated."/>
59+ <entry name="Client" value="1" summary="Client-side decoration: The decoration is part of the surface and the client."/>
60+ <entry name="Server" value="2" summary="Server-side decoration: The server embeds the surface into a decoration frame."/>
61+ </enum>
62+ <request name="request_mode">
63+ <description summary="The decoration mode the surface wants to use."/>
64+ <arg name="mode" type="uint" summary="The mode this surface wants to use."/>
65+ </request>
66+ <event name="mode">
67+ <description summary="The new decoration mode applied by the server">
68+ This event is emitted directly after the decoration is created and
69+ represents the base decoration policy by the server. E.g. a server
70+ which wants all surfaces to be client-side decorated will send Client,
71+ a server which wants server-side decoration will send Server.
72+
73+ The client can request a different mode through the decoration request.
74+ The server will acknowledge this by another event with the same mode. So
75+ even if a server prefers server-side decoration it's possible to force a
76+ client-side decoration.
77+
78+ The server may emit this event at any time. In this case the client can
79+ again request a different mode. It's the responsibility of the server to
80+ prevent a feedback loop.
81+ </description>
82+ <arg name="mode" type="uint" summary="The decoration mode applied to the surface by the server."/>
83+ </event>
84+ </interface>
85+</protocol>
A internal/gen/client/testdata/wlr-layer-shell-unstable-v1.xml +407 -0
@@ -0,0 +1,407 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="wlr_layer_shell_unstable_v1">
3+ <copyright>
4+ Copyright © 2017 Drew DeVault
5+
6+ Permission to use, copy, modify, distribute, and sell this
7+ software and its documentation for any purpose is hereby granted
8+ without fee, provided that the above copyright notice appear in
9+ all copies and that both that copyright notice and this permission
10+ notice appear in supporting documentation, and that the name of
11+ the copyright holders not be used in advertising or publicity
12+ pertaining to distribution of the software without specific,
13+ written prior permission. The copyright holders make no
14+ representations about the suitability of this software for any
15+ purpose. It is provided "as is" without express or implied
16+ warranty.
17+
18+ THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
19+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
20+ FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
21+ SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
23+ AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
24+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
25+ THIS SOFTWARE.
26+ </copyright>
27+
28+ <interface name="zwlr_layer_shell_v1" version="5">
29+ <description summary="create surfaces that are layers of the desktop">
30+ Clients can use this interface to assign the surface_layer role to
31+ wl_surfaces. Such surfaces are assigned to a "layer" of the output and
32+ rendered with a defined z-depth respective to each other. They may also be
33+ anchored to the edges and corners of a screen and specify input handling
34+ semantics. This interface should be suitable for the implementation of
35+ many desktop shell components, and a broad number of other applications
36+ that interact with the desktop.
37+ </description>
38+
39+ <request name="get_layer_surface">
40+ <description summary="create a layer_surface from a surface">
41+ Create a layer surface for an existing surface. This assigns the role of
42+ layer_surface, or raises a protocol error if another role is already
43+ assigned.
44+
45+ Creating a layer surface from a wl_surface which has a buffer attached
46+ or committed is a client error, and any attempts by a client to attach
47+ or manipulate a buffer prior to the first layer_surface.configure call
48+ must also be treated as errors.
49+
50+ After creating a layer_surface object and setting it up, the client
51+ must perform an initial commit without any buffer attached.
52+ The compositor will reply with a layer_surface.configure event.
53+ The client must acknowledge it and is then allowed to attach a buffer
54+ to map the surface.
55+
56+ You may pass NULL for output to allow the compositor to decide which
57+ output to use. Generally this will be the one that the user most
58+ recently interacted with.
59+
60+ Clients can specify a namespace that defines the purpose of the layer
61+ surface.
62+ </description>
63+ <arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
64+ <arg name="surface" type="object" interface="wl_surface"/>
65+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
66+ <arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
67+ <arg name="namespace" type="string" summary="namespace for the layer surface"/>
68+ </request>
69+
70+ <enum name="error">
71+ <entry name="role" value="0" summary="wl_surface has another role"/>
72+ <entry name="invalid_layer" value="1" summary="layer value is invalid"/>
73+ <entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
74+ </enum>
75+
76+ <enum name="layer">
77+ <description summary="available layers for surfaces">
78+ These values indicate which layers a surface can be rendered in. They
79+ are ordered by z depth, bottom-most first. Traditional shell surfaces
80+ will typically be rendered between the bottom and top layers.
81+ Fullscreen shell surfaces are typically rendered at the top layer.
82+ Multiple surfaces can share a single layer, and ordering within a
83+ single layer is undefined.
84+ </description>
85+
86+ <entry name="background" value="0"/>
87+ <entry name="bottom" value="1"/>
88+ <entry name="top" value="2"/>
89+ <entry name="overlay" value="3"/>
90+ </enum>
91+
92+ <!-- Version 3 additions -->
93+
94+ <request name="destroy" type="destructor" since="3">
95+ <description summary="destroy the layer_shell object">
96+ This request indicates that the client will not use the layer_shell
97+ object any more. Objects that have been created through this instance
98+ are not affected.
99+ </description>
100+ </request>
101+ </interface>
102+
103+ <interface name="zwlr_layer_surface_v1" version="5">
104+ <description summary="layer metadata interface">
105+ An interface that may be implemented by a wl_surface, for surfaces that
106+ are designed to be rendered as a layer of a stacked desktop-like
107+ environment.
108+
109+ Layer surface state (layer, size, anchor, exclusive zone,
110+ margin, interactivity) is double-buffered, and will be applied at the
111+ time wl_surface.commit of the corresponding wl_surface is called.
112+
113+ Attaching a null buffer to a layer surface unmaps it.
114+
115+ Unmapping a layer_surface means that the surface cannot be shown by the
116+ compositor until it is explicitly mapped again. The layer_surface
117+ returns to the state it had right after layer_shell.get_layer_surface.
118+ The client can re-map the surface by performing a commit without any
119+ buffer attached, waiting for a configure event and handling it as usual.
120+ </description>
121+
122+ <request name="set_size">
123+ <description summary="sets the size of the surface">
124+ Sets the size of the surface in surface-local coordinates. The
125+ compositor will display the surface centered with respect to its
126+ anchors.
127+
128+ If you pass 0 for either value, the compositor will assign it and
129+ inform you of the assignment in the configure event. You must set your
130+ anchor to opposite edges in the dimensions you omit; not doing so is a
131+ protocol error. Both values are 0 by default.
132+
133+ Size is double-buffered, see wl_surface.commit.
134+ </description>
135+ <arg name="width" type="uint"/>
136+ <arg name="height" type="uint"/>
137+ </request>
138+
139+ <request name="set_anchor">
140+ <description summary="configures the anchor point of the surface">
141+ Requests that the compositor anchor the surface to the specified edges
142+ and corners. If two orthogonal edges are specified (e.g. 'top' and
143+ 'left'), then the anchor point will be the intersection of the edges
144+ (e.g. the top left corner of the output); otherwise the anchor point
145+ will be centered on that edge, or in the center if none is specified.
146+
147+ Anchor is double-buffered, see wl_surface.commit.
148+ </description>
149+ <arg name="anchor" type="uint" enum="anchor"/>
150+ </request>
151+
152+ <request name="set_exclusive_zone">
153+ <description summary="configures the exclusive geometry of this surface">
154+ Requests that the compositor avoids occluding an area with other
155+ surfaces. The compositor's use of this information is
156+ implementation-dependent - do not assume that this region will not
157+ actually be occluded.
158+
159+ A positive value is only meaningful if the surface is anchored to one
160+ edge or an edge and both perpendicular edges. If the surface is not
161+ anchored, anchored to only two perpendicular edges (a corner), anchored
162+ to only two parallel edges or anchored to all edges, a positive value
163+ will be treated the same as zero.
164+
165+ A positive zone is the distance from the edge in surface-local
166+ coordinates to consider exclusive.
167+
168+ Surfaces that do not wish to have an exclusive zone may instead specify
169+ how they should interact with surfaces that do. If set to zero, the
170+ surface indicates that it would like to be moved to avoid occluding
171+ surfaces with a positive exclusive zone. If set to -1, the surface
172+ indicates that it would not like to be moved to accommodate for other
173+ surfaces, and the compositor should extend it all the way to the edges
174+ it is anchored to.
175+
176+ For example, a panel might set its exclusive zone to 10, so that
177+ maximized shell surfaces are not shown on top of it. A notification
178+ might set its exclusive zone to 0, so that it is moved to avoid
179+ occluding the panel, but shell surfaces are shown underneath it. A
180+ wallpaper or lock screen might set their exclusive zone to -1, so that
181+ they stretch below or over the panel.
182+
183+ The default value is 0.
184+
185+ Exclusive zone is double-buffered, see wl_surface.commit.
186+ </description>
187+ <arg name="zone" type="int"/>
188+ </request>
189+
190+ <request name="set_margin">
191+ <description summary="sets a margin from the anchor point">
192+ Requests that the surface be placed some distance away from the anchor
193+ point on the output, in surface-local coordinates. Setting this value
194+ for edges you are not anchored to has no effect.
195+
196+ The exclusive zone includes the margin.
197+
198+ Margin is double-buffered, see wl_surface.commit.
199+ </description>
200+ <arg name="top" type="int"/>
201+ <arg name="right" type="int"/>
202+ <arg name="bottom" type="int"/>
203+ <arg name="left" type="int"/>
204+ </request>
205+
206+ <enum name="keyboard_interactivity">
207+ <description summary="types of keyboard interaction possible for a layer shell surface">
208+ Types of keyboard interaction possible for layer shell surfaces. The
209+ rationale for this is twofold: (1) some applications are not interested
210+ in keyboard events and not allowing them to be focused can improve the
211+ desktop experience; (2) some applications will want to take exclusive
212+ keyboard focus.
213+ </description>
214+
215+ <entry name="none" value="0">
216+ <description summary="no keyboard focus is possible">
217+ This value indicates that this surface is not interested in keyboard
218+ events and the compositor should never assign it the keyboard focus.
219+
220+ This is the default value, set for newly created layer shell surfaces.
221+
222+ This is useful for e.g. desktop widgets that display information or
223+ only have interaction with non-keyboard input devices.
224+ </description>
225+ </entry>
226+ <entry name="exclusive" value="1">
227+ <description summary="request exclusive keyboard focus">
228+ Request exclusive keyboard focus if this surface is above the shell surface layer.
229+
230+ For the top and overlay layers, the seat will always give
231+ exclusive keyboard focus to the top-most layer which has keyboard
232+ interactivity set to exclusive. If this layer contains multiple
233+ surfaces with keyboard interactivity set to exclusive, the compositor
234+ determines the one receiving keyboard events in an implementation-
235+ defined manner. In this case, no guarantee is made when this surface
236+ will receive keyboard focus (if ever).
237+
238+ For the bottom and background layers, the compositor is allowed to use
239+ normal focus semantics.
240+
241+ This setting is mainly intended for applications that need to ensure
242+ they receive all keyboard events, such as a lock screen or a password
243+ prompt.
244+ </description>
245+ </entry>
246+ <entry name="on_demand" value="2" since="4">
247+ <description summary="request regular keyboard focus semantics">
248+ This requests the compositor to allow this surface to be focused and
249+ unfocused by the user in an implementation-defined manner. The user
250+ should be able to unfocus this surface even regardless of the layer
251+ it is on.
252+
253+ Typically, the compositor will want to use its normal mechanism to
254+ manage keyboard focus between layer shell surfaces with this setting
255+ and regular toplevels on the desktop layer (e.g. click to focus).
256+ Nevertheless, it is possible for a compositor to require a special
257+ interaction to focus or unfocus layer shell surfaces (e.g. requiring
258+ a click even if focus follows the mouse normally, or providing a
259+ keybinding to switch focus between layers).
260+
261+ This setting is mainly intended for desktop shell components (e.g.
262+ panels) that allow keyboard interaction. Using this option can allow
263+ implementing a desktop shell that can be fully usable without the
264+ mouse.
265+ </description>
266+ </entry>
267+ </enum>
268+
269+ <request name="set_keyboard_interactivity">
270+ <description summary="requests keyboard events">
271+ Set how keyboard events are delivered to this surface. By default,
272+ layer shell surfaces do not receive keyboard events; this request can
273+ be used to change this.
274+
275+ This setting is inherited by child surfaces set by the get_popup
276+ request.
277+
278+ Layer surfaces receive pointer, touch, and tablet events normally. If
279+ you do not want to receive them, set the input region on your surface
280+ to an empty region.
281+
282+ Keyboard interactivity is double-buffered, see wl_surface.commit.
283+ </description>
284+ <arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
285+ </request>
286+
287+ <request name="get_popup">
288+ <description summary="assign this layer_surface as an xdg_popup parent">
289+ This assigns an xdg_popup's parent to this layer_surface. This popup
290+ should have been created via xdg_surface::get_popup with the parent set
291+ to NULL, and this request must be invoked before committing the popup's
292+ initial state.
293+
294+ See the documentation of xdg_popup for more details about what an
295+ xdg_popup is and how it is used.
296+ </description>
297+ <arg name="popup" type="object" interface="xdg_popup"/>
298+ </request>
299+
300+ <request name="ack_configure">
301+ <description summary="ack a configure event">
302+ When a configure event is received, if a client commits the
303+ surface in response to the configure event, then the client
304+ must make an ack_configure request sometime before the commit
305+ request, passing along the serial of the configure event.
306+
307+ If the client receives multiple configure events before it
308+ can respond to one, it only has to ack the last configure event.
309+
310+ A client is not required to commit immediately after sending
311+ an ack_configure request - it may even ack_configure several times
312+ before its next surface commit.
313+
314+ A client may send multiple ack_configure requests before committing, but
315+ only the last request sent before a commit indicates which configure
316+ event the client really is responding to.
317+ </description>
318+ <arg name="serial" type="uint" summary="the serial from the configure event"/>
319+ </request>
320+
321+ <request name="destroy" type="destructor">
322+ <description summary="destroy the layer_surface">
323+ This request destroys the layer surface.
324+ </description>
325+ </request>
326+
327+ <event name="configure">
328+ <description summary="suggest a surface change">
329+ The configure event asks the client to resize its surface.
330+
331+ Clients should arrange their surface for the new states, and then send
332+ an ack_configure request with the serial sent in this configure event at
333+ some point before committing the new surface.
334+
335+ The client is free to dismiss all but the last configure event it
336+ received.
337+
338+ The width and height arguments specify the size of the window in
339+ surface-local coordinates.
340+
341+ The size is a hint, in the sense that the client is free to ignore it if
342+ it doesn't resize, pick a smaller size (to satisfy aspect ratio or
343+ resize in steps of NxM pixels). If the client picks a smaller size and
344+ is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
345+ surface will be centered on this axis.
346+
347+ If the width or height arguments are zero, it means the client should
348+ decide its own window dimension.
349+ </description>
350+ <arg name="serial" type="uint"/>
351+ <arg name="width" type="uint"/>
352+ <arg name="height" type="uint"/>
353+ </event>
354+
355+ <event name="closed">
356+ <description summary="surface should be closed">
357+ The closed event is sent by the compositor when the surface will no
358+ longer be shown. The output may have been destroyed or the user may
359+ have asked for it to be removed. Further changes to the surface will be
360+ ignored. The client should destroy the resource after receiving this
361+ event, and create a new surface if they so choose.
362+ </description>
363+ </event>
364+
365+ <enum name="error">
366+ <entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
367+ <entry name="invalid_size" value="1" summary="size is invalid"/>
368+ <entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
369+ <entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
370+ <entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
371+ </enum>
372+
373+ <enum name="anchor" bitfield="true">
374+ <entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
375+ <entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
376+ <entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
377+ <entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
378+ </enum>
379+
380+ <!-- Version 2 additions -->
381+
382+ <request name="set_layer" since="2">
383+ <description summary="change the layer of the surface">
384+ Change the layer that the surface is rendered on.
385+
386+ Layer is double-buffered, see wl_surface.commit.
387+ </description>
388+ <arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
389+ </request>
390+
391+ <!-- Version 5 additions -->
392+
393+ <request name="set_exclusive_edge" since="5">
394+ <description summary="set the edge the exclusive zone will be applied to">
395+ Requests an edge for the exclusive zone to apply. The exclusive
396+ edge will be automatically deduced from anchor points when possible,
397+ but when the surface is anchored to a corner, it will be necessary
398+ to set it explicitly to disambiguate, as it is not possible to deduce
399+ which one of the two corner edges should be used.
400+
401+ The edge must be one the surface is anchored to, otherwise the
402+ invalid_exclusive_edge protocol error will be raised.
403+ </description>
404+ <arg name="edge" type="uint" enum="anchor"/>
405+ </request>
406+ </interface>
407+</protocol>
@@ -0,0 +1,407 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="wlr_layer_shell_unstable_v1">
3+ <copyright>
4+ Copyright © 2017 Drew DeVault
5+
6+ Permission to use, copy, modify, distribute, and sell this
7+ software and its documentation for any purpose is hereby granted
8+ without fee, provided that the above copyright notice appear in
9+ all copies and that both that copyright notice and this permission
10+ notice appear in supporting documentation, and that the name of
11+ the copyright holders not be used in advertising or publicity
12+ pertaining to distribution of the software without specific,
13+ written prior permission. The copyright holders make no
14+ representations about the suitability of this software for any
15+ purpose. It is provided "as is" without express or implied
16+ warranty.
17+
18+ THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
19+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
20+ FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
21+ SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
23+ AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
24+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
25+ THIS SOFTWARE.
26+ </copyright>
27+
28+ <interface name="zwlr_layer_shell_v1" version="5">
29+ <description summary="create surfaces that are layers of the desktop">
30+ Clients can use this interface to assign the surface_layer role to
31+ wl_surfaces. Such surfaces are assigned to a "layer" of the output and
32+ rendered with a defined z-depth respective to each other. They may also be
33+ anchored to the edges and corners of a screen and specify input handling
34+ semantics. This interface should be suitable for the implementation of
35+ many desktop shell components, and a broad number of other applications
36+ that interact with the desktop.
37+ </description>
38+
39+ <request name="get_layer_surface">
40+ <description summary="create a layer_surface from a surface">
41+ Create a layer surface for an existing surface. This assigns the role of
42+ layer_surface, or raises a protocol error if another role is already
43+ assigned.
44+
45+ Creating a layer surface from a wl_surface which has a buffer attached
46+ or committed is a client error, and any attempts by a client to attach
47+ or manipulate a buffer prior to the first layer_surface.configure call
48+ must also be treated as errors.
49+
50+ After creating a layer_surface object and setting it up, the client
51+ must perform an initial commit without any buffer attached.
52+ The compositor will reply with a layer_surface.configure event.
53+ The client must acknowledge it and is then allowed to attach a buffer
54+ to map the surface.
55+
56+ You may pass NULL for output to allow the compositor to decide which
57+ output to use. Generally this will be the one that the user most
58+ recently interacted with.
59+
60+ Clients can specify a namespace that defines the purpose of the layer
61+ surface.
62+ </description>
63+ <arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
64+ <arg name="surface" type="object" interface="wl_surface"/>
65+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
66+ <arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
67+ <arg name="namespace" type="string" summary="namespace for the layer surface"/>
68+ </request>
69+
70+ <enum name="error">
71+ <entry name="role" value="0" summary="wl_surface has another role"/>
72+ <entry name="invalid_layer" value="1" summary="layer value is invalid"/>
73+ <entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
74+ </enum>
75+
76+ <enum name="layer">
77+ <description summary="available layers for surfaces">
78+ These values indicate which layers a surface can be rendered in. They
79+ are ordered by z depth, bottom-most first. Traditional shell surfaces
80+ will typically be rendered between the bottom and top layers.
81+ Fullscreen shell surfaces are typically rendered at the top layer.
82+ Multiple surfaces can share a single layer, and ordering within a
83+ single layer is undefined.
84+ </description>
85+
86+ <entry name="background" value="0"/>
87+ <entry name="bottom" value="1"/>
88+ <entry name="top" value="2"/>
89+ <entry name="overlay" value="3"/>
90+ </enum>
91+
92+ <!-- Version 3 additions -->
93+
94+ <request name="destroy" type="destructor" since="3">
95+ <description summary="destroy the layer_shell object">
96+ This request indicates that the client will not use the layer_shell
97+ object any more. Objects that have been created through this instance
98+ are not affected.
99+ </description>
100+ </request>
101+ </interface>
102+
103+ <interface name="zwlr_layer_surface_v1" version="5">
104+ <description summary="layer metadata interface">
105+ An interface that may be implemented by a wl_surface, for surfaces that
106+ are designed to be rendered as a layer of a stacked desktop-like
107+ environment.
108+
109+ Layer surface state (layer, size, anchor, exclusive zone,
110+ margin, interactivity) is double-buffered, and will be applied at the
111+ time wl_surface.commit of the corresponding wl_surface is called.
112+
113+ Attaching a null buffer to a layer surface unmaps it.
114+
115+ Unmapping a layer_surface means that the surface cannot be shown by the
116+ compositor until it is explicitly mapped again. The layer_surface
117+ returns to the state it had right after layer_shell.get_layer_surface.
118+ The client can re-map the surface by performing a commit without any
119+ buffer attached, waiting for a configure event and handling it as usual.
120+ </description>
121+
122+ <request name="set_size">
123+ <description summary="sets the size of the surface">
124+ Sets the size of the surface in surface-local coordinates. The
125+ compositor will display the surface centered with respect to its
126+ anchors.
127+
128+ If you pass 0 for either value, the compositor will assign it and
129+ inform you of the assignment in the configure event. You must set your
130+ anchor to opposite edges in the dimensions you omit; not doing so is a
131+ protocol error. Both values are 0 by default.
132+
133+ Size is double-buffered, see wl_surface.commit.
134+ </description>
135+ <arg name="width" type="uint"/>
136+ <arg name="height" type="uint"/>
137+ </request>
138+
139+ <request name="set_anchor">
140+ <description summary="configures the anchor point of the surface">
141+ Requests that the compositor anchor the surface to the specified edges
142+ and corners. If two orthogonal edges are specified (e.g. 'top' and
143+ 'left'), then the anchor point will be the intersection of the edges
144+ (e.g. the top left corner of the output); otherwise the anchor point
145+ will be centered on that edge, or in the center if none is specified.
146+
147+ Anchor is double-buffered, see wl_surface.commit.
148+ </description>
149+ <arg name="anchor" type="uint" enum="anchor"/>
150+ </request>
151+
152+ <request name="set_exclusive_zone">
153+ <description summary="configures the exclusive geometry of this surface">
154+ Requests that the compositor avoids occluding an area with other
155+ surfaces. The compositor's use of this information is
156+ implementation-dependent - do not assume that this region will not
157+ actually be occluded.
158+
159+ A positive value is only meaningful if the surface is anchored to one
160+ edge or an edge and both perpendicular edges. If the surface is not
161+ anchored, anchored to only two perpendicular edges (a corner), anchored
162+ to only two parallel edges or anchored to all edges, a positive value
163+ will be treated the same as zero.
164+
165+ A positive zone is the distance from the edge in surface-local
166+ coordinates to consider exclusive.
167+
168+ Surfaces that do not wish to have an exclusive zone may instead specify
169+ how they should interact with surfaces that do. If set to zero, the
170+ surface indicates that it would like to be moved to avoid occluding
171+ surfaces with a positive exclusive zone. If set to -1, the surface
172+ indicates that it would not like to be moved to accommodate for other
173+ surfaces, and the compositor should extend it all the way to the edges
174+ it is anchored to.
175+
176+ For example, a panel might set its exclusive zone to 10, so that
177+ maximized shell surfaces are not shown on top of it. A notification
178+ might set its exclusive zone to 0, so that it is moved to avoid
179+ occluding the panel, but shell surfaces are shown underneath it. A
180+ wallpaper or lock screen might set their exclusive zone to -1, so that
181+ they stretch below or over the panel.
182+
183+ The default value is 0.
184+
185+ Exclusive zone is double-buffered, see wl_surface.commit.
186+ </description>
187+ <arg name="zone" type="int"/>
188+ </request>
189+
190+ <request name="set_margin">
191+ <description summary="sets a margin from the anchor point">
192+ Requests that the surface be placed some distance away from the anchor
193+ point on the output, in surface-local coordinates. Setting this value
194+ for edges you are not anchored to has no effect.
195+
196+ The exclusive zone includes the margin.
197+
198+ Margin is double-buffered, see wl_surface.commit.
199+ </description>
200+ <arg name="top" type="int"/>
201+ <arg name="right" type="int"/>
202+ <arg name="bottom" type="int"/>
203+ <arg name="left" type="int"/>
204+ </request>
205+
206+ <enum name="keyboard_interactivity">
207+ <description summary="types of keyboard interaction possible for a layer shell surface">
208+ Types of keyboard interaction possible for layer shell surfaces. The
209+ rationale for this is twofold: (1) some applications are not interested
210+ in keyboard events and not allowing them to be focused can improve the
211+ desktop experience; (2) some applications will want to take exclusive
212+ keyboard focus.
213+ </description>
214+
215+ <entry name="none" value="0">
216+ <description summary="no keyboard focus is possible">
217+ This value indicates that this surface is not interested in keyboard
218+ events and the compositor should never assign it the keyboard focus.
219+
220+ This is the default value, set for newly created layer shell surfaces.
221+
222+ This is useful for e.g. desktop widgets that display information or
223+ only have interaction with non-keyboard input devices.
224+ </description>
225+ </entry>
226+ <entry name="exclusive" value="1">
227+ <description summary="request exclusive keyboard focus">
228+ Request exclusive keyboard focus if this surface is above the shell surface layer.
229+
230+ For the top and overlay layers, the seat will always give
231+ exclusive keyboard focus to the top-most layer which has keyboard
232+ interactivity set to exclusive. If this layer contains multiple
233+ surfaces with keyboard interactivity set to exclusive, the compositor
234+ determines the one receiving keyboard events in an implementation-
235+ defined manner. In this case, no guarantee is made when this surface
236+ will receive keyboard focus (if ever).
237+
238+ For the bottom and background layers, the compositor is allowed to use
239+ normal focus semantics.
240+
241+ This setting is mainly intended for applications that need to ensure
242+ they receive all keyboard events, such as a lock screen or a password
243+ prompt.
244+ </description>
245+ </entry>
246+ <entry name="on_demand" value="2" since="4">
247+ <description summary="request regular keyboard focus semantics">
248+ This requests the compositor to allow this surface to be focused and
249+ unfocused by the user in an implementation-defined manner. The user
250+ should be able to unfocus this surface even regardless of the layer
251+ it is on.
252+
253+ Typically, the compositor will want to use its normal mechanism to
254+ manage keyboard focus between layer shell surfaces with this setting
255+ and regular toplevels on the desktop layer (e.g. click to focus).
256+ Nevertheless, it is possible for a compositor to require a special
257+ interaction to focus or unfocus layer shell surfaces (e.g. requiring
258+ a click even if focus follows the mouse normally, or providing a
259+ keybinding to switch focus between layers).
260+
261+ This setting is mainly intended for desktop shell components (e.g.
262+ panels) that allow keyboard interaction. Using this option can allow
263+ implementing a desktop shell that can be fully usable without the
264+ mouse.
265+ </description>
266+ </entry>
267+ </enum>
268+
269+ <request name="set_keyboard_interactivity">
270+ <description summary="requests keyboard events">
271+ Set how keyboard events are delivered to this surface. By default,
272+ layer shell surfaces do not receive keyboard events; this request can
273+ be used to change this.
274+
275+ This setting is inherited by child surfaces set by the get_popup
276+ request.
277+
278+ Layer surfaces receive pointer, touch, and tablet events normally. If
279+ you do not want to receive them, set the input region on your surface
280+ to an empty region.
281+
282+ Keyboard interactivity is double-buffered, see wl_surface.commit.
283+ </description>
284+ <arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
285+ </request>
286+
287+ <request name="get_popup">
288+ <description summary="assign this layer_surface as an xdg_popup parent">
289+ This assigns an xdg_popup's parent to this layer_surface. This popup
290+ should have been created via xdg_surface::get_popup with the parent set
291+ to NULL, and this request must be invoked before committing the popup's
292+ initial state.
293+
294+ See the documentation of xdg_popup for more details about what an
295+ xdg_popup is and how it is used.
296+ </description>
297+ <arg name="popup" type="object" interface="xdg_popup"/>
298+ </request>
299+
300+ <request name="ack_configure">
301+ <description summary="ack a configure event">
302+ When a configure event is received, if a client commits the
303+ surface in response to the configure event, then the client
304+ must make an ack_configure request sometime before the commit
305+ request, passing along the serial of the configure event.
306+
307+ If the client receives multiple configure events before it
308+ can respond to one, it only has to ack the last configure event.
309+
310+ A client is not required to commit immediately after sending
311+ an ack_configure request - it may even ack_configure several times
312+ before its next surface commit.
313+
314+ A client may send multiple ack_configure requests before committing, but
315+ only the last request sent before a commit indicates which configure
316+ event the client really is responding to.
317+ </description>
318+ <arg name="serial" type="uint" summary="the serial from the configure event"/>
319+ </request>
320+
321+ <request name="destroy" type="destructor">
322+ <description summary="destroy the layer_surface">
323+ This request destroys the layer surface.
324+ </description>
325+ </request>
326+
327+ <event name="configure">
328+ <description summary="suggest a surface change">
329+ The configure event asks the client to resize its surface.
330+
331+ Clients should arrange their surface for the new states, and then send
332+ an ack_configure request with the serial sent in this configure event at
333+ some point before committing the new surface.
334+
335+ The client is free to dismiss all but the last configure event it
336+ received.
337+
338+ The width and height arguments specify the size of the window in
339+ surface-local coordinates.
340+
341+ The size is a hint, in the sense that the client is free to ignore it if
342+ it doesn't resize, pick a smaller size (to satisfy aspect ratio or
343+ resize in steps of NxM pixels). If the client picks a smaller size and
344+ is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
345+ surface will be centered on this axis.
346+
347+ If the width or height arguments are zero, it means the client should
348+ decide its own window dimension.
349+ </description>
350+ <arg name="serial" type="uint"/>
351+ <arg name="width" type="uint"/>
352+ <arg name="height" type="uint"/>
353+ </event>
354+
355+ <event name="closed">
356+ <description summary="surface should be closed">
357+ The closed event is sent by the compositor when the surface will no
358+ longer be shown. The output may have been destroyed or the user may
359+ have asked for it to be removed. Further changes to the surface will be
360+ ignored. The client should destroy the resource after receiving this
361+ event, and create a new surface if they so choose.
362+ </description>
363+ </event>
364+
365+ <enum name="error">
366+ <entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
367+ <entry name="invalid_size" value="1" summary="size is invalid"/>
368+ <entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
369+ <entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
370+ <entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
371+ </enum>
372+
373+ <enum name="anchor" bitfield="true">
374+ <entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
375+ <entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
376+ <entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
377+ <entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
378+ </enum>
379+
380+ <!-- Version 2 additions -->
381+
382+ <request name="set_layer" since="2">
383+ <description summary="change the layer of the surface">
384+ Change the layer that the surface is rendered on.
385+
386+ Layer is double-buffered, see wl_surface.commit.
387+ </description>
388+ <arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
389+ </request>
390+
391+ <!-- Version 5 additions -->
392+
393+ <request name="set_exclusive_edge" since="5">
394+ <description summary="set the edge the exclusive zone will be applied to">
395+ Requests an edge for the exclusive zone to apply. The exclusive
396+ edge will be automatically deduced from anchor points when possible,
397+ but when the surface is anchored to a corner, it will be necessary
398+ to set it explicitly to disambiguate, as it is not possible to deduce
399+ which one of the two corner edges should be used.
400+
401+ The edge must be one the surface is anchored to, otherwise the
402+ invalid_exclusive_edge protocol error will be raised.
403+ </description>
404+ <arg name="edge" type="uint" enum="anchor"/>
405+ </request>
406+ </interface>
407+</protocol>

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 1

↑↓ moveenter openesc close