detached d5c3b4c

feat(scanner): port Wayland XML generator and pin core protocol source

commit
d5c3b4c24b23fa49a5ca03e6b65c6e3f5ecf405d
author
brice · · 2026-09-28 07:14 UTC
parent
ab733e5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +4596 -0
A cmd/wlturbo-scanner/main.go +132 -0
@@ -0,0 +1,132 @@
1+// Command wlturbo-scanner generates Go bindings from Wayland protocol XML files.
2+//
3+// Usage:
4+//
5+// wlturbo-scanner [flags] protocol.xml
6+//
7+// Flags:
8+//
9+// -o, --output Output file path (default: <protocol>_generated.go)
10+// -p, --package Go package name (default: derived from protocol name)
11+// -h, --help Show help
12+//
13+// Examples:
14+//
15+// # Generate bindings for virtual pointer protocol
16+// wlturbo-scanner -o virtual_pointer.go ../wlr-protocols/unstable/wlr-virtual-pointer-unstable-v1.xml
17+//
18+// # Generate with custom package name
19+// wlturbo-scanner -p vpointer -o vpointer.go protocol.xml
20+package main
21+
22+import (
23+ "flag"
24+ "fmt"
25+ "log"
26+ "os"
27+ "path/filepath"
28+ "strings"
29+
30+ "github.com/bnema/wlturbo/internal/scanner"
31+)
32+
33+func main() {
34+ var (
35+ outputPath string
36+ packageName string
37+ showHelp bool
38+ crossImports crossFlags
39+ )
40+
41+ flag.StringVar(&outputPath, "o", "", "Output file path")
42+ flag.StringVar(&outputPath, "output", "", "Output file path")
43+ flag.StringVar(&packageName, "p", "", "Go package name")
44+ flag.StringVar(&packageName, "package", "", "Go package name")
45+ flag.Var(&crossImports, "import", "Interface=Go import path (repeatable)")
46+ flag.BoolVar(&showHelp, "h", false, "Show help")
47+ flag.BoolVar(&showHelp, "help", false, "Show help")
48+
49+ flag.Usage = func() {
50+ fmt.Fprintf(os.Stderr, "Usage: %s [flags] protocol.xml\n\n", os.Args[0])
51+ fmt.Fprintf(os.Stderr, "Generates Go bindings from Wayland protocol XML files.\n\n")
52+ fmt.Fprintf(os.Stderr, "Flags:\n")
53+ flag.PrintDefaults()
54+ fmt.Fprintf(os.Stderr, "\nExamples:\n")
55+ fmt.Fprintf(os.Stderr, " # Generate bindings for virtual pointer protocol\n")
56+ fmt.Fprintf(os.Stderr, " %s -o virtual_pointer.go wlr-virtual-pointer-unstable-v1.xml\n\n", os.Args[0])
57+ fmt.Fprintf(os.Stderr, " # Generate with custom package name\n")
58+ fmt.Fprintf(os.Stderr, " %s -p vpointer -o vpointer.go protocol.xml\n", os.Args[0])
59+ }
60+
61+ flag.Parse()
62+
63+ if showHelp || flag.NArg() == 0 {
64+ flag.Usage()
65+ if showHelp {
66+ os.Exit(0)
67+ }
68+ os.Exit(1)
69+ }
70+
71+ xmlPath := flag.Arg(0)
72+
73+ // Derive output path if not specified
74+ if outputPath == "" {
75+ base := filepath.Base(xmlPath)
76+ base = strings.TrimSuffix(base, filepath.Ext(base))
77+ outputPath = base + "_generated.go"
78+ }
79+
80+ // Derive package name if not specified
81+ if packageName == "" {
82+ base := filepath.Base(xmlPath)
83+ base = strings.TrimSuffix(base, filepath.Ext(base))
84+
85+ // Clean up common prefixes/suffixes
86+ base = strings.TrimPrefix(base, "wlr-")
87+ base = strings.TrimPrefix(base, "zwlr-")
88+ base = strings.TrimSuffix(base, "-unstable-v1")
89+ base = strings.TrimSuffix(base, "-unstable-v2")
90+ base = strings.TrimSuffix(base, "-stable-v1")
91+
92+ // Convert to valid Go package name
93+ packageName = strings.ReplaceAll(base, "-", "_")
94+ }
95+
96+ // Create scanner
97+ s := scanner.NewScanner()
98+ s.CrossPackage = crossImports
99+
100+ // Parse XML
101+ if err := s.ParseXML(xmlPath); err != nil {
102+ log.Fatalf("Failed to parse XML: %v", err)
103+ }
104+
105+ // Generate code
106+ code, err := s.Generate(packageName)
107+ if err != nil {
108+ log.Fatalf("Failed to generate code: %v", err)
109+ }
110+
111+ // Write output with secure permissions
112+ if err := os.WriteFile(outputPath, code, 0600); err != nil {
113+ log.Fatalf("Failed to write output: %v", err)
114+ }
115+
116+ fmt.Printf("Generated %s (package %s)\n", outputPath, packageName)
117+}
118+
119+type crossFlags map[string]string
120+
121+func (f *crossFlags) String() string { return fmt.Sprint(map[string]string(*f)) }
122+func (f *crossFlags) Set(v string) error {
123+ parts := strings.SplitN(v, "=", 2)
124+ if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
125+ return fmt.Errorf("-import must be interface=GoImportPath")
126+ }
127+ if *f == nil {
128+ *f = make(map[string]string)
129+ }
130+ (*f)[parts[0]] = parts[1]
131+ return nil
132+}
@@ -0,0 +1,132 @@
1+// Command wlturbo-scanner generates Go bindings from Wayland protocol XML files.
2+//
3+// Usage:
4+//
5+// wlturbo-scanner [flags] protocol.xml
6+//
7+// Flags:
8+//
9+// -o, --output Output file path (default: <protocol>_generated.go)
10+// -p, --package Go package name (default: derived from protocol name)
11+// -h, --help Show help
12+//
13+// Examples:
14+//
15+// # Generate bindings for virtual pointer protocol
16+// wlturbo-scanner -o virtual_pointer.go ../wlr-protocols/unstable/wlr-virtual-pointer-unstable-v1.xml
17+//
18+// # Generate with custom package name
19+// wlturbo-scanner -p vpointer -o vpointer.go protocol.xml
20+package main
21+
22+import (
23+ "flag"
24+ "fmt"
25+ "log"
26+ "os"
27+ "path/filepath"
28+ "strings"
29+
30+ "github.com/bnema/wlturbo/internal/scanner"
31+)
32+
33+func main() {
34+ var (
35+ outputPath string
36+ packageName string
37+ showHelp bool
38+ crossImports crossFlags
39+ )
40+
41+ flag.StringVar(&outputPath, "o", "", "Output file path")
42+ flag.StringVar(&outputPath, "output", "", "Output file path")
43+ flag.StringVar(&packageName, "p", "", "Go package name")
44+ flag.StringVar(&packageName, "package", "", "Go package name")
45+ flag.Var(&crossImports, "import", "Interface=Go import path (repeatable)")
46+ flag.BoolVar(&showHelp, "h", false, "Show help")
47+ flag.BoolVar(&showHelp, "help", false, "Show help")
48+
49+ flag.Usage = func() {
50+ fmt.Fprintf(os.Stderr, "Usage: %s [flags] protocol.xml\n\n", os.Args[0])
51+ fmt.Fprintf(os.Stderr, "Generates Go bindings from Wayland protocol XML files.\n\n")
52+ fmt.Fprintf(os.Stderr, "Flags:\n")
53+ flag.PrintDefaults()
54+ fmt.Fprintf(os.Stderr, "\nExamples:\n")
55+ fmt.Fprintf(os.Stderr, " # Generate bindings for virtual pointer protocol\n")
56+ fmt.Fprintf(os.Stderr, " %s -o virtual_pointer.go wlr-virtual-pointer-unstable-v1.xml\n\n", os.Args[0])
57+ fmt.Fprintf(os.Stderr, " # Generate with custom package name\n")
58+ fmt.Fprintf(os.Stderr, " %s -p vpointer -o vpointer.go protocol.xml\n", os.Args[0])
59+ }
60+
61+ flag.Parse()
62+
63+ if showHelp || flag.NArg() == 0 {
64+ flag.Usage()
65+ if showHelp {
66+ os.Exit(0)
67+ }
68+ os.Exit(1)
69+ }
70+
71+ xmlPath := flag.Arg(0)
72+
73+ // Derive output path if not specified
74+ if outputPath == "" {
75+ base := filepath.Base(xmlPath)
76+ base = strings.TrimSuffix(base, filepath.Ext(base))
77+ outputPath = base + "_generated.go"
78+ }
79+
80+ // Derive package name if not specified
81+ if packageName == "" {
82+ base := filepath.Base(xmlPath)
83+ base = strings.TrimSuffix(base, filepath.Ext(base))
84+
85+ // Clean up common prefixes/suffixes
86+ base = strings.TrimPrefix(base, "wlr-")
87+ base = strings.TrimPrefix(base, "zwlr-")
88+ base = strings.TrimSuffix(base, "-unstable-v1")
89+ base = strings.TrimSuffix(base, "-unstable-v2")
90+ base = strings.TrimSuffix(base, "-stable-v1")
91+
92+ // Convert to valid Go package name
93+ packageName = strings.ReplaceAll(base, "-", "_")
94+ }
95+
96+ // Create scanner
97+ s := scanner.NewScanner()
98+ s.CrossPackage = crossImports
99+
100+ // Parse XML
101+ if err := s.ParseXML(xmlPath); err != nil {
102+ log.Fatalf("Failed to parse XML: %v", err)
103+ }
104+
105+ // Generate code
106+ code, err := s.Generate(packageName)
107+ if err != nil {
108+ log.Fatalf("Failed to generate code: %v", err)
109+ }
110+
111+ // Write output with secure permissions
112+ if err := os.WriteFile(outputPath, code, 0600); err != nil {
113+ log.Fatalf("Failed to write output: %v", err)
114+ }
115+
116+ fmt.Printf("Generated %s (package %s)\n", outputPath, packageName)
117+}
118+
119+type crossFlags map[string]string
120+
121+func (f *crossFlags) String() string { return fmt.Sprint(map[string]string(*f)) }
122+func (f *crossFlags) Set(v string) error {
123+ parts := strings.SplitN(v, "=", 2)
124+ if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
125+ return fmt.Errorf("-import must be interface=GoImportPath")
126+ }
127+ if *f == nil {
128+ *f = make(map[string]string)
129+ }
130+ (*f)[parts[0]] = parts[1]
131+ return nil
132+}
A internal/scanner/scanner.go +714 -0
@@ -0,0 +1,714 @@
1+// Package scanner 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 scanner
10+
11+import (
12+ "bytes"
13+ "encoding/xml"
14+ "fmt"
15+ "go/format"
16+ "io"
17+ "os"
18+ "path/filepath"
19+ "sort"
20+ "strings"
21+ "text/template"
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+// Protocol represents a Wayland protocol specification
29+type Protocol struct {
30+ XMLName xml.Name `xml:"protocol"`
31+ Name string `xml:"name,attr"`
32+ Copyright string `xml:"copyright"`
33+ Description *Description `xml:"description"`
34+ Interfaces []Interface `xml:"interface"`
35+}
36+
37+// Interface represents a Wayland interface in the protocol
38+type Interface struct {
39+ Name string `xml:"name,attr"`
40+ Version int `xml:"version,attr"`
41+ Description *Description `xml:"description"`
42+ Requests []Request `xml:"request"`
43+ Events []Event `xml:"event"`
44+ Enums []Enum `xml:"enum"`
45+}
46+
47+// Request represents a client-to-server message
48+type Request struct {
49+ Name string `xml:"name,attr"`
50+ Type string `xml:"type,attr"` // "destructor" or empty
51+ Since int `xml:"since,attr"`
52+ Description *Description `xml:"description"`
53+ Args []Arg `xml:"arg"`
54+}
55+
56+// Event represents a server-to-client message
57+type Event struct {
58+ Type string `xml:"type,attr"`
59+ Name string `xml:"name,attr"`
60+ Since int `xml:"since,attr"`
61+ Description *Description `xml:"description"`
62+ Args []Arg `xml:"arg"`
63+}
64+
65+// Arg represents a message argument
66+type Arg struct {
67+ Name string `xml:"name,attr"`
68+ Type string `xml:"type,attr"`
69+ Summary string `xml:"summary,attr"`
70+ Interface string `xml:"interface,attr"`
71+ AllowNull bool `xml:"allow-null,attr"`
72+ Enum string `xml:"enum,attr"`
73+}
74+
75+// Enum represents an enumeration type
76+type Enum struct {
77+ Name string `xml:"name,attr"`
78+ Since int `xml:"since,attr"`
79+ Bitfield bool `xml:"bitfield,attr"`
80+ Description *Description `xml:"description"`
81+ Entries []Entry `xml:"entry"`
82+}
83+
84+// Entry represents an enum value
85+type Entry struct {
86+ Name string `xml:"name,attr"`
87+ Value string `xml:"value,attr"`
88+ Summary string `xml:"summary,attr"`
89+ Since int `xml:"since,attr"`
90+}
91+
92+// Description represents documentation
93+type Description struct {
94+ Summary string `xml:"summary,attr"`
95+ Text string `xml:",chardata"`
96+}
97+
98+// Scanner generates Go bindings from Wayland protocol XML
99+type Scanner struct {
100+ protocol *Protocol
101+ source string
102+ // CrossPackage maps interface names to Go package import paths. Local wins.
103+ CrossPackage map[string]string
104+}
105+
106+// NewScanner creates a new protocol scanner
107+func NewScanner() *Scanner {
108+ return &Scanner{}
109+}
110+
111+// ParseXML parses a Wayland protocol XML file
112+func (s *Scanner) ParseXML(path string) error {
113+ file, err := os.Open(path)
114+ if err != nil {
115+ return fmt.Errorf("failed to open XML file: %w", err)
116+ }
117+ defer func() { _ = file.Close() }()
118+
119+ data, err := io.ReadAll(file)
120+ if err != nil {
121+ return fmt.Errorf("failed to read XML file: %w", err)
122+ }
123+
124+ var protocol Protocol
125+ if err := xml.Unmarshal(data, &protocol); err != nil {
126+ return fmt.Errorf("failed to parse XML: %w", err)
127+ }
128+
129+ s.protocol = &protocol
130+ // Only the base name is recorded: generated output must not depend on the
131+ // path the XML happened to be read from.
132+ s.source = filepath.Base(path)
133+ return nil
134+}
135+
136+// Generate creates Go source code for the parsed protocol
137+func (s *Scanner) Generate(packageName string) ([]byte, error) {
138+ if s.protocol == nil {
139+ return nil, fmt.Errorf("no protocol parsed")
140+ }
141+
142+ data, err := s.prepareTemplateData(packageName)
143+ if err != nil {
144+ return nil, err
145+ }
146+
147+ var buf bytes.Buffer
148+ tmpl := template.Must(template.New("protocol").Parse(protocolTemplate))
149+ if err := tmpl.Execute(&buf, data); err != nil {
150+ return nil, fmt.Errorf("failed to execute template: %w", err)
151+ }
152+
153+ formatted, err := format.Source(buf.Bytes())
154+ if err != nil {
155+ return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err)
156+ }
157+ return formatted, nil
158+}
159+
160+// templateData holds data for template generation
161+type templateData struct {
162+ Package string
163+ Protocol string
164+ Source string
165+ ImportBlock string
166+ Constants []constantData
167+ Interfaces []interfaceData
168+}
169+
170+type interfaceData struct {
171+ Name string
172+ GoName string
173+ Version int
174+ HasEvents bool
175+ SignatureCases []signatureCase
176+ Requests []requestData
177+ Events []eventData
178+}
179+
180+type signatureCase struct {
181+ Opcode int
182+ Signature string
183+}
184+type requestData struct {
185+ Name string
186+ GoName string
187+ Doc string
188+ Opcode int
189+ Destructor bool
190+ Params string
191+ Results string
192+ ArgPreparations []string
193+ ArgExprs []string
194+ CreatesChild bool
195+ ChildVar string
196+ ChildType string
197+ ErrorReturn string
198+ FDArgs []string
199+ HasFDs bool
200+ SendCall string
201+}
202+
203+// argSlot is one positional argument of a request, in declaration order. A
204+// new_id slot is filled in once the child object has been created.
205+type argSlot struct {
206+ newID bool
207+ expr string
208+}
209+
210+type eventData struct {
211+ HasFD bool
212+ ServerDestroy bool
213+ FDNames []string
214+ Name string
215+ GoName string
216+ Opcode int
217+ HandlerField string
218+ HandlerAccessor string
219+ HandlerParams string
220+ HandlerArgs string
221+ DecodeLines []string
222+}
223+
224+type constantData struct {
225+ Name string
226+ Value string
227+ Type string
228+ Enum string
229+ Entry string
230+}
231+
232+// Only display and registry have handwritten bootstrap bindings.
233+var bootstrapTypes = map[string]bool{
234+ "Display": true,
235+ "Registry": true,
236+}
237+
238+// goKeywords are avoided as parameter names because generated identifiers
239+// must not collide with the language.
240+var goKeywords = map[string]bool{
241+ "break": true, "case": true, "chan": true, "const": true, "continue": true,
242+ "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
243+ "func": true, "go": true, "goto": true, "if": true, "import": true,
244+ "interface": true, "map": true, "package": true, "range": true, "return": true,
245+ "select": true, "struct": true, "switch": true, "type": true, "var": true,
246+}
247+
248+func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
249+ data := templateData{
250+ Package: packageName,
251+ Protocol: s.protocol.Name,
252+ Source: s.source,
253+ }
254+
255+ imports := []string{TransportImport}
256+ usedCross := make(map[string]bool)
257+ for _, candidate := range s.protocol.Interfaces {
258+ for _, req := range candidate.Requests {
259+ for _, arg := range req.Args {
260+ if arg.Type == "object" || arg.Type == "new_id" {
261+ usedCross[arg.Interface] = true
262+ }
263+ }
264+ }
265+ for _, ev := range candidate.Events {
266+ for _, arg := range ev.Args {
267+ if arg.Type == "object" || arg.Type == "new_id" {
268+ usedCross[arg.Interface] = true
269+ }
270+ }
271+ }
272+ }
273+ for iface, path := range s.CrossPackage {
274+ local := false
275+ for _, candidate := range s.protocol.Interfaces {
276+ if candidate.Name == iface {
277+ local = true
278+ break
279+ }
280+ }
281+ if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
282+ continue
283+ }
284+ alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
285+ imports = append(imports, alias+" "+path)
286+ }
287+ sort.Strings(imports)
288+ stdImports := []string{}
289+ needsSync := false
290+
291+ // Enum constants are package-level declarations, so two interfaces that
292+ // declare the same enum entry (for example xdg_surface.error.invalid_size
293+ // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
294+ // occurs in more than one interface are qualified with their interface
295+ // name below; unique names keep their historical, unprefixed form.
296+ enumEntryCounts := map[string]int{}
297+ for _, iface := range s.protocol.Interfaces {
298+ for _, enum := range iface.Enums {
299+ for _, entry := range enum.Entries {
300+ enumEntryCounts[constantName(enum.Name, entry.Name)]++
301+ }
302+ }
303+ }
304+
305+ for _, iface := range s.protocol.Interfaces {
306+ if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
307+ continue
308+ }
309+ ifaceData, err := s.processInterface(iface)
310+ if err != nil {
311+ return data, err
312+ }
313+ data.Interfaces = append(data.Interfaces, ifaceData)
314+ if len(iface.Events) > 0 {
315+ needsSync = true
316+ }
317+
318+ for _, enum := range iface.Enums {
319+ for _, entry := range enum.Entries {
320+ name := constantName(enum.Name, entry.Name)
321+ if enumEntryCounts[name] > 1 {
322+ name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
323+ }
324+ // Enum values are emitted untyped: the same enum travels as a uint
325+ // in one protocol and as an int in another, and an untyped constant
326+ // assigns to either without casts at the call site.
327+ data.Constants = append(data.Constants, constantData{
328+ Name: name,
329+ Value: entry.Value,
330+ Enum: enum.Name,
331+ Entry: entry.Name,
332+ })
333+ }
334+ }
335+ }
336+
337+ // Handlers are registered and dispatched from different goroutines, so an
338+ // interface with events needs a mutex.
339+ if needsSync {
340+ stdImports = append(stdImports, "sync")
341+ }
342+
343+ var block strings.Builder
344+ block.WriteString("import (\n")
345+ for _, imp := range stdImports {
346+ fmt.Fprintf(&block, "\t%q\n", imp)
347+ }
348+ if len(stdImports) > 0 && len(imports) > 0 {
349+ block.WriteString("\n")
350+ }
351+ for _, imp := range imports {
352+ if strings.Contains(imp, " ") {
353+ parts := strings.SplitN(imp, " ", 2)
354+ fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
355+ } else {
356+ fmt.Fprintf(&block, "\t%q\n", imp)
357+ }
358+ }
359+ block.WriteString(")")
360+ data.ImportBlock = block.String()
361+
362+ return data, nil
363+}
364+
365+func (s *Scanner) processInterface(iface Interface) (interfaceData, error) {
366+ data := interfaceData{
367+ Name: iface.Name,
368+ GoName: s.toGoName(iface.Name),
369+ Version: iface.Version,
370+ HasEvents: len(iface.Events) > 0,
371+ }
372+
373+ for i, req := range iface.Requests {
374+ request, err := s.processRequest(iface, req, i)
375+ if err != nil {
376+ return data, err
377+ }
378+ data.Requests = append(data.Requests, request)
379+ }
380+
381+ for i, event := range iface.Events {
382+ var sig strings.Builder
383+ for _, arg := range event.Args {
384+ sig.WriteString(arg.Type)
385+ sig.WriteByte(',')
386+ }
387+ data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
388+ eventData, err := s.processEvent(event, i)
389+ if iface.Name == "wl_callback" && event.Name == "done" {
390+ eventData.ServerDestroy = true
391+ }
392+ if err != nil {
393+ return data, err
394+ }
395+ data.Events = append(data.Events, eventData)
396+ }
397+
398+ return data, nil
399+}
400+
401+func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) {
402+ data := requestData{
403+ Name: req.Name,
404+ GoName: s.toGoName(req.Name),
405+ Doc: s.formatDescription(req.Description),
406+ Opcode: opcode,
407+ Destructor: req.Type == "destructor",
408+ ChildVar: "child",
409+ }
410+
411+ var (
412+ params []string
413+ argSlots []argSlot
414+ argExprs []string
415+ fdExprs []string
416+ newIDArg *Arg
417+ )
418+
419+ for i, arg := range req.Args {
420+ if arg.Type == "new_id" {
421+ if newIDArg != nil {
422+ return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
423+ }
424+ newIDArg = &arg
425+ // The child object is created later, but it keeps its declared
426+ // position: the wire order is the argument order.
427+ argSlots = append(argSlots, argSlot{newID: true})
428+ continue
429+ }
430+
431+ name := s.paramName(arg.Name, i)
432+ goType, err := s.goTypeForArg(arg)
433+ if err != nil {
434+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
435+ }
436+
437+ expr := name
438+ switch arg.Type {
439+ case "object":
440+ // Objects are pointer types, and travel as wl.Object so a nil
441+ // pointer is never sent as a non-nil interface holding a typed nil.
442+ params = append(params, name+" *"+goType)
443+ holder := fmt.Sprintf("arg%d", i)
444+ data.ArgPreparations = append(data.ArgPreparations,
445+ fmt.Sprintf("var %s wl.Object", holder),
446+ fmt.Sprintf("if %s != nil {", name),
447+ fmt.Sprintf("\t%s = %s", holder, name),
448+ "}")
449+ expr = holder
450+
451+ case "fd":
452+ // Descriptors travel out of band: the body carries no value for
453+ // them, and the descriptor list is attached to the request.
454+ params = append(params, name+" int")
455+ fdExprs = append(fdExprs, name)
456+ data.FDArgs = append(data.FDArgs, name)
457+ expr = "uintptr(" + name + ")"
458+
459+ default:
460+ params = append(params, name+" "+goType)
461+ }
462+
463+ argSlots = append(argSlots, argSlot{expr: expr})
464+ }
465+
466+ if newIDArg != nil {
467+ childType, err := s.goTypeForArg(*newIDArg)
468+ if err != nil {
469+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
470+ }
471+ data.CreatesChild = true
472+ data.ChildType = childType
473+ }
474+
475+ for _, slot := range argSlots {
476+ if slot.newID {
477+ argExprs = append(argExprs, data.ChildVar)
478+ continue
479+ }
480+ argExprs = append(argExprs, slot.expr)
481+ }
482+
483+ data.Params = strings.Join(params, ", ")
484+ data.ArgExprs = argExprs
485+ args := ""
486+ if len(argExprs) > 0 {
487+ args = ", " + strings.Join(argExprs, ", ")
488+ }
489+ if len(fdExprs) > 0 {
490+ data.HasFDs = true
491+ data.SendCall = fmt.Sprintf("SendRequestWithFDs(o, %d, []int{%s}%s)", opcode, strings.Join(fdExprs, ", "), args)
492+ } else {
493+ data.SendCall = fmt.Sprintf("SendRequest(o, %d%s)", opcode, args)
494+ }
495+ if data.CreatesChild {
496+ data.Results = "(*" + data.ChildType + ", error)"
497+ data.ErrorReturn = "nil, err"
498+ } else {
499+ data.Results = "error"
500+ data.ErrorReturn = "err"
501+ }
502+
503+ return data, nil
504+}
505+
506+func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) {
507+ goName := s.toGoName(event.Name)
508+ data := eventData{
509+ ServerDestroy: event.Type == "destructor",
510+ Name: event.Name,
511+ GoName: goName,
512+ Opcode: opcode,
513+ HandlerField: "on" + goName,
514+ HandlerAccessor: "handlersFor" + goName,
515+ }
516+
517+ var params, args []string
518+
519+ for i, arg := range event.Args {
520+ name := s.paramName(arg.Name, i)
521+
522+ switch arg.Type {
523+ case "new_id":
524+ childType, err := s.goTypeForArg(arg)
525+ if err != nil {
526+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
527+ }
528+ child := name + "Object"
529+ data.DecodeLines = append(data.DecodeLines,
530+ fmt.Sprintf("%sID := event.Uint32()", name),
531+ fmt.Sprintf("%s := &%s{}", child, childType),
532+ fmt.Sprintf("%s.SetContext(o.Context())", child),
533+ fmt.Sprintf("%s.SetID(%sID)", child, name),
534+ fmt.Sprintf("o.Context().Register(%s)", child),
535+ )
536+ params = append(params, child+" *"+childType)
537+ args = append(args, child)
538+
539+ case "object":
540+ // Object references are reported as raw object IDs: the generated
541+ // layer does not own a registry of foreign proxies.
542+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
543+ params = append(params, name+"ID uint32")
544+ args = append(args, name+"ID")
545+
546+ default:
547+ if arg.Type == "fd" {
548+ data.HasFD = true
549+ data.FDNames = append(data.FDNames, name)
550+ }
551+ goType, decoder, err := s.scalarDecoder(arg)
552+ if err != nil {
553+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
554+ }
555+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
556+ params = append(params, name+" "+goType)
557+ args = append(args, name)
558+ }
559+ }
560+
561+ data.HandlerParams = strings.Join(params, ", ")
562+ data.HandlerArgs = strings.Join(args, ", ")
563+ return data, nil
564+}
565+
566+// scalarDecoder maps a simple argument to its Go type and event decoder.
567+func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) {
568+ switch arg.Type {
569+ case "int":
570+ return "int32", "Int32", nil
571+ case "uint":
572+ return "uint32", "Uint32", nil
573+ case "fixed":
574+ return "wl.Fixed", "Fixed", nil
575+ case "string":
576+ return "string", "String", nil
577+ case "array":
578+ return "[]byte", "Array", nil
579+ case "fd":
580+ return "*wl.OwnedFD", "FD", nil
581+ }
582+ return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
583+}
584+
585+// goTypeForArg returns the Go type of an object-like argument. Protocol-local
586+// interfaces map to generated types, explicit external references to their
587+// mapped package, and bootstrap display/registry to transport wrappers.
588+func (s *Scanner) goTypeForArg(arg Arg) (string, error) {
589+ switch arg.Type {
590+ case "int":
591+ return "int32", nil
592+ case "uint":
593+ return "uint32", nil
594+ case "fixed":
595+ return "wl.Fixed", nil
596+ case "string":
597+ return "string", nil
598+ case "array":
599+ return "[]byte", nil
600+ case "fd":
601+ return "int", nil
602+ case "object", "new_id":
603+ return s.goTypeForInterface(arg.Interface), nil
604+ }
605+ return "", fmt.Errorf("unsupported argument type %q", arg.Type)
606+}
607+
608+func (s *Scanner) goTypeForInterface(iface string) string {
609+ if iface == "" {
610+ return "wl.BaseProxy"
611+ }
612+ // The XML package owns its own definitions even when the name starts wl_.
613+ for _, local := range s.protocol.Interfaces {
614+ if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
615+ return s.toGoName(iface)
616+ }
617+ }
618+ if bootstrapTypes[s.toGoName(iface)] {
619+ return "wl." + s.toGoName(iface)
620+ }
621+ if _, ok := s.CrossPackage[iface]; ok {
622+ return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface)
623+ }
624+ return "wl.BaseProxy"
625+}
626+
627+// paramName turns a protocol argument name into a Go parameter name.
628+func (s *Scanner) paramName(name string, index int) string {
629+ if name == "" {
630+ return fmt.Sprintf("arg%d", index)
631+ }
632+
633+ parts := strings.Split(name, "_")
634+ for i, part := range parts {
635+ if part == "" {
636+ continue
637+ }
638+ if i == 0 {
639+ parts[i] = strings.ToLower(part[:1]) + part[1:]
640+ continue
641+ }
642+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
643+ }
644+ result := strings.Join(parts, "")
645+ if result == "" {
646+ return fmt.Sprintf("arg%d", index)
647+ }
648+ if goKeywords[result] {
649+ return result + "Arg"
650+ }
651+ return result
652+}
653+
654+func (s *Scanner) toGoName(name string) string {
655+ name = strings.TrimPrefix(name, "zwlr_")
656+ name = strings.TrimPrefix(name, "zwp_")
657+ name = strings.TrimPrefix(name, "wl_")
658+ name = strings.TrimSuffix(name, "_v1")
659+ name = strings.TrimSuffix(name, "_v2")
660+
661+ parts := strings.Split(name, "_")
662+ for i, part := range parts {
663+ if part != "" {
664+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
665+ }
666+ }
667+ return strings.Join(parts, "")
668+}
669+
670+// constantName renders an enum entry as the unprefixed constant name.
671+func constantName(enum, entry string) string {
672+ return strings.Join(append(joinName(enum), joinName(entry)...), "_")
673+}
674+
675+// qualifiedConstantName prefixes an enum entry constant with its interface
676+// name. It is used for entries that several interfaces declare, which would
677+// otherwise collide in the generated package.
678+func qualifiedConstantName(iface, enum, entry string) string {
679+ return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
680+}
681+
682+// joinName upper-cases the underscore-separated parts of a protocol name.
683+func joinName(value string) []string {
684+ parts := strings.Split(value, "_")
685+ out := make([]string, 0, len(parts))
686+ for _, part := range parts {
687+ if part != "" {
688+ out = append(out, strings.ToUpper(part))
689+ }
690+ }
691+ return out
692+}
693+
694+// formatDescription renders an argument or request description as a single
695+// documentation line.
696+func (s *Scanner) formatDescription(desc *Description) string {
697+ if desc == nil {
698+ return ""
699+ }
700+
701+ text := strings.TrimSpace(desc.Text)
702+ if text == "" {
703+ return desc.Summary
704+ }
705+
706+ lines := strings.Split(text, "\n")
707+ formatted := make([]string, 0, len(lines))
708+ for _, line := range lines {
709+ if line = strings.TrimSpace(line); line != "" {
710+ formatted = append(formatted, line)
711+ }
712+ }
713+ return strings.Join(formatted, " ")
714+}
@@ -0,0 +1,714 @@
1+// Package scanner 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 scanner
10+
11+import (
12+ "bytes"
13+ "encoding/xml"
14+ "fmt"
15+ "go/format"
16+ "io"
17+ "os"
18+ "path/filepath"
19+ "sort"
20+ "strings"
21+ "text/template"
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+// Protocol represents a Wayland protocol specification
29+type Protocol struct {
30+ XMLName xml.Name `xml:"protocol"`
31+ Name string `xml:"name,attr"`
32+ Copyright string `xml:"copyright"`
33+ Description *Description `xml:"description"`
34+ Interfaces []Interface `xml:"interface"`
35+}
36+
37+// Interface represents a Wayland interface in the protocol
38+type Interface struct {
39+ Name string `xml:"name,attr"`
40+ Version int `xml:"version,attr"`
41+ Description *Description `xml:"description"`
42+ Requests []Request `xml:"request"`
43+ Events []Event `xml:"event"`
44+ Enums []Enum `xml:"enum"`
45+}
46+
47+// Request represents a client-to-server message
48+type Request struct {
49+ Name string `xml:"name,attr"`
50+ Type string `xml:"type,attr"` // "destructor" or empty
51+ Since int `xml:"since,attr"`
52+ Description *Description `xml:"description"`
53+ Args []Arg `xml:"arg"`
54+}
55+
56+// Event represents a server-to-client message
57+type Event struct {
58+ Type string `xml:"type,attr"`
59+ Name string `xml:"name,attr"`
60+ Since int `xml:"since,attr"`
61+ Description *Description `xml:"description"`
62+ Args []Arg `xml:"arg"`
63+}
64+
65+// Arg represents a message argument
66+type Arg struct {
67+ Name string `xml:"name,attr"`
68+ Type string `xml:"type,attr"`
69+ Summary string `xml:"summary,attr"`
70+ Interface string `xml:"interface,attr"`
71+ AllowNull bool `xml:"allow-null,attr"`
72+ Enum string `xml:"enum,attr"`
73+}
74+
75+// Enum represents an enumeration type
76+type Enum struct {
77+ Name string `xml:"name,attr"`
78+ Since int `xml:"since,attr"`
79+ Bitfield bool `xml:"bitfield,attr"`
80+ Description *Description `xml:"description"`
81+ Entries []Entry `xml:"entry"`
82+}
83+
84+// Entry represents an enum value
85+type Entry struct {
86+ Name string `xml:"name,attr"`
87+ Value string `xml:"value,attr"`
88+ Summary string `xml:"summary,attr"`
89+ Since int `xml:"since,attr"`
90+}
91+
92+// Description represents documentation
93+type Description struct {
94+ Summary string `xml:"summary,attr"`
95+ Text string `xml:",chardata"`
96+}
97+
98+// Scanner generates Go bindings from Wayland protocol XML
99+type Scanner struct {
100+ protocol *Protocol
101+ source string
102+ // CrossPackage maps interface names to Go package import paths. Local wins.
103+ CrossPackage map[string]string
104+}
105+
106+// NewScanner creates a new protocol scanner
107+func NewScanner() *Scanner {
108+ return &Scanner{}
109+}
110+
111+// ParseXML parses a Wayland protocol XML file
112+func (s *Scanner) ParseXML(path string) error {
113+ file, err := os.Open(path)
114+ if err != nil {
115+ return fmt.Errorf("failed to open XML file: %w", err)
116+ }
117+ defer func() { _ = file.Close() }()
118+
119+ data, err := io.ReadAll(file)
120+ if err != nil {
121+ return fmt.Errorf("failed to read XML file: %w", err)
122+ }
123+
124+ var protocol Protocol
125+ if err := xml.Unmarshal(data, &protocol); err != nil {
126+ return fmt.Errorf("failed to parse XML: %w", err)
127+ }
128+
129+ s.protocol = &protocol
130+ // Only the base name is recorded: generated output must not depend on the
131+ // path the XML happened to be read from.
132+ s.source = filepath.Base(path)
133+ return nil
134+}
135+
136+// Generate creates Go source code for the parsed protocol
137+func (s *Scanner) Generate(packageName string) ([]byte, error) {
138+ if s.protocol == nil {
139+ return nil, fmt.Errorf("no protocol parsed")
140+ }
141+
142+ data, err := s.prepareTemplateData(packageName)
143+ if err != nil {
144+ return nil, err
145+ }
146+
147+ var buf bytes.Buffer
148+ tmpl := template.Must(template.New("protocol").Parse(protocolTemplate))
149+ if err := tmpl.Execute(&buf, data); err != nil {
150+ return nil, fmt.Errorf("failed to execute template: %w", err)
151+ }
152+
153+ formatted, err := format.Source(buf.Bytes())
154+ if err != nil {
155+ return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err)
156+ }
157+ return formatted, nil
158+}
159+
160+// templateData holds data for template generation
161+type templateData struct {
162+ Package string
163+ Protocol string
164+ Source string
165+ ImportBlock string
166+ Constants []constantData
167+ Interfaces []interfaceData
168+}
169+
170+type interfaceData struct {
171+ Name string
172+ GoName string
173+ Version int
174+ HasEvents bool
175+ SignatureCases []signatureCase
176+ Requests []requestData
177+ Events []eventData
178+}
179+
180+type signatureCase struct {
181+ Opcode int
182+ Signature string
183+}
184+type requestData struct {
185+ Name string
186+ GoName string
187+ Doc string
188+ Opcode int
189+ Destructor bool
190+ Params string
191+ Results string
192+ ArgPreparations []string
193+ ArgExprs []string
194+ CreatesChild bool
195+ ChildVar string
196+ ChildType string
197+ ErrorReturn string
198+ FDArgs []string
199+ HasFDs bool
200+ SendCall string
201+}
202+
203+// argSlot is one positional argument of a request, in declaration order. A
204+// new_id slot is filled in once the child object has been created.
205+type argSlot struct {
206+ newID bool
207+ expr string
208+}
209+
210+type eventData struct {
211+ HasFD bool
212+ ServerDestroy bool
213+ FDNames []string
214+ Name string
215+ GoName string
216+ Opcode int
217+ HandlerField string
218+ HandlerAccessor string
219+ HandlerParams string
220+ HandlerArgs string
221+ DecodeLines []string
222+}
223+
224+type constantData struct {
225+ Name string
226+ Value string
227+ Type string
228+ Enum string
229+ Entry string
230+}
231+
232+// Only display and registry have handwritten bootstrap bindings.
233+var bootstrapTypes = map[string]bool{
234+ "Display": true,
235+ "Registry": true,
236+}
237+
238+// goKeywords are avoided as parameter names because generated identifiers
239+// must not collide with the language.
240+var goKeywords = map[string]bool{
241+ "break": true, "case": true, "chan": true, "const": true, "continue": true,
242+ "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
243+ "func": true, "go": true, "goto": true, "if": true, "import": true,
244+ "interface": true, "map": true, "package": true, "range": true, "return": true,
245+ "select": true, "struct": true, "switch": true, "type": true, "var": true,
246+}
247+
248+func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
249+ data := templateData{
250+ Package: packageName,
251+ Protocol: s.protocol.Name,
252+ Source: s.source,
253+ }
254+
255+ imports := []string{TransportImport}
256+ usedCross := make(map[string]bool)
257+ for _, candidate := range s.protocol.Interfaces {
258+ for _, req := range candidate.Requests {
259+ for _, arg := range req.Args {
260+ if arg.Type == "object" || arg.Type == "new_id" {
261+ usedCross[arg.Interface] = true
262+ }
263+ }
264+ }
265+ for _, ev := range candidate.Events {
266+ for _, arg := range ev.Args {
267+ if arg.Type == "object" || arg.Type == "new_id" {
268+ usedCross[arg.Interface] = true
269+ }
270+ }
271+ }
272+ }
273+ for iface, path := range s.CrossPackage {
274+ local := false
275+ for _, candidate := range s.protocol.Interfaces {
276+ if candidate.Name == iface {
277+ local = true
278+ break
279+ }
280+ }
281+ if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
282+ continue
283+ }
284+ alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
285+ imports = append(imports, alias+" "+path)
286+ }
287+ sort.Strings(imports)
288+ stdImports := []string{}
289+ needsSync := false
290+
291+ // Enum constants are package-level declarations, so two interfaces that
292+ // declare the same enum entry (for example xdg_surface.error.invalid_size
293+ // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
294+ // occurs in more than one interface are qualified with their interface
295+ // name below; unique names keep their historical, unprefixed form.
296+ enumEntryCounts := map[string]int{}
297+ for _, iface := range s.protocol.Interfaces {
298+ for _, enum := range iface.Enums {
299+ for _, entry := range enum.Entries {
300+ enumEntryCounts[constantName(enum.Name, entry.Name)]++
301+ }
302+ }
303+ }
304+
305+ for _, iface := range s.protocol.Interfaces {
306+ if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
307+ continue
308+ }
309+ ifaceData, err := s.processInterface(iface)
310+ if err != nil {
311+ return data, err
312+ }
313+ data.Interfaces = append(data.Interfaces, ifaceData)
314+ if len(iface.Events) > 0 {
315+ needsSync = true
316+ }
317+
318+ for _, enum := range iface.Enums {
319+ for _, entry := range enum.Entries {
320+ name := constantName(enum.Name, entry.Name)
321+ if enumEntryCounts[name] > 1 {
322+ name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
323+ }
324+ // Enum values are emitted untyped: the same enum travels as a uint
325+ // in one protocol and as an int in another, and an untyped constant
326+ // assigns to either without casts at the call site.
327+ data.Constants = append(data.Constants, constantData{
328+ Name: name,
329+ Value: entry.Value,
330+ Enum: enum.Name,
331+ Entry: entry.Name,
332+ })
333+ }
334+ }
335+ }
336+
337+ // Handlers are registered and dispatched from different goroutines, so an
338+ // interface with events needs a mutex.
339+ if needsSync {
340+ stdImports = append(stdImports, "sync")
341+ }
342+
343+ var block strings.Builder
344+ block.WriteString("import (\n")
345+ for _, imp := range stdImports {
346+ fmt.Fprintf(&block, "\t%q\n", imp)
347+ }
348+ if len(stdImports) > 0 && len(imports) > 0 {
349+ block.WriteString("\n")
350+ }
351+ for _, imp := range imports {
352+ if strings.Contains(imp, " ") {
353+ parts := strings.SplitN(imp, " ", 2)
354+ fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
355+ } else {
356+ fmt.Fprintf(&block, "\t%q\n", imp)
357+ }
358+ }
359+ block.WriteString(")")
360+ data.ImportBlock = block.String()
361+
362+ return data, nil
363+}
364+
365+func (s *Scanner) processInterface(iface Interface) (interfaceData, error) {
366+ data := interfaceData{
367+ Name: iface.Name,
368+ GoName: s.toGoName(iface.Name),
369+ Version: iface.Version,
370+ HasEvents: len(iface.Events) > 0,
371+ }
372+
373+ for i, req := range iface.Requests {
374+ request, err := s.processRequest(iface, req, i)
375+ if err != nil {
376+ return data, err
377+ }
378+ data.Requests = append(data.Requests, request)
379+ }
380+
381+ for i, event := range iface.Events {
382+ var sig strings.Builder
383+ for _, arg := range event.Args {
384+ sig.WriteString(arg.Type)
385+ sig.WriteByte(',')
386+ }
387+ data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
388+ eventData, err := s.processEvent(event, i)
389+ if iface.Name == "wl_callback" && event.Name == "done" {
390+ eventData.ServerDestroy = true
391+ }
392+ if err != nil {
393+ return data, err
394+ }
395+ data.Events = append(data.Events, eventData)
396+ }
397+
398+ return data, nil
399+}
400+
401+func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) {
402+ data := requestData{
403+ Name: req.Name,
404+ GoName: s.toGoName(req.Name),
405+ Doc: s.formatDescription(req.Description),
406+ Opcode: opcode,
407+ Destructor: req.Type == "destructor",
408+ ChildVar: "child",
409+ }
410+
411+ var (
412+ params []string
413+ argSlots []argSlot
414+ argExprs []string
415+ fdExprs []string
416+ newIDArg *Arg
417+ )
418+
419+ for i, arg := range req.Args {
420+ if arg.Type == "new_id" {
421+ if newIDArg != nil {
422+ return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
423+ }
424+ newIDArg = &arg
425+ // The child object is created later, but it keeps its declared
426+ // position: the wire order is the argument order.
427+ argSlots = append(argSlots, argSlot{newID: true})
428+ continue
429+ }
430+
431+ name := s.paramName(arg.Name, i)
432+ goType, err := s.goTypeForArg(arg)
433+ if err != nil {
434+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
435+ }
436+
437+ expr := name
438+ switch arg.Type {
439+ case "object":
440+ // Objects are pointer types, and travel as wl.Object so a nil
441+ // pointer is never sent as a non-nil interface holding a typed nil.
442+ params = append(params, name+" *"+goType)
443+ holder := fmt.Sprintf("arg%d", i)
444+ data.ArgPreparations = append(data.ArgPreparations,
445+ fmt.Sprintf("var %s wl.Object", holder),
446+ fmt.Sprintf("if %s != nil {", name),
447+ fmt.Sprintf("\t%s = %s", holder, name),
448+ "}")
449+ expr = holder
450+
451+ case "fd":
452+ // Descriptors travel out of band: the body carries no value for
453+ // them, and the descriptor list is attached to the request.
454+ params = append(params, name+" int")
455+ fdExprs = append(fdExprs, name)
456+ data.FDArgs = append(data.FDArgs, name)
457+ expr = "uintptr(" + name + ")"
458+
459+ default:
460+ params = append(params, name+" "+goType)
461+ }
462+
463+ argSlots = append(argSlots, argSlot{expr: expr})
464+ }
465+
466+ if newIDArg != nil {
467+ childType, err := s.goTypeForArg(*newIDArg)
468+ if err != nil {
469+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
470+ }
471+ data.CreatesChild = true
472+ data.ChildType = childType
473+ }
474+
475+ for _, slot := range argSlots {
476+ if slot.newID {
477+ argExprs = append(argExprs, data.ChildVar)
478+ continue
479+ }
480+ argExprs = append(argExprs, slot.expr)
481+ }
482+
483+ data.Params = strings.Join(params, ", ")
484+ data.ArgExprs = argExprs
485+ args := ""
486+ if len(argExprs) > 0 {
487+ args = ", " + strings.Join(argExprs, ", ")
488+ }
489+ if len(fdExprs) > 0 {
490+ data.HasFDs = true
491+ data.SendCall = fmt.Sprintf("SendRequestWithFDs(o, %d, []int{%s}%s)", opcode, strings.Join(fdExprs, ", "), args)
492+ } else {
493+ data.SendCall = fmt.Sprintf("SendRequest(o, %d%s)", opcode, args)
494+ }
495+ if data.CreatesChild {
496+ data.Results = "(*" + data.ChildType + ", error)"
497+ data.ErrorReturn = "nil, err"
498+ } else {
499+ data.Results = "error"
500+ data.ErrorReturn = "err"
501+ }
502+
503+ return data, nil
504+}
505+
506+func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) {
507+ goName := s.toGoName(event.Name)
508+ data := eventData{
509+ ServerDestroy: event.Type == "destructor",
510+ Name: event.Name,
511+ GoName: goName,
512+ Opcode: opcode,
513+ HandlerField: "on" + goName,
514+ HandlerAccessor: "handlersFor" + goName,
515+ }
516+
517+ var params, args []string
518+
519+ for i, arg := range event.Args {
520+ name := s.paramName(arg.Name, i)
521+
522+ switch arg.Type {
523+ case "new_id":
524+ childType, err := s.goTypeForArg(arg)
525+ if err != nil {
526+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
527+ }
528+ child := name + "Object"
529+ data.DecodeLines = append(data.DecodeLines,
530+ fmt.Sprintf("%sID := event.Uint32()", name),
531+ fmt.Sprintf("%s := &%s{}", child, childType),
532+ fmt.Sprintf("%s.SetContext(o.Context())", child),
533+ fmt.Sprintf("%s.SetID(%sID)", child, name),
534+ fmt.Sprintf("o.Context().Register(%s)", child),
535+ )
536+ params = append(params, child+" *"+childType)
537+ args = append(args, child)
538+
539+ case "object":
540+ // Object references are reported as raw object IDs: the generated
541+ // layer does not own a registry of foreign proxies.
542+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
543+ params = append(params, name+"ID uint32")
544+ args = append(args, name+"ID")
545+
546+ default:
547+ if arg.Type == "fd" {
548+ data.HasFD = true
549+ data.FDNames = append(data.FDNames, name)
550+ }
551+ goType, decoder, err := s.scalarDecoder(arg)
552+ if err != nil {
553+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
554+ }
555+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
556+ params = append(params, name+" "+goType)
557+ args = append(args, name)
558+ }
559+ }
560+
561+ data.HandlerParams = strings.Join(params, ", ")
562+ data.HandlerArgs = strings.Join(args, ", ")
563+ return data, nil
564+}
565+
566+// scalarDecoder maps a simple argument to its Go type and event decoder.
567+func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) {
568+ switch arg.Type {
569+ case "int":
570+ return "int32", "Int32", nil
571+ case "uint":
572+ return "uint32", "Uint32", nil
573+ case "fixed":
574+ return "wl.Fixed", "Fixed", nil
575+ case "string":
576+ return "string", "String", nil
577+ case "array":
578+ return "[]byte", "Array", nil
579+ case "fd":
580+ return "*wl.OwnedFD", "FD", nil
581+ }
582+ return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
583+}
584+
585+// goTypeForArg returns the Go type of an object-like argument. Protocol-local
586+// interfaces map to generated types, explicit external references to their
587+// mapped package, and bootstrap display/registry to transport wrappers.
588+func (s *Scanner) goTypeForArg(arg Arg) (string, error) {
589+ switch arg.Type {
590+ case "int":
591+ return "int32", nil
592+ case "uint":
593+ return "uint32", nil
594+ case "fixed":
595+ return "wl.Fixed", nil
596+ case "string":
597+ return "string", nil
598+ case "array":
599+ return "[]byte", nil
600+ case "fd":
601+ return "int", nil
602+ case "object", "new_id":
603+ return s.goTypeForInterface(arg.Interface), nil
604+ }
605+ return "", fmt.Errorf("unsupported argument type %q", arg.Type)
606+}
607+
608+func (s *Scanner) goTypeForInterface(iface string) string {
609+ if iface == "" {
610+ return "wl.BaseProxy"
611+ }
612+ // The XML package owns its own definitions even when the name starts wl_.
613+ for _, local := range s.protocol.Interfaces {
614+ if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
615+ return s.toGoName(iface)
616+ }
617+ }
618+ if bootstrapTypes[s.toGoName(iface)] {
619+ return "wl." + s.toGoName(iface)
620+ }
621+ if _, ok := s.CrossPackage[iface]; ok {
622+ return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface)
623+ }
624+ return "wl.BaseProxy"
625+}
626+
627+// paramName turns a protocol argument name into a Go parameter name.
628+func (s *Scanner) paramName(name string, index int) string {
629+ if name == "" {
630+ return fmt.Sprintf("arg%d", index)
631+ }
632+
633+ parts := strings.Split(name, "_")
634+ for i, part := range parts {
635+ if part == "" {
636+ continue
637+ }
638+ if i == 0 {
639+ parts[i] = strings.ToLower(part[:1]) + part[1:]
640+ continue
641+ }
642+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
643+ }
644+ result := strings.Join(parts, "")
645+ if result == "" {
646+ return fmt.Sprintf("arg%d", index)
647+ }
648+ if goKeywords[result] {
649+ return result + "Arg"
650+ }
651+ return result
652+}
653+
654+func (s *Scanner) toGoName(name string) string {
655+ name = strings.TrimPrefix(name, "zwlr_")
656+ name = strings.TrimPrefix(name, "zwp_")
657+ name = strings.TrimPrefix(name, "wl_")
658+ name = strings.TrimSuffix(name, "_v1")
659+ name = strings.TrimSuffix(name, "_v2")
660+
661+ parts := strings.Split(name, "_")
662+ for i, part := range parts {
663+ if part != "" {
664+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
665+ }
666+ }
667+ return strings.Join(parts, "")
668+}
669+
670+// constantName renders an enum entry as the unprefixed constant name.
671+func constantName(enum, entry string) string {
672+ return strings.Join(append(joinName(enum), joinName(entry)...), "_")
673+}
674+
675+// qualifiedConstantName prefixes an enum entry constant with its interface
676+// name. It is used for entries that several interfaces declare, which would
677+// otherwise collide in the generated package.
678+func qualifiedConstantName(iface, enum, entry string) string {
679+ return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
680+}
681+
682+// joinName upper-cases the underscore-separated parts of a protocol name.
683+func joinName(value string) []string {
684+ parts := strings.Split(value, "_")
685+ out := make([]string, 0, len(parts))
686+ for _, part := range parts {
687+ if part != "" {
688+ out = append(out, strings.ToUpper(part))
689+ }
690+ }
691+ return out
692+}
693+
694+// formatDescription renders an argument or request description as a single
695+// documentation line.
696+func (s *Scanner) formatDescription(desc *Description) string {
697+ if desc == nil {
698+ return ""
699+ }
700+
701+ text := strings.TrimSpace(desc.Text)
702+ if text == "" {
703+ return desc.Summary
704+ }
705+
706+ lines := strings.Split(text, "\n")
707+ formatted := make([]string, 0, len(lines))
708+ for _, line := range lines {
709+ if line = strings.TrimSpace(line); line != "" {
710+ formatted = append(formatted, line)
711+ }
712+ }
713+ return strings.Join(formatted, " ")
714+}
A internal/scanner/scanner_test.go +47 -0
@@ -0,0 +1,47 @@
1+package scanner
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "strings"
7+ "testing"
8+)
9+
10+func TestResolution(t *testing.T) {
11+ p := filepath.Join(t.TempDir(), "fixture.xml")
12+ 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>`
13+ if err := os.WriteFile(p, []byte(xml), 0600); err != nil {
14+ t.Fatal(err)
15+ }
16+ s := NewScanner()
17+ if err := s.ParseXML(p); err != nil {
18+ t.Fatal(err)
19+ }
20+ s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/wlturbo/protocol/core", "wl_output": "github.com/bnema/wlturbo/protocol/core"}
21+ b, err := s.Generate("fixture")
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ text := string(b)
26+ if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") {
27+ t.Fatalf("wrong resolution: %s", text)
28+ }
29+}
30+func TestCoreGeneration(t *testing.T) {
31+ s := NewScanner()
32+ if err := s.ParseXML("../../protocol/wayland.xml"); err != nil {
33+ t.Fatal(err)
34+ }
35+ b, err := s.Generate("core")
36+ if err != nil {
37+ t.Fatal(err)
38+ }
39+ for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} {
40+ if !strings.Contains(string(b), want) {
41+ t.Errorf("missing %s", want)
42+ }
43+ }
44+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") {
45+ t.Fatal("bootstrap generated twice")
46+ }
47+}
@@ -0,0 +1,47 @@
1+package scanner
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "strings"
7+ "testing"
8+)
9+
10+func TestResolution(t *testing.T) {
11+ p := filepath.Join(t.TempDir(), "fixture.xml")
12+ 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>`
13+ if err := os.WriteFile(p, []byte(xml), 0600); err != nil {
14+ t.Fatal(err)
15+ }
16+ s := NewScanner()
17+ if err := s.ParseXML(p); err != nil {
18+ t.Fatal(err)
19+ }
20+ s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/wlturbo/protocol/core", "wl_output": "github.com/bnema/wlturbo/protocol/core"}
21+ b, err := s.Generate("fixture")
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ text := string(b)
26+ if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") {
27+ t.Fatalf("wrong resolution: %s", text)
28+ }
29+}
30+func TestCoreGeneration(t *testing.T) {
31+ s := NewScanner()
32+ if err := s.ParseXML("../../protocol/wayland.xml"); err != nil {
33+ t.Fatal(err)
34+ }
35+ b, err := s.Generate("core")
36+ if err != nil {
37+ t.Fatal(err)
38+ }
39+ for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} {
40+ if !strings.Contains(string(b), want) {
41+ t.Errorf("missing %s", want)
42+ }
43+ }
44+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") {
45+ t.Fatal("bootstrap generated twice")
46+ }
47+}
A internal/scanner/template.go +139 -0
@@ -0,0 +1,139 @@
1+package scanner
2+
3+// protocolTemplate is the template for generating WLTurbo-backed Go bindings.
4+//
5+// Generated objects embed wl.BaseProxy, allocate and register child objects
6+// through the wl.Context, send requests through that context, and dispatch
7+// typed events to registered handlers. Nothing in the generated code opens a
8+// socket or frames messages itself: that is the transport's job.
9+const protocolTemplate = `// Code generated by wlturbo-scanner. DO NOT EDIT.
10+// Protocol: {{.Protocol}}
11+// Source: {{.Source}}
12+
13+package {{.Package}}
14+
15+{{.ImportBlock}}
16+
17+{{range .Constants}}
18+// {{.Name}} is the {{.Enum}} {{.Entry}} value.
19+const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}}
20+{{- end}}
21+{{range .Interfaces}}
22+{{- $iface := .}}
23+
24+// {{$iface.GoName}}Interface is the protocol interface name.
25+const {{$iface.GoName}}Interface = "{{$iface.Name}}"
26+
27+// {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}.
28+type {{$iface.GoName}} struct {
29+ wl.BaseProxy
30+{{- if $iface.HasEvents}}
31+ // handlersMu guards the handler slices: a client may register or replace
32+ // handlers while another goroutine dispatches events.
33+ handlersMu sync.Mutex
34+{{- end}}
35+{{- range $iface.Events}}
36+ {{.HandlerField}} []func({{.HandlerParams}})
37+{{- end}}
38+}
39+
40+// New{{$iface.GoName}} returns an unbound {{$iface.GoName}}.
41+func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} {
42+ obj := &{{$iface.GoName}}{}
43+ obj.SetContext(ctx)
44+ ctx.Register(obj)
45+ return obj
46+}
47+{{- range $iface.Requests}}
48+
49+{{- if .Doc}}
50+// {{.Doc}}
51+{{- end}}
52+func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} {
53+{{- range .ArgPreparations}}
54+ {{.}}
55+{{- end}}
56+{{- if .CreatesChild}}
57+ {{.ChildVar}} := &{{.ChildType}}{}
58+ {{.ChildVar}}.SetContext(o.Context())
59+ {{.ChildVar}}.SetID(o.Context().AllocateID())
60+ o.Context().Register({{.ChildVar}})
61+{{- end}}
62+ if err := o.Context().{{.SendCall}}; err != nil {
63+{{- if .CreatesChild}}
64+ o.Context().Unregister({{.ChildVar}})
65+{{- end}}
66+ return {{.ErrorReturn}}
67+ }
68+{{- range .FDArgs}}
69+ _ = wl.CloseSentFD({{.}})
70+{{- end}}
71+{{- if .Destructor}}
72+ o.Context().Unregister(o)
73+{{- end}}
74+{{- if .CreatesChild}}
75+ return {{.ChildVar}}, nil
76+{{- else}}
77+ return nil
78+{{- end}}
79+}
80+{{- end}}
81+{{- range $iface.Events}}
82+
83+// On{{.GoName}} registers a handler for the {{.Name}} event.
84+//
85+// Handlers are appended, so a second call adds another handler rather than
86+// replacing the first. Registration is safe while another goroutine dispatches.
87+func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) {
88+ if handler == nil { return }
89+ o.handlersMu.Lock()
90+ defer o.handlersMu.Unlock()
91+ o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler)
92+}
93+
94+// {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken
95+// under the lock so a handler may register another handler while events are
96+// being dispatched.
97+func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) {
98+ o.handlersMu.Lock()
99+ defer o.handlersMu.Unlock()
100+ return o.{{.HandlerField}}
101+}
102+{{- end}}
103+
104+// EventSignature returns the declared event argument types in wire order.
105+func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) {
106+ switch opcode {
107+ {{- range $iface.SignatureCases}}
108+ case {{.Opcode}}: return "{{.Signature}}", true
109+ {{- end}}
110+ default: return "", false
111+ }
112+}
113+
114+// Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers.
115+func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
116+ switch event.Opcode {
117+{{- range $iface.Events}}
118+ case {{.Opcode}}: // {{.Name}}
119+{{- range .DecodeLines}}
120+ {{.}}
121+{{- end}}
122+ {{if .HasFD}}defer func(){ {{range .FDNames}}_ = {{.}}.Close(); {{end}} }(){{end}}
123+ for i, handler := range o.{{.HandlerAccessor}}() {
124+ {{- if .HasFD}}
125+ if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
126+ {{- else}}
127+ _ = i
128+ {{- end}}
129+ handler({{.HandlerArgs}})
130+ }
131+{{- if .ServerDestroy}}
132+ o.Context().Unregister(o)
133+{{- end}}
134+{{- end}}
135+ default:
136+ }
137+}
138+{{end}}
139+`
@@ -0,0 +1,139 @@
1+package scanner
2+
3+// protocolTemplate is the template for generating WLTurbo-backed Go bindings.
4+//
5+// Generated objects embed wl.BaseProxy, allocate and register child objects
6+// through the wl.Context, send requests through that context, and dispatch
7+// typed events to registered handlers. Nothing in the generated code opens a
8+// socket or frames messages itself: that is the transport's job.
9+const protocolTemplate = `// Code generated by wlturbo-scanner. DO NOT EDIT.
10+// Protocol: {{.Protocol}}
11+// Source: {{.Source}}
12+
13+package {{.Package}}
14+
15+{{.ImportBlock}}
16+
17+{{range .Constants}}
18+// {{.Name}} is the {{.Enum}} {{.Entry}} value.
19+const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}}
20+{{- end}}
21+{{range .Interfaces}}
22+{{- $iface := .}}
23+
24+// {{$iface.GoName}}Interface is the protocol interface name.
25+const {{$iface.GoName}}Interface = "{{$iface.Name}}"
26+
27+// {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}.
28+type {{$iface.GoName}} struct {
29+ wl.BaseProxy
30+{{- if $iface.HasEvents}}
31+ // handlersMu guards the handler slices: a client may register or replace
32+ // handlers while another goroutine dispatches events.
33+ handlersMu sync.Mutex
34+{{- end}}
35+{{- range $iface.Events}}
36+ {{.HandlerField}} []func({{.HandlerParams}})
37+{{- end}}
38+}
39+
40+// New{{$iface.GoName}} returns an unbound {{$iface.GoName}}.
41+func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} {
42+ obj := &{{$iface.GoName}}{}
43+ obj.SetContext(ctx)
44+ ctx.Register(obj)
45+ return obj
46+}
47+{{- range $iface.Requests}}
48+
49+{{- if .Doc}}
50+// {{.Doc}}
51+{{- end}}
52+func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} {
53+{{- range .ArgPreparations}}
54+ {{.}}
55+{{- end}}
56+{{- if .CreatesChild}}
57+ {{.ChildVar}} := &{{.ChildType}}{}
58+ {{.ChildVar}}.SetContext(o.Context())
59+ {{.ChildVar}}.SetID(o.Context().AllocateID())
60+ o.Context().Register({{.ChildVar}})
61+{{- end}}
62+ if err := o.Context().{{.SendCall}}; err != nil {
63+{{- if .CreatesChild}}
64+ o.Context().Unregister({{.ChildVar}})
65+{{- end}}
66+ return {{.ErrorReturn}}
67+ }
68+{{- range .FDArgs}}
69+ _ = wl.CloseSentFD({{.}})
70+{{- end}}
71+{{- if .Destructor}}
72+ o.Context().Unregister(o)
73+{{- end}}
74+{{- if .CreatesChild}}
75+ return {{.ChildVar}}, nil
76+{{- else}}
77+ return nil
78+{{- end}}
79+}
80+{{- end}}
81+{{- range $iface.Events}}
82+
83+// On{{.GoName}} registers a handler for the {{.Name}} event.
84+//
85+// Handlers are appended, so a second call adds another handler rather than
86+// replacing the first. Registration is safe while another goroutine dispatches.
87+func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) {
88+ if handler == nil { return }
89+ o.handlersMu.Lock()
90+ defer o.handlersMu.Unlock()
91+ o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler)
92+}
93+
94+// {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken
95+// under the lock so a handler may register another handler while events are
96+// being dispatched.
97+func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) {
98+ o.handlersMu.Lock()
99+ defer o.handlersMu.Unlock()
100+ return o.{{.HandlerField}}
101+}
102+{{- end}}
103+
104+// EventSignature returns the declared event argument types in wire order.
105+func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) {
106+ switch opcode {
107+ {{- range $iface.SignatureCases}}
108+ case {{.Opcode}}: return "{{.Signature}}", true
109+ {{- end}}
110+ default: return "", false
111+ }
112+}
113+
114+// Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers.
115+func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
116+ switch event.Opcode {
117+{{- range $iface.Events}}
118+ case {{.Opcode}}: // {{.Name}}
119+{{- range .DecodeLines}}
120+ {{.}}
121+{{- end}}
122+ {{if .HasFD}}defer func(){ {{range .FDNames}}_ = {{.}}.Close(); {{end}} }(){{end}}
123+ for i, handler := range o.{{.HandlerAccessor}}() {
124+ {{- if .HasFD}}
125+ if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
126+ {{- else}}
127+ _ = i
128+ {{- end}}
129+ handler({{.HandlerArgs}})
130+ }
131+{{- if .ServerDestroy}}
132+ o.Context().Unregister(o)
133+{{- end}}
134+{{- end}}
135+ default:
136+ }
137+}
138+{{end}}
139+`
A internal/scanner/testdata/protocol_fixture.xml +112 -0
@@ -0,0 +1,112 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="wlvision_fixture">
3+ <copyright>
4+ Synthetic protocol used to test the wlvision protocol generator. It exists
5+ only in this repository's tests.
6+ </copyright>
7+
8+ <interface name="zwlr_fixture_manager_v1" version="2">
9+ <description summary="fixture manager">
10+ Exercises new_id requests, object references, fixed point numbers,
11+ strings, arrays, file descriptors and destructors.
12+ </description>
13+
14+ <request name="create_thing">
15+ <description summary="create a fixture object"/>
16+ <arg name="seat" type="object" interface="wl_seat"/>
17+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
18+ </request>
19+
20+ <request name="set_ratio">
21+ <arg name="ratio" type="fixed"/>
22+ </request>
23+
24+ <request name="set_label">
25+ <arg name="label" type="string"/>
26+ </request>
27+
28+ <request name="set_region">
29+ <arg name="region" type="object" interface="wl_region"/>
30+ </request>
31+
32+ <request name="set_buffer">
33+ <description summary="reference an interface without a typed wrapper"/>
34+ <arg name="buffer" type="object" interface="wl_buffer"/>
35+ </request>
36+
37+ <request name="set_optional_output">
38+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
39+ </request>
40+
41+ <request name="attach_raw">
42+ <arg name="data" type="array"/>
43+ </request>
44+
45+ <request name="pass_fd">
46+ <arg name="fd" type="fd"/>
47+ </request>
48+
49+ <request name="destroy" type="destructor">
50+ <description summary="destroy the manager"/>
51+ </request>
52+
53+ <request name="create_labeled_thing">
54+ <description summary="new_id argument that is not the last one"/>
55+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
56+ <arg name="label" type="string"/>
57+ <arg name="ratio" type="fixed"/>
58+ </request>
59+ </interface>
60+
61+ <interface name="zwlr_fixture_thing_v1" version="1">
62+ <description summary="fixture object"/>
63+
64+ <enum name="fixture_state">
65+ <entry name="idle" value="0"/>
66+ <entry name="busy" value="1"/>
67+ </enum>
68+
69+ <request name="set_scale">
70+ <arg name="scale" type="fixed"/>
71+ </request>
72+
73+ <request name="destroy" type="destructor"/>
74+
75+ <event name="ready">
76+ <arg name="serial" type="uint"/>
77+ </event>
78+
79+ <event name="label_changed">
80+ <arg name="label" type="string"/>
81+ </event>
82+
83+ <event name="geometry">
84+ <arg name="x" type="int"/>
85+ <arg name="y" type="int"/>
86+ <arg name="width" type="uint"/>
87+ <arg name="height" type="uint"/>
88+ </event>
89+
90+ <event name="mode">
91+ <arg name="mode" type="new_id" interface="zwlr_fixture_mode_v1"/>
92+ </event>
93+
94+ <event name="peer">
95+ <arg name="surface" type="object" interface="wl_surface"/>
96+ </event>
97+
98+ <event name="scaled">
99+ <arg name="scale" type="fixed"/>
100+ </event>
101+
102+ <event name="blob">
103+ <arg name="data" type="array"/>
104+ </event>
105+ </interface>
106+
107+ <interface name="zwlr_fixture_mode_v1" version="1">
108+ <description summary="fixture mode object"/>
109+
110+ <request name="destroy" type="destructor"/>
111+ </interface>
112+</protocol>
@@ -0,0 +1,112 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="wlvision_fixture">
3+ <copyright>
4+ Synthetic protocol used to test the wlvision protocol generator. It exists
5+ only in this repository's tests.
6+ </copyright>
7+
8+ <interface name="zwlr_fixture_manager_v1" version="2">
9+ <description summary="fixture manager">
10+ Exercises new_id requests, object references, fixed point numbers,
11+ strings, arrays, file descriptors and destructors.
12+ </description>
13+
14+ <request name="create_thing">
15+ <description summary="create a fixture object"/>
16+ <arg name="seat" type="object" interface="wl_seat"/>
17+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
18+ </request>
19+
20+ <request name="set_ratio">
21+ <arg name="ratio" type="fixed"/>
22+ </request>
23+
24+ <request name="set_label">
25+ <arg name="label" type="string"/>
26+ </request>
27+
28+ <request name="set_region">
29+ <arg name="region" type="object" interface="wl_region"/>
30+ </request>
31+
32+ <request name="set_buffer">
33+ <description summary="reference an interface without a typed wrapper"/>
34+ <arg name="buffer" type="object" interface="wl_buffer"/>
35+ </request>
36+
37+ <request name="set_optional_output">
38+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
39+ </request>
40+
41+ <request name="attach_raw">
42+ <arg name="data" type="array"/>
43+ </request>
44+
45+ <request name="pass_fd">
46+ <arg name="fd" type="fd"/>
47+ </request>
48+
49+ <request name="destroy" type="destructor">
50+ <description summary="destroy the manager"/>
51+ </request>
52+
53+ <request name="create_labeled_thing">
54+ <description summary="new_id argument that is not the last one"/>
55+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
56+ <arg name="label" type="string"/>
57+ <arg name="ratio" type="fixed"/>
58+ </request>
59+ </interface>
60+
61+ <interface name="zwlr_fixture_thing_v1" version="1">
62+ <description summary="fixture object"/>
63+
64+ <enum name="fixture_state">
65+ <entry name="idle" value="0"/>
66+ <entry name="busy" value="1"/>
67+ </enum>
68+
69+ <request name="set_scale">
70+ <arg name="scale" type="fixed"/>
71+ </request>
72+
73+ <request name="destroy" type="destructor"/>
74+
75+ <event name="ready">
76+ <arg name="serial" type="uint"/>
77+ </event>
78+
79+ <event name="label_changed">
80+ <arg name="label" type="string"/>
81+ </event>
82+
83+ <event name="geometry">
84+ <arg name="x" type="int"/>
85+ <arg name="y" type="int"/>
86+ <arg name="width" type="uint"/>
87+ <arg name="height" type="uint"/>
88+ </event>
89+
90+ <event name="mode">
91+ <arg name="mode" type="new_id" interface="zwlr_fixture_mode_v1"/>
92+ </event>
93+
94+ <event name="peer">
95+ <arg name="surface" type="object" interface="wl_surface"/>
96+ </event>
97+
98+ <event name="scaled">
99+ <arg name="scale" type="fixed"/>
100+ </event>
101+
102+ <event name="blob">
103+ <arg name="data" type="array"/>
104+ </event>
105+ </interface>
106+
107+ <interface name="zwlr_fixture_mode_v1" version="1">
108+ <description summary="fixture mode object"/>
109+
110+ <request name="destroy" type="destructor"/>
111+ </interface>
112+</protocol>
A protocol/SOURCE.md +7 -0
@@ -0,0 +1,7 @@
1+# Core protocol source
2+
3+`wayland.xml` is from Wayland 1.26.0, upstream GitLab
4+`https://gitlab.freedesktop.org/wayland/wayland`, tag `1.26.0`, peeled commit
5+`87cc8a8728a923fc57938faa81ba0e74f34ecdc7`.
6+SHA-256: `cc860987e54f8d85c940e97fa1270c69b6e4ad31fbcf5a7f00107ce1157f5e07`.
7+License: MIT (full copyright and grant in `wayland.xml` `<copyright>`).
@@ -0,0 +1,7 @@
1+# Core protocol source
2+
3+`wayland.xml` is from Wayland 1.26.0, upstream GitLab
4+`https://gitlab.freedesktop.org/wayland/wayland`, tag `1.26.0`, peeled commit
5+`87cc8a8728a923fc57938faa81ba0e74f34ecdc7`.
6+SHA-256: `cc860987e54f8d85c940e97fa1270c69b6e4ad31fbcf5a7f00107ce1157f5e07`.
7+License: MIT (full copyright and grant in `wayland.xml` `<copyright>`).
A protocol/generate.go +3 -0
@@ -0,0 +1,3 @@
1+package protocol
2+
3+//go:generate go run ../cmd/wlturbo-scanner -p core -o core/wayland_generated.go wayland.xml
@@ -0,0 +1,3 @@
1+package protocol
2+
3+//go:generate go run ../cmd/wlturbo-scanner -p core -o core/wayland_generated.go wayland.xml
A protocol/wayland.xml +3442 -0 large diff (3442 lines changed): collapsed

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close