detached f771147

Task 2 review fixes: interface methods, purego dep, error context, templates

commit
f77114756f911bb9a6a71ea5a0edced65466a99c
author
brice · · 2026-04-09 05:32 UTC
parent
768bff5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +129 -77
M .mockery.yml +7 -16
@@ -1,21 +1,12 @@
1-with-expecter: true
1+all: false
2+template: testify
3+formatter: goimports
24 dir: "{{.InterfaceDir}}/mocks"
3-outpkg: mocks
5+pkgname: mocks
6+filename: "mock_{{.InterfaceName}}.go"
7+structname: "Mock{{.InterfaceName}}"
8+
49 packages:
510 github.com/bnema/purego-pipewire/internal/ports/out:
611 interfaces:
7- CAPI:
812 InitAPI:
9- LoopAPI:
10- ContextAPI:
11- CoreAPI:
12- RegistryAPI:
13- StreamAPI:
14- github.com/bnema/purego-pipewire/pipewire:
15- interfaces:
16- Runtime:
17- MainLoop:
18- Context:
19- Core:
20- Registry:
21- Stream:
@@ -1,21 +1,12 @@
1-with-expecter: true1+all: false
2+template: testify
3+formatter: goimports
2 dir: "{{.InterfaceDir}}/mocks"4 dir: "{{.InterfaceDir}}/mocks"
3-outpkg: mocks5+pkgname: mocks
6+filename: "mock_{{.InterfaceName}}.go"
7+structname: "Mock{{.InterfaceName}}"
8+
4 packages:9 packages:
5 github.com/bnema/purego-pipewire/internal/ports/out:10 github.com/bnema/purego-pipewire/internal/ports/out:
6 interfaces:11 interfaces:
7- CAPI:
8 InitAPI:12 InitAPI:
9- LoopAPI:
10- ContextAPI:
11- CoreAPI:
12- RegistryAPI:
13- StreamAPI:
14- github.com/bnema/purego-pipewire/pipewire:
15- interfaces:
16- Runtime:
17- MainLoop:
18- Context:
19- Core:
20- Registry:
21- Stream:
M cmd/pipewiregen/internal/emitter/emitter.go +92 -37
@@ -8,10 +8,40 @@ import (
88 "path/filepath"
99 "strings"
1010 "text/template"
11+ "unicode"
1112
1213 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
1314 )
1415
16+var (
17+ capiTmpl *template.Template
18+ portTmpl *template.Template
19+)
20+
21+func init() {
22+ capiTmpl = template.Must(template.New("capi").Funcs(template.FuncMap{
23+ "title": titleCase,
24+ }).Parse(capiTemplate))
25+
26+ portTmpl = template.Must(template.New("port").Funcs(template.FuncMap{
27+ "title": titleCase,
28+ }).Parse(portTemplate))
29+}
30+
31+// titleCase converts a string to title case (first letter uppercase, rest lowercase).
32+// Replaces deprecated strings.Title.
33+func titleCase(s string) string {
34+ if s == "" {
35+ return ""
36+ }
37+ runes := []rune(s)
38+ runes[0] = unicode.ToUpper(runes[0])
39+ for i := 1; i < len(runes); i++ {
40+ runes[i] = unicode.ToLower(runes[i])
41+ }
42+ return string(runes)
43+}
44+
1545 // Emit generates Go source files from the model and writes them to the root directory.
1646 // It returns a map of generated file paths to their content.
1747 func Emit(m *model.Model, root string) (map[string][]byte, error) {
@@ -56,16 +86,9 @@ func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Lib
5686 LibraryMap: libMap,
5787 }
5888
59- tmpl, err := template.New("capi").Funcs(template.FuncMap{
60- "title": strings.Title,
61- }).Parse(capiTemplate)
62- if err != nil {
63- return nil, err
64- }
65-
6689 var buf bytes.Buffer
67- if err := tmpl.Execute(&buf, data); err != nil {
68- return nil, err
90+ if err := capiTmpl.Execute(&buf, data); err != nil {
91+ return nil, fmt.Errorf("execute capi template for group %q: %w", group.Name, err)
6992 }
7093 return buf.Bytes(), nil
7194 }
@@ -76,19 +99,22 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
7699
77100 // Convert symbols to methods with proper Go names
78101 type Method struct {
79- Name string
80- GoName string
81- Signature string
82- CName string
102+ Name string
103+ GoName string
104+ Params string
105+ Results string
106+ CName string
83107 }
84108
85109 methods := make([]Method, 0, len(groupSymbols))
86110 for _, sym := range groupSymbols {
111+ params, results := parseSignature(sym.Signature)
87112 methods = append(methods, Method{
88- Name: sym.Name,
89- GoName: toGoName(sym.Name),
90- Signature: sym.Signature,
91- CName: sym.Name,
113+ Name: sym.Name,
114+ GoName: toGoName(sym.Name),
115+ Params: params,
116+ Results: results,
117+ CName: sym.Name,
92118 })
93119 }
94120
@@ -100,20 +126,53 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
100126 Methods: methods,
101127 }
102128
103- tmpl, err := template.New("port").Funcs(template.FuncMap{
104- "title": strings.Title,
105- }).Parse(portTemplate)
106- if err != nil {
107- return nil, err
108- }
109-
110129 var buf bytes.Buffer
111- if err := tmpl.Execute(&buf, data); err != nil {
112- return nil, err
130+ if err := portTmpl.Execute(&buf, data); err != nil {
131+ return nil, fmt.Errorf("execute port template for group %q: %w", group.Name, err)
113132 }
114133 return buf.Bytes(), nil
115134 }
116135
136+// parseSignature splits a func signature like "func(argc *int32, argv ***byte)" into params and results.
137+// Returns ("(argc *int32, argv ***byte)", "") for func with no returns,
138+// or ("(x int)", "(int, error)") for func with returns.
139+func parseSignature(sig string) (params, results string) {
140+ // Remove "func" prefix
141+ if !strings.HasPrefix(sig, "func") {
142+ return "()", ""
143+ }
144+ body := strings.TrimSpace(sig[4:])
145+
146+ // Find the closing paren of params (handles nested parens)
147+ depth := 0
148+ paramsEnd := -1
149+ for i, ch := range body {
150+ if ch == '(' {
151+ depth++
152+ } else if ch == ')' {
153+ depth--
154+ if depth == 0 {
155+ paramsEnd = i
156+ break
157+ }
158+ }
159+ }
160+ if paramsEnd < 0 {
161+ return "()", ""
162+ }
163+
164+ params = body[:paramsEnd+1]
165+ rest := strings.TrimSpace(body[paramsEnd+1:])
166+
167+ // Check for return values
168+ if rest != "" {
169+ // Returns could be "error" or "(int, error)" or "int"
170+ results = rest
171+ }
172+
173+ return params, results
174+}
175+
117176 // filterSymbolsByGroup returns symbols that belong to the given group.
118177 func filterSymbolsByGroup(symbols []model.Symbol, groupName string) []model.Symbol {
119178 var result []model.Symbol
@@ -132,14 +191,14 @@ func toGoName(cName string) string {
132191 name := cName[3:]
133192 parts := strings.Split(name, "_")
134193 for i, p := range parts {
135- parts[i] = strings.Title(p)
194+ parts[i] = titleCase(p)
136195 }
137196 return "PW" + strings.Join(parts, "")
138197 }
139198 // Generic conversion
140199 parts := strings.Split(cName, "_")
141200 for i, p := range parts {
142- parts[i] = strings.Title(p)
201+ parts[i] = titleCase(p)
143202 }
144203 return strings.Join(parts, "")
145204 }
@@ -164,7 +223,6 @@ const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
164223 package capi
165224
166225 import "github.com/ebitengine/purego"
167-
168226 {{range .Symbols}}
169227 // {{.Name}}Func is the function type for {{.Name}}.
170228 type {{.Name}}Func {{.Signature}}
@@ -176,13 +234,10 @@ var (
176234 {{end}}
177235 )
178236
179-func register{{.Group.Name | title}}(handle uintptr) error {
237+func register{{.Group.Name | title}}(handle uintptr) {
180238 {{range .Symbols}}
181- if err := purego.RegisterLibFunc(&{{.Name}}, handle, "{{.Name}}"); err != nil {
182- return err
183- }
239+ purego.RegisterLibFunc(&{{.Name}}, handle, "{{.Name}}")
184240 {{end}}
185- return nil
186241 }
187242 `
188243
@@ -192,8 +247,8 @@ package out
192247
193248 // {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.
194249 type {{.Group.Interface}} interface {
195-{{range .Methods}}
196- {{.GoName}} {{.Signature}}
197-{{end}}
250+{{- range .Methods}}
251+ {{.GoName}}{{.Params}}{{if .Results}} {{.Results}}{{end}}
252+{{- end}}
198253 }
199254 `
@@ -8,10 +8,40 @@ import (
8 "path/filepath"8 "path/filepath"
9 "strings"9 "strings"
10 "text/template"10 "text/template"
11+ "unicode"
11 12
12 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"13 "github.com/bnema/purego-pipewire/cmd/pipewiregen/internal/model"
13 )14 )
14 15
16+var (
17+ capiTmpl *template.Template
18+ portTmpl *template.Template
19+)
20+
21+func init() {
22+ capiTmpl = template.Must(template.New("capi").Funcs(template.FuncMap{
23+ "title": titleCase,
24+ }).Parse(capiTemplate))
25+
26+ portTmpl = template.Must(template.New("port").Funcs(template.FuncMap{
27+ "title": titleCase,
28+ }).Parse(portTemplate))
29+}
30+
31+// titleCase converts a string to title case (first letter uppercase, rest lowercase).
32+// Replaces deprecated strings.Title.
33+func titleCase(s string) string {
34+ if s == "" {
35+ return ""
36+ }
37+ runes := []rune(s)
38+ runes[0] = unicode.ToUpper(runes[0])
39+ for i := 1; i < len(runes); i++ {
40+ runes[i] = unicode.ToLower(runes[i])
41+ }
42+ return string(runes)
43+}
44+
15 // Emit generates Go source files from the model and writes them to the root directory.45 // Emit generates Go source files from the model and writes them to the root directory.
16 // It returns a map of generated file paths to their content.46 // It returns a map of generated file paths to their content.
17 func Emit(m *model.Model, root string) (map[string][]byte, error) {47 func Emit(m *model.Model, root string) (map[string][]byte, error) {
@@ -56,16 +86,9 @@ func renderCAPI(group model.Group, symbols []model.Symbol, libraries []model.Lib
56 LibraryMap: libMap,86 LibraryMap: libMap,
57 }87 }
58 88
59- tmpl, err := template.New("capi").Funcs(template.FuncMap{
60- "title": strings.Title,
61- }).Parse(capiTemplate)
62- if err != nil {
63- return nil, err
64- }
65-
66 var buf bytes.Buffer89 var buf bytes.Buffer
67- if err := tmpl.Execute(&buf, data); err != nil {90+ if err := capiTmpl.Execute(&buf, data); err != nil {
68- return nil, err91+ return nil, fmt.Errorf("execute capi template for group %q: %w", group.Name, err)
69 }92 }
70 return buf.Bytes(), nil93 return buf.Bytes(), nil
71 }94 }
@@ -76,19 +99,22 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
76 99
77 // Convert symbols to methods with proper Go names100 // Convert symbols to methods with proper Go names
78 type Method struct {101 type Method struct {
79- Name string102+ Name string
80- GoName string103+ GoName string
81- Signature string104+ Params string
82- CName string105+ Results string
106+ CName string
83 }107 }
84 108
85 methods := make([]Method, 0, len(groupSymbols))109 methods := make([]Method, 0, len(groupSymbols))
86 for _, sym := range groupSymbols {110 for _, sym := range groupSymbols {
111+ params, results := parseSignature(sym.Signature)
87 methods = append(methods, Method{112 methods = append(methods, Method{
88- Name: sym.Name,113+ Name: sym.Name,
89- GoName: toGoName(sym.Name),114+ GoName: toGoName(sym.Name),
90- Signature: sym.Signature,115+ Params: params,
91- CName: sym.Name,116+ Results: results,
117+ CName: sym.Name,
92 })118 })
93 }119 }
94 120
@@ -100,20 +126,53 @@ func renderPort(group model.Group, symbols []model.Symbol) ([]byte, error) {
100 Methods: methods,126 Methods: methods,
101 }127 }
102 128
103- tmpl, err := template.New("port").Funcs(template.FuncMap{
104- "title": strings.Title,
105- }).Parse(portTemplate)
106- if err != nil {
107- return nil, err
108- }
109-
110 var buf bytes.Buffer129 var buf bytes.Buffer
111- if err := tmpl.Execute(&buf, data); err != nil {130+ if err := portTmpl.Execute(&buf, data); err != nil {
112- return nil, err131+ return nil, fmt.Errorf("execute port template for group %q: %w", group.Name, err)
113 }132 }
114 return buf.Bytes(), nil133 return buf.Bytes(), nil
115 }134 }
116 135
136+// parseSignature splits a func signature like "func(argc *int32, argv ***byte)" into params and results.
137+// Returns ("(argc *int32, argv ***byte)", "") for func with no returns,
138+// or ("(x int)", "(int, error)") for func with returns.
139+func parseSignature(sig string) (params, results string) {
140+ // Remove "func" prefix
141+ if !strings.HasPrefix(sig, "func") {
142+ return "()", ""
143+ }
144+ body := strings.TrimSpace(sig[4:])
145+
146+ // Find the closing paren of params (handles nested parens)
147+ depth := 0
148+ paramsEnd := -1
149+ for i, ch := range body {
150+ if ch == '(' {
151+ depth++
152+ } else if ch == ')' {
153+ depth--
154+ if depth == 0 {
155+ paramsEnd = i
156+ break
157+ }
158+ }
159+ }
160+ if paramsEnd < 0 {
161+ return "()", ""
162+ }
163+
164+ params = body[:paramsEnd+1]
165+ rest := strings.TrimSpace(body[paramsEnd+1:])
166+
167+ // Check for return values
168+ if rest != "" {
169+ // Returns could be "error" or "(int, error)" or "int"
170+ results = rest
171+ }
172+
173+ return params, results
174+}
175+
117 // filterSymbolsByGroup returns symbols that belong to the given group.176 // filterSymbolsByGroup returns symbols that belong to the given group.
118 func filterSymbolsByGroup(symbols []model.Symbol, groupName string) []model.Symbol {177 func filterSymbolsByGroup(symbols []model.Symbol, groupName string) []model.Symbol {
119 var result []model.Symbol178 var result []model.Symbol
@@ -132,14 +191,14 @@ func toGoName(cName string) string {
132 name := cName[3:]191 name := cName[3:]
133 parts := strings.Split(name, "_")192 parts := strings.Split(name, "_")
134 for i, p := range parts {193 for i, p := range parts {
135- parts[i] = strings.Title(p)194+ parts[i] = titleCase(p)
136 }195 }
137 return "PW" + strings.Join(parts, "")196 return "PW" + strings.Join(parts, "")
138 }197 }
139 // Generic conversion198 // Generic conversion
140 parts := strings.Split(cName, "_")199 parts := strings.Split(cName, "_")
141 for i, p := range parts {200 for i, p := range parts {
142- parts[i] = strings.Title(p)201+ parts[i] = titleCase(p)
143 }202 }
144 return strings.Join(parts, "")203 return strings.Join(parts, "")
145 }204 }
@@ -164,7 +223,6 @@ const capiTemplate = `// Code generated by pipewiregen. DO NOT EDIT.
164 package capi223 package capi
165 224
166 import "github.com/ebitengine/purego"225 import "github.com/ebitengine/purego"
167-
168 {{range .Symbols}}226 {{range .Symbols}}
169 // {{.Name}}Func is the function type for {{.Name}}.227 // {{.Name}}Func is the function type for {{.Name}}.
170 type {{.Name}}Func {{.Signature}}228 type {{.Name}}Func {{.Signature}}
@@ -176,13 +234,10 @@ var (
176 {{end}}234 {{end}}
177 )235 )
178 236
179-func register{{.Group.Name | title}}(handle uintptr) error {237+func register{{.Group.Name | title}}(handle uintptr) {
180 {{range .Symbols}}238 {{range .Symbols}}
181- if err := purego.RegisterLibFunc(&{{.Name}}, handle, "{{.Name}}"); err != nil {239+ purego.RegisterLibFunc(&{{.Name}}, handle, "{{.Name}}")
182- return err
183- }
184 {{end}}240 {{end}}
185- return nil
186 }241 }
187 `242 `
188 243
@@ -192,8 +247,8 @@ package out
192 247
193 // {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.248 // {{.Group.Interface}} defines the outbound interface for {{.Group.Name}} operations.
194 type {{.Group.Interface}} interface {249 type {{.Group.Interface}} interface {
195-{{range .Methods}}250+{{- range .Methods}}
196- {{.GoName}} {{.Signature}}251+ {{.GoName}}{{.Params}}{{if .Results}} {{.Results}}{{end}}
197-{{end}}252+{{- end}}
198 }253 }
199 `254 `
M cmd/pipewiregen/internal/parser/parser.go +12 -1
@@ -37,11 +37,16 @@ func validate(m *model.Model) error {
3737 libNames[lib.Name] = true
3838 }
3939
40- // Validate groups
40+ // Build set of valid group names
41+ groupNames := make(map[string]bool)
4142 for i, group := range m.Groups {
4243 if group.Name == "" {
4344 return fmt.Errorf("group at index %d has empty name", i)
4445 }
46+ if group.Interface == "" {
47+ return fmt.Errorf("group %q has empty interface", group.Name)
48+ }
49+ groupNames[group.Name] = true
4550 }
4651
4752 // Validate symbols
@@ -58,6 +63,12 @@ func validate(m *model.Model) error {
5863 if !libNames[sym.Library] {
5964 return fmt.Errorf("symbol %q references unknown library %q", sym.Name, sym.Library)
6065 }
66+ if sym.Group == "" {
67+ return fmt.Errorf("symbol %q has empty group", sym.Name)
68+ }
69+ if !groupNames[sym.Group] {
70+ return fmt.Errorf("symbol %q references unknown group %q", sym.Name, sym.Group)
71+ }
6172 }
6273
6374 return nil
@@ -37,11 +37,16 @@ func validate(m *model.Model) error {
37 libNames[lib.Name] = true37 libNames[lib.Name] = true
38 }38 }
39 39
40- // Validate groups40+ // Build set of valid group names
41+ groupNames := make(map[string]bool)
41 for i, group := range m.Groups {42 for i, group := range m.Groups {
42 if group.Name == "" {43 if group.Name == "" {
43 return fmt.Errorf("group at index %d has empty name", i)44 return fmt.Errorf("group at index %d has empty name", i)
44 }45 }
46+ if group.Interface == "" {
47+ return fmt.Errorf("group %q has empty interface", group.Name)
48+ }
49+ groupNames[group.Name] = true
45 }50 }
46 51
47 // Validate symbols52 // Validate symbols
@@ -58,6 +63,12 @@ func validate(m *model.Model) error {
58 if !libNames[sym.Library] {63 if !libNames[sym.Library] {
59 return fmt.Errorf("symbol %q references unknown library %q", sym.Name, sym.Library)64 return fmt.Errorf("symbol %q references unknown library %q", sym.Name, sym.Library)
60 }65 }
66+ if sym.Group == "" {
67+ return fmt.Errorf("symbol %q has empty group", sym.Name)
68+ }
69+ if !groupNames[sym.Group] {
70+ return fmt.Errorf("symbol %q references unknown group %q", sym.Name, sym.Group)
71+ }
61 }72 }
62 73
63 return nil74 return nil
M cmd/pipewiregen/internal/parser/testdata/empty_symbol_name.json +2 -2
@@ -3,9 +3,9 @@
33 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
44 ],
55 "groups": [
6- {"name": "init", "symbols": [""]}
6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": [""]}
77 ],
88 "symbols": [
9- {"name": "", "library": "pipewire", "signature": "func()"}
9+ {"name": "", "library": "pipewire", "group": "init", "signature": "func()"}
1010 ]
1111 }
@@ -3,9 +3,9 @@
3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
4 ],4 ],
5 "groups": [5 "groups": [
6- {"name": "init", "symbols": [""]}6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": [""]}
7 ],7 ],
8 "symbols": [8 "symbols": [
9- {"name": "", "library": "pipewire", "signature": "func()"}9+ {"name": "", "library": "pipewire", "group": "init", "signature": "func()"}
10 ]10 ]
11 }11 }
M cmd/pipewiregen/internal/parser/testdata/empty_symbol_sig.json +2 -2
@@ -3,9 +3,9 @@
33 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
44 ],
55 "groups": [
6- {"name": "init", "symbols": ["pw_init"]}
6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]}
77 ],
88 "symbols": [
9- {"name": "pw_init", "library": "pipewire", "signature": ""}
9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": ""}
1010 ]
1111 }
@@ -3,9 +3,9 @@
3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
4 ],4 ],
5 "groups": [5 "groups": [
6- {"name": "init", "symbols": ["pw_init"]}6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]}
7 ],7 ],
8 "symbols": [8 "symbols": [
9- {"name": "pw_init", "library": "pipewire", "signature": ""}9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": ""}
10 ]10 ]
11 }11 }
M cmd/pipewiregen/internal/parser/testdata/minimal_pipewire.json +3 -3
@@ -3,10 +3,10 @@
33 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
44 ],
55 "groups": [
6- {"name": "init", "symbols": ["pw_init", "pw_deinit"]}
6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]}
77 ],
88 "symbols": [
9- {"name": "pw_init", "library": "pipewire", "signature": "func(argc *int32, argv ***byte)"},
10- {"name": "pw_deinit", "library": "pipewire", "signature": "func()"}
9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
10+ {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"}
1111 ]
1212 }
@@ -3,10 +3,10 @@
3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
4 ],4 ],
5 "groups": [5 "groups": [
6- {"name": "init", "symbols": ["pw_init", "pw_deinit"]}6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init", "pw_deinit"]}
7 ],7 ],
8 "symbols": [8 "symbols": [
9- {"name": "pw_init", "library": "pipewire", "signature": "func(argc *int32, argv ***byte)"},9+ {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func(argc *int32, argv ***byte)"},
10- {"name": "pw_deinit", "library": "pipewire", "signature": "func()"}10+ {"name": "pw_deinit", "library": "pipewire", "group": "init", "signature": "func()"}
11 ]11 ]
12 }12 }
M cmd/pipewiregen/internal/parser/testdata/unknown_library.json +2 -2
@@ -3,9 +3,9 @@
33 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
44 ],
55 "groups": [
6- {"name": "init", "symbols": ["pw_init"]}
6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]}
77 ],
88 "symbols": [
9- {"name": "pw_init", "library": "nonexistent", "signature": "func()"}
9+ {"name": "pw_init", "library": "nonexistent", "group": "init", "signature": "func()"}
1010 ]
1111 }
@@ -3,9 +3,9 @@
3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}3 {"name": "pipewire", "soname": "libpipewire-0.3.so.0"}
4 ],4 ],
5 "groups": [5 "groups": [
6- {"name": "init", "symbols": ["pw_init"]}6+ {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]}
7 ],7 ],
8 "symbols": [8 "symbols": [
9- {"name": "pw_init", "library": "nonexistent", "signature": "func()"}9+ {"name": "pw_init", "library": "nonexistent", "group": "init", "signature": "func()"}
10 ]10 ]
11 }11 }
M go.mod +2 -0
@@ -1,3 +1,5 @@
11 module github.com/bnema/purego-pipewire
22
33 go 1.23
4+
5+require github.com/ebitengine/purego v0.10.0 // indirect
@@ -1,3 +1,5 @@
1 module github.com/bnema/purego-pipewire1 module github.com/bnema/purego-pipewire
2 2
3 go 1.233 go 1.23
4+
5+require github.com/ebitengine/purego v0.10.0 // indirect
A go.sum +2 -0 generated file: collapsed
M internal/capi/init_gen.go +3 -9 generated file: collapsed
M internal/ports/out/init_gen.go +2 -5 generated file: 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