detached 9a83276

fix(ui): release mode frame border reference and tidy native GTK test setup

commit
9a8327648025fde1929b81b8a17debb6e17ab01a
author
brice · · 2026-10-14 08:21 UTC
parent
320f6a5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +26 -1
M internal/ui/mode_frame.go +2 -0
@@ -75,6 +75,8 @@ func (f *modeFrame) destroy() {
7575 f.borderRect = nil
7676 if f.overlay != nil && f.border != nil {
7777 f.overlay.RemoveOverlay(&f.border.Widget)
78+ // NewBox's reference is not released by the removal.
79+ f.border.Unref()
7880 }
7981 f.overlay = nil
8082 f.border = nil
@@ -75,6 +75,8 @@ func (f *modeFrame) destroy() {
75 f.borderRect = nil75 f.borderRect = nil
76 if f.overlay != nil && f.border != nil {76 if f.overlay != nil && f.border != nil {
77 f.overlay.RemoveOverlay(&f.border.Widget)77 f.overlay.RemoveOverlay(&f.border.Widget)
78+ // NewBox's reference is not released by the removal.
79+ f.border.Unref()
78 }80 }
79 f.overlay = nil81 f.overlay = nil
80 f.border = nil82 f.border = nil
M internal/ui/mode_toaster_view_gtk_test.go +24 -1
@@ -22,14 +22,21 @@ func watchFinalized(t *testing.T, widget *gtk.Widget) *bool {
2222 func TestGTKModeToasterViewReleasesWidgets(t *testing.T) {
2323 gtkApp := requireGTKDisplayApp(t)
2424 defer gtkApp.Unref()
25+ // Windows may only be created once the GApplication has started up;
26+ // registering emits the startup signal.
27+ registered, err := gtkApp.Register(nil)
28+ require.NoError(t, err)
29+ require.True(t, registered, "the test application registers")
2530
2631 var view *gtkModeToasterView
2732 done := make(chan struct{})
2833 run := glib.SourceFunc(func(uintptr) bool {
2934 defer close(done)
3035 window := gtk.NewApplicationWindow(gtkApp)
36+ require.NotNil(t, window)
3137 overlay := gtk.NewOverlay()
3238 window.SetChild(&overlay.Widget)
39+ overlayFinalized := watchFinalized(t, &overlay.Widget)
3340 view = newGTKModeToasterView(overlay, window, modeToasterEvents{})
3441 require.NotNil(t, view)
3542
@@ -59,7 +66,23 @@ func TestGTKModeToasterViewReleasesWidgets(t *testing.T) {
5966 require.True(t, *body, "destroy releases the legend body")
6067 require.True(t, *label, "destroy releases the pill label")
6168 require.True(t, *lastRow, "destroy releases the legend rows")
62- window.Destroy()
69+
70+ frame := newModeFrame(overlay)
71+ require.NotNil(t, frame)
72+ border := watchFinalized(t, &frame.border.Widget)
73+ frame.destroy()
74+ require.True(t, *border, "destroy releases the mode frame border")
75+
76+ // The window is never presented: destroying an unrealized window
77+ // crashes on Wayland, so drop the references instead (the same as
78+ // the global shortcut handler test). The overlay's last reference
79+ // goes with the window's child; the window itself stays on GTK's
80+ // toplevel list until destroyed, so its finalization is not asserted.
81+ window.SetChild(nil)
82+ overlay.Unref()
83+ require.True(t, *overlayFinalized, "the test releases its overlay")
84+ gtkApp.RemoveWindow(&window.Window)
85+ window.Unref()
6386 return false
6487 })
6588 glib.MainContextDefault().Invoke(&run, 0)
@@ -22,14 +22,21 @@ func watchFinalized(t *testing.T, widget *gtk.Widget) *bool {
22 func TestGTKModeToasterViewReleasesWidgets(t *testing.T) {22 func TestGTKModeToasterViewReleasesWidgets(t *testing.T) {
23 gtkApp := requireGTKDisplayApp(t)23 gtkApp := requireGTKDisplayApp(t)
24 defer gtkApp.Unref()24 defer gtkApp.Unref()
25+ // Windows may only be created once the GApplication has started up;
26+ // registering emits the startup signal.
27+ registered, err := gtkApp.Register(nil)
28+ require.NoError(t, err)
29+ require.True(t, registered, "the test application registers")
25 30
26 var view *gtkModeToasterView31 var view *gtkModeToasterView
27 done := make(chan struct{})32 done := make(chan struct{})
28 run := glib.SourceFunc(func(uintptr) bool {33 run := glib.SourceFunc(func(uintptr) bool {
29 defer close(done)34 defer close(done)
30 window := gtk.NewApplicationWindow(gtkApp)35 window := gtk.NewApplicationWindow(gtkApp)
36+ require.NotNil(t, window)
31 overlay := gtk.NewOverlay()37 overlay := gtk.NewOverlay()
32 window.SetChild(&overlay.Widget)38 window.SetChild(&overlay.Widget)
39+ overlayFinalized := watchFinalized(t, &overlay.Widget)
33 view = newGTKModeToasterView(overlay, window, modeToasterEvents{})40 view = newGTKModeToasterView(overlay, window, modeToasterEvents{})
34 require.NotNil(t, view)41 require.NotNil(t, view)
35 42
@@ -59,7 +66,23 @@ func TestGTKModeToasterViewReleasesWidgets(t *testing.T) {
59 require.True(t, *body, "destroy releases the legend body")66 require.True(t, *body, "destroy releases the legend body")
60 require.True(t, *label, "destroy releases the pill label")67 require.True(t, *label, "destroy releases the pill label")
61 require.True(t, *lastRow, "destroy releases the legend rows")68 require.True(t, *lastRow, "destroy releases the legend rows")
62- window.Destroy()69+
70+ frame := newModeFrame(overlay)
71+ require.NotNil(t, frame)
72+ border := watchFinalized(t, &frame.border.Widget)
73+ frame.destroy()
74+ require.True(t, *border, "destroy releases the mode frame border")
75+
76+ // The window is never presented: destroying an unrealized window
77+ // crashes on Wayland, so drop the references instead (the same as
78+ // the global shortcut handler test). The overlay's last reference
79+ // goes with the window's child; the window itself stays on GTK's
80+ // toplevel list until destroyed, so its finalization is not asserted.
81+ window.SetChild(nil)
82+ overlay.Unref()
83+ require.True(t, *overlayFinalized, "the test releases its overlay")
84+ gtkApp.RemoveWindow(&window.Window)
85+ window.Unref()
63 return false86 return false
64 })87 })
65 glib.MainContextDefault().Invoke(&run, 0)88 glib.MainContextDefault().Invoke(&run, 0)

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