detached 2db8eeb

fix(permission): work around purego bool bug and fix linting

- Use GObject property accessors instead of C function wrappers in
  determinePermissionTypes to avoid purego bool return value bug
- Refactor permission_popup.go to fix funlen and mnd linter issues:
  - Extract setupContainers() and setupLabels() helpers
  - Add createPermissionButton() and wireButton() helpers
  - Add buttonSpacing constant to replace magic number
commit
2db8eeb50b581584429de12642231982760f42f6
author
brice · · 2026-01-30 06:38 UTC
committer
brice · · 2026-02-07 11:58 UTC
parent
9bb5879
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +71 -44
M internal/infrastructure/webkit/webview.go +14 -3
@@ -1004,17 +1004,28 @@ func (wv *WebView) connectPermissionRequestSignal() {
10041004
10051005 // determinePermissionTypes extracts permission types from a WebKit permission request.
10061006 // This uses type checking to identify UserMediaPermissionRequest and its specific types.
1007+// We use GObject property accessors as the primary method because the C function wrappers
1008+// can hit a purego bug where bool return values are misread.
10071009 func (wv *WebView) determinePermissionTypes(requestPtr uintptr) []string {
1008- _ = wv // suppress unused receiver warning, method may need receiver in future
10091010 var types []string
10101011
10111012 // Try to cast to UserMediaPermissionRequest
10121013 userMediaReq := webkit.UserMediaPermissionRequestNewFromInternalPtr(requestPtr)
10131014 if userMediaReq != nil {
1014- if webkit.UserMediaPermissionIsForAudioDevice(userMediaReq) {
1015+ // Use GObject property accessors — more reliable than the C function wrappers
1016+ // which can hit the purego bool return value bug.
1017+ isAudio := userMediaReq.GetPropertyIsForAudioDevice()
1018+ isVideo := userMediaReq.GetPropertyIsForVideoDevice()
1019+
1020+ wv.logger.Debug().
1021+ Bool("is_audio", isAudio).
1022+ Bool("is_video", isVideo).
1023+ Msg("permission request type detection")
1024+
1025+ if isAudio {
10151026 types = append(types, "microphone")
10161027 }
1017- if webkit.UserMediaPermissionIsForVideoDevice(userMediaReq) {
1028+ if isVideo {
10181029 // Check if this is display capture or camera
10191030 if webkit.UserMediaPermissionIsForDisplayDevice(userMediaReq) {
10201031 types = append(types, "display")
@@ -1004,17 +1004,28 @@ func (wv *WebView) connectPermissionRequestSignal() {
1004 1004
1005 // determinePermissionTypes extracts permission types from a WebKit permission request.1005 // determinePermissionTypes extracts permission types from a WebKit permission request.
1006 // This uses type checking to identify UserMediaPermissionRequest and its specific types.1006 // This uses type checking to identify UserMediaPermissionRequest and its specific types.
1007+// We use GObject property accessors as the primary method because the C function wrappers
1008+// can hit a purego bug where bool return values are misread.
1007 func (wv *WebView) determinePermissionTypes(requestPtr uintptr) []string {1009 func (wv *WebView) determinePermissionTypes(requestPtr uintptr) []string {
1008- _ = wv // suppress unused receiver warning, method may need receiver in future
1009 var types []string1010 var types []string
1010 1011
1011 // Try to cast to UserMediaPermissionRequest1012 // Try to cast to UserMediaPermissionRequest
1012 userMediaReq := webkit.UserMediaPermissionRequestNewFromInternalPtr(requestPtr)1013 userMediaReq := webkit.UserMediaPermissionRequestNewFromInternalPtr(requestPtr)
1013 if userMediaReq != nil {1014 if userMediaReq != nil {
1014- if webkit.UserMediaPermissionIsForAudioDevice(userMediaReq) {1015+ // Use GObject property accessors — more reliable than the C function wrappers
1016+ // which can hit the purego bool return value bug.
1017+ isAudio := userMediaReq.GetPropertyIsForAudioDevice()
1018+ isVideo := userMediaReq.GetPropertyIsForVideoDevice()
1019+
1020+ wv.logger.Debug().
1021+ Bool("is_audio", isAudio).
1022+ Bool("is_video", isVideo).
1023+ Msg("permission request type detection")
1024+
1025+ if isAudio {
1015 types = append(types, "microphone")1026 types = append(types, "microphone")
1016 }1027 }
1017- if webkit.UserMediaPermissionIsForVideoDevice(userMediaReq) {1028+ if isVideo {
1018 // Check if this is display capture or camera1029 // Check if this is display capture or camera
1019 if webkit.UserMediaPermissionIsForDisplayDevice(userMediaReq) {1030 if webkit.UserMediaPermissionIsForDisplayDevice(userMediaReq) {
1020 types = append(types, "display")1031 types = append(types, "display")
M internal/ui/component/permission_popup.go +57 -41
@@ -10,6 +10,9 @@ import (
1010 "github.com/jwijenbergh/puregotk/v4/gtk"
1111 )
1212
13+// buttonSpacing is the spacing between buttons in the permission popup.
14+const buttonSpacing = 6
15+
1316 // PermissionPopup is a custom overlay component for permission prompts.
1417 // It replaces the Adwaita AlertDialog to sidestep the purego ConnectResponse bug
1518 // and match the app's custom UI style.
@@ -135,8 +138,8 @@ func (pp *PermissionPopup) dismiss(allowed, persistent bool) {
135138 }
136139 }
137140
138-func (pp *PermissionPopup) createWidgets() error {
139- // Outer box - positioned in overlay, hidden by default
141+// setupContainers creates and configures the outer and main boxes.
142+func (pp *PermissionPopup) setupContainers() error {
140143 pp.outerBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
141144 if pp.outerBox == nil {
142145 return errNilWidget("permissionPopupOuterBox")
@@ -146,14 +149,16 @@ func (pp *PermissionPopup) createWidgets() error {
146149 pp.outerBox.SetValign(gtk.AlignStartValue)
147150 pp.outerBox.SetVisible(false)
148151
149- // Main box - styled container
150152 pp.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
151153 if pp.mainBox == nil {
152154 return errNilWidget("permissionPopupMainBox")
153155 }
154156 pp.mainBox.AddCssClass("permission-popup-container")
157+ return nil
158+}
155159
156- // Heading label
160+// setupLabels creates and configures the heading and body labels.
161+func (pp *PermissionPopup) setupLabels() error {
157162 emptyText := ""
158163 pp.headingLabel = gtk.NewLabel(&emptyText)
159164 if pp.headingLabel == nil {
@@ -162,7 +167,6 @@ func (pp *PermissionPopup) createWidgets() error {
162167 pp.headingLabel.AddCssClass("permission-popup-heading")
163168 pp.headingLabel.SetHalign(gtk.AlignStartValue)
164169
165- // Body label
166170 pp.bodyLabel = gtk.NewLabel(&emptyText)
167171 if pp.bodyLabel == nil {
168172 return errNilWidget("permissionPopupBodyLabel")
@@ -170,9 +174,20 @@ func (pp *PermissionPopup) createWidgets() error {
170174 pp.bodyLabel.AddCssClass("permission-popup-body")
171175 pp.bodyLabel.SetHalign(gtk.AlignStartValue)
172176 pp.bodyLabel.SetWrap(true)
177+ return nil
178+}
179+
180+func (pp *PermissionPopup) createWidgets() error {
181+ if err := pp.setupContainers(); err != nil {
182+ return err
183+ }
184+
185+ if err := pp.setupLabels(); err != nil {
186+ return err
187+ }
173188
174189 // Button row
175- btnRow := gtk.NewBox(gtk.OrientationHorizontalValue, 6)
190+ btnRow := gtk.NewBox(gtk.OrientationHorizontalValue, buttonSpacing)
176191 if btnRow == nil {
177192 return errNilWidget("permissionPopupBtnRow")
178193 }
@@ -180,50 +195,32 @@ func (pp *PermissionPopup) createWidgets() error {
180195 btnRow.SetHalign(gtk.AlignEndValue)
181196
182197 // Create buttons: Always Deny | Deny | Allow | Always Allow
183- pp.btnAlwaysDeny = gtk.NewButtonWithLabel("Always Deny")
184- if pp.btnAlwaysDeny == nil {
185- return errNilWidget("permissionPopupBtnAlwaysDeny")
198+ var err error
199+ pp.btnAlwaysDeny, err = pp.createPermissionButton("Always Deny", []string{"permission-popup-btn", "permission-popup-btn-destructive"})
200+ if err != nil {
201+ return err
186202 }
187- pp.btnAlwaysDeny.AddCssClass("permission-popup-btn")
188- pp.btnAlwaysDeny.AddCssClass("permission-popup-btn-destructive")
189203
190- pp.btnDeny = gtk.NewButtonWithLabel("Deny")
191- if pp.btnDeny == nil {
192- return errNilWidget("permissionPopupBtnDeny")
204+ pp.btnDeny, err = pp.createPermissionButton("Deny", []string{"permission-popup-btn", "permission-popup-btn-deny"})
205+ if err != nil {
206+ return err
193207 }
194- pp.btnDeny.AddCssClass("permission-popup-btn")
195- pp.btnDeny.AddCssClass("permission-popup-btn-deny")
196208
197- pp.btnAllow = gtk.NewButtonWithLabel("Allow")
198- if pp.btnAllow == nil {
199- return errNilWidget("permissionPopupBtnAllow")
209+ pp.btnAllow, err = pp.createPermissionButton("Allow", []string{"permission-popup-btn", "permission-popup-btn-allow"})
210+ if err != nil {
211+ return err
200212 }
201- pp.btnAllow.AddCssClass("permission-popup-btn")
202- pp.btnAllow.AddCssClass("permission-popup-btn-allow")
203213
204- pp.btnAlwaysAllow = gtk.NewButtonWithLabel("Always Allow")
205- if pp.btnAlwaysAllow == nil {
206- return errNilWidget("permissionPopupBtnAlwaysAllow")
214+ pp.btnAlwaysAllow, err = pp.createPermissionButton("Always Allow", []string{"permission-popup-btn", "permission-popup-btn-allow"})
215+ if err != nil {
216+ return err
207217 }
208- pp.btnAlwaysAllow.AddCssClass("permission-popup-btn")
209- pp.btnAlwaysAllow.AddCssClass("permission-popup-btn-allow")
210218
211219 // Wire button clicks
212- alwaysDenyCb := func(_ gtk.Button) { pp.dismiss(false, true) }
213- pp.retainedCallbacks = append(pp.retainedCallbacks, alwaysDenyCb)
214- pp.btnAlwaysDeny.ConnectClicked(&alwaysDenyCb)
215-
216- denyCb := func(_ gtk.Button) { pp.dismiss(false, false) }
217- pp.retainedCallbacks = append(pp.retainedCallbacks, denyCb)
218- pp.btnDeny.ConnectClicked(&denyCb)
219-
220- allowCb := func(_ gtk.Button) { pp.dismiss(true, false) }
221- pp.retainedCallbacks = append(pp.retainedCallbacks, allowCb)
222- pp.btnAllow.ConnectClicked(&allowCb)
223-
224- alwaysAllowCb := func(_ gtk.Button) { pp.dismiss(true, true) }
225- pp.retainedCallbacks = append(pp.retainedCallbacks, alwaysAllowCb)
226- pp.btnAlwaysAllow.ConnectClicked(&alwaysAllowCb)
220+ pp.wireButton(pp.btnAlwaysDeny, false, true)
221+ pp.wireButton(pp.btnDeny, false, false)
222+ pp.wireButton(pp.btnAllow, true, false)
223+ pp.wireButton(pp.btnAlwaysAllow, true, true)
227224
228225 // Assemble button row
229226 btnRow.Append(&pp.btnAlwaysDeny.Widget)
@@ -242,6 +239,25 @@ func (pp *PermissionPopup) createWidgets() error {
242239 return nil
243240 }
244241
242+// createPermissionButton creates a permission button with the given label and CSS classes.
243+func (pp *PermissionPopup) createPermissionButton(label string, cssClasses []string) (*gtk.Button, error) {
244+ btn := gtk.NewButtonWithLabel(label)
245+ if btn == nil {
246+ return nil, errNilWidget("permissionPopupBtn" + label)
247+ }
248+ for _, class := range cssClasses {
249+ btn.AddCssClass(class)
250+ }
251+ return btn, nil
252+}
253+
254+// wireButton connects a button click to the dismiss callback.
255+func (pp *PermissionPopup) wireButton(btn *gtk.Button, allowed, persistent bool) {
256+ cb := func(_ gtk.Button) { pp.dismiss(allowed, persistent) }
257+ pp.retainedCallbacks = append(pp.retainedCallbacks, cb)
258+ btn.ConnectClicked(&cb)
259+}
260+
245261 func (pp *PermissionPopup) attachKeyController() {
246262 if pp.outerBox == nil {
247263 return
@@ -10,6 +10,9 @@ import (
10 "github.com/jwijenbergh/puregotk/v4/gtk"10 "github.com/jwijenbergh/puregotk/v4/gtk"
11 )11 )
12 12
13+// buttonSpacing is the spacing between buttons in the permission popup.
14+const buttonSpacing = 6
15+
13 // PermissionPopup is a custom overlay component for permission prompts.16 // PermissionPopup is a custom overlay component for permission prompts.
14 // It replaces the Adwaita AlertDialog to sidestep the purego ConnectResponse bug17 // It replaces the Adwaita AlertDialog to sidestep the purego ConnectResponse bug
15 // and match the app's custom UI style.18 // and match the app's custom UI style.
@@ -135,8 +138,8 @@ func (pp *PermissionPopup) dismiss(allowed, persistent bool) {
135 }138 }
136 }139 }
137 140
138-func (pp *PermissionPopup) createWidgets() error {141+// setupContainers creates and configures the outer and main boxes.
139- // Outer box - positioned in overlay, hidden by default142+func (pp *PermissionPopup) setupContainers() error {
140 pp.outerBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)143 pp.outerBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
141 if pp.outerBox == nil {144 if pp.outerBox == nil {
142 return errNilWidget("permissionPopupOuterBox")145 return errNilWidget("permissionPopupOuterBox")
@@ -146,14 +149,16 @@ func (pp *PermissionPopup) createWidgets() error {
146 pp.outerBox.SetValign(gtk.AlignStartValue)149 pp.outerBox.SetValign(gtk.AlignStartValue)
147 pp.outerBox.SetVisible(false)150 pp.outerBox.SetVisible(false)
148 151
149- // Main box - styled container
150 pp.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)152 pp.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
151 if pp.mainBox == nil {153 if pp.mainBox == nil {
152 return errNilWidget("permissionPopupMainBox")154 return errNilWidget("permissionPopupMainBox")
153 }155 }
154 pp.mainBox.AddCssClass("permission-popup-container")156 pp.mainBox.AddCssClass("permission-popup-container")
157+ return nil
158+}
155 159
156- // Heading label160+// setupLabels creates and configures the heading and body labels.
161+func (pp *PermissionPopup) setupLabels() error {
157 emptyText := ""162 emptyText := ""
158 pp.headingLabel = gtk.NewLabel(&emptyText)163 pp.headingLabel = gtk.NewLabel(&emptyText)
159 if pp.headingLabel == nil {164 if pp.headingLabel == nil {
@@ -162,7 +167,6 @@ func (pp *PermissionPopup) createWidgets() error {
162 pp.headingLabel.AddCssClass("permission-popup-heading")167 pp.headingLabel.AddCssClass("permission-popup-heading")
163 pp.headingLabel.SetHalign(gtk.AlignStartValue)168 pp.headingLabel.SetHalign(gtk.AlignStartValue)
164 169
165- // Body label
166 pp.bodyLabel = gtk.NewLabel(&emptyText)170 pp.bodyLabel = gtk.NewLabel(&emptyText)
167 if pp.bodyLabel == nil {171 if pp.bodyLabel == nil {
168 return errNilWidget("permissionPopupBodyLabel")172 return errNilWidget("permissionPopupBodyLabel")
@@ -170,9 +174,20 @@ func (pp *PermissionPopup) createWidgets() error {
170 pp.bodyLabel.AddCssClass("permission-popup-body")174 pp.bodyLabel.AddCssClass("permission-popup-body")
171 pp.bodyLabel.SetHalign(gtk.AlignStartValue)175 pp.bodyLabel.SetHalign(gtk.AlignStartValue)
172 pp.bodyLabel.SetWrap(true)176 pp.bodyLabel.SetWrap(true)
177+ return nil
178+}
179+
180+func (pp *PermissionPopup) createWidgets() error {
181+ if err := pp.setupContainers(); err != nil {
182+ return err
183+ }
184+
185+ if err := pp.setupLabels(); err != nil {
186+ return err
187+ }
173 188
174 // Button row189 // Button row
175- btnRow := gtk.NewBox(gtk.OrientationHorizontalValue, 6)190+ btnRow := gtk.NewBox(gtk.OrientationHorizontalValue, buttonSpacing)
176 if btnRow == nil {191 if btnRow == nil {
177 return errNilWidget("permissionPopupBtnRow")192 return errNilWidget("permissionPopupBtnRow")
178 }193 }
@@ -180,50 +195,32 @@ func (pp *PermissionPopup) createWidgets() error {
180 btnRow.SetHalign(gtk.AlignEndValue)195 btnRow.SetHalign(gtk.AlignEndValue)
181 196
182 // Create buttons: Always Deny | Deny | Allow | Always Allow197 // Create buttons: Always Deny | Deny | Allow | Always Allow
183- pp.btnAlwaysDeny = gtk.NewButtonWithLabel("Always Deny")198+ var err error
184- if pp.btnAlwaysDeny == nil {199+ pp.btnAlwaysDeny, err = pp.createPermissionButton("Always Deny", []string{"permission-popup-btn", "permission-popup-btn-destructive"})
185- return errNilWidget("permissionPopupBtnAlwaysDeny")200+ if err != nil {
201+ return err
186 }202 }
187- pp.btnAlwaysDeny.AddCssClass("permission-popup-btn")
188- pp.btnAlwaysDeny.AddCssClass("permission-popup-btn-destructive")
189 203
190- pp.btnDeny = gtk.NewButtonWithLabel("Deny")204+ pp.btnDeny, err = pp.createPermissionButton("Deny", []string{"permission-popup-btn", "permission-popup-btn-deny"})
191- if pp.btnDeny == nil {205+ if err != nil {
192- return errNilWidget("permissionPopupBtnDeny")206+ return err
193 }207 }
194- pp.btnDeny.AddCssClass("permission-popup-btn")
195- pp.btnDeny.AddCssClass("permission-popup-btn-deny")
196 208
197- pp.btnAllow = gtk.NewButtonWithLabel("Allow")209+ pp.btnAllow, err = pp.createPermissionButton("Allow", []string{"permission-popup-btn", "permission-popup-btn-allow"})
198- if pp.btnAllow == nil {210+ if err != nil {
199- return errNilWidget("permissionPopupBtnAllow")211+ return err
200 }212 }
201- pp.btnAllow.AddCssClass("permission-popup-btn")
202- pp.btnAllow.AddCssClass("permission-popup-btn-allow")
203 213
204- pp.btnAlwaysAllow = gtk.NewButtonWithLabel("Always Allow")214+ pp.btnAlwaysAllow, err = pp.createPermissionButton("Always Allow", []string{"permission-popup-btn", "permission-popup-btn-allow"})
205- if pp.btnAlwaysAllow == nil {215+ if err != nil {
206- return errNilWidget("permissionPopupBtnAlwaysAllow")216+ return err
207 }217 }
208- pp.btnAlwaysAllow.AddCssClass("permission-popup-btn")
209- pp.btnAlwaysAllow.AddCssClass("permission-popup-btn-allow")
210 218
211 // Wire button clicks219 // Wire button clicks
212- alwaysDenyCb := func(_ gtk.Button) { pp.dismiss(false, true) }220+ pp.wireButton(pp.btnAlwaysDeny, false, true)
213- pp.retainedCallbacks = append(pp.retainedCallbacks, alwaysDenyCb)221+ pp.wireButton(pp.btnDeny, false, false)
214- pp.btnAlwaysDeny.ConnectClicked(&alwaysDenyCb)222+ pp.wireButton(pp.btnAllow, true, false)
215-223+ pp.wireButton(pp.btnAlwaysAllow, true, true)
216- denyCb := func(_ gtk.Button) { pp.dismiss(false, false) }
217- pp.retainedCallbacks = append(pp.retainedCallbacks, denyCb)
218- pp.btnDeny.ConnectClicked(&denyCb)
219-
220- allowCb := func(_ gtk.Button) { pp.dismiss(true, false) }
221- pp.retainedCallbacks = append(pp.retainedCallbacks, allowCb)
222- pp.btnAllow.ConnectClicked(&allowCb)
223-
224- alwaysAllowCb := func(_ gtk.Button) { pp.dismiss(true, true) }
225- pp.retainedCallbacks = append(pp.retainedCallbacks, alwaysAllowCb)
226- pp.btnAlwaysAllow.ConnectClicked(&alwaysAllowCb)
227 224
228 // Assemble button row225 // Assemble button row
229 btnRow.Append(&pp.btnAlwaysDeny.Widget)226 btnRow.Append(&pp.btnAlwaysDeny.Widget)
@@ -242,6 +239,25 @@ func (pp *PermissionPopup) createWidgets() error {
242 return nil239 return nil
243 }240 }
244 241
242+// createPermissionButton creates a permission button with the given label and CSS classes.
243+func (pp *PermissionPopup) createPermissionButton(label string, cssClasses []string) (*gtk.Button, error) {
244+ btn := gtk.NewButtonWithLabel(label)
245+ if btn == nil {
246+ return nil, errNilWidget("permissionPopupBtn" + label)
247+ }
248+ for _, class := range cssClasses {
249+ btn.AddCssClass(class)
250+ }
251+ return btn, nil
252+}
253+
254+// wireButton connects a button click to the dismiss callback.
255+func (pp *PermissionPopup) wireButton(btn *gtk.Button, allowed, persistent bool) {
256+ cb := func(_ gtk.Button) { pp.dismiss(allowed, persistent) }
257+ pp.retainedCallbacks = append(pp.retainedCallbacks, cb)
258+ btn.ConnectClicked(&cb)
259+}
260+
245 func (pp *PermissionPopup) attachKeyController() {261 func (pp *PermissionPopup) attachKeyController() {
246 if pp.outerBox == nil {262 if pp.outerBox == nil {
247 return263 return

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 8

Tags 42

↑↓ moveenter openesc close