detached 85a20aa

fix(favorites): release dynamic tag callbacks

commit
85a20aa2497614a5b44e3fa825e4cfccea14417d
author
brice · · 2026-08-12 05:50 UTC
committer
brice · · 2026-08-12 05:52 UTC
parent
965ed59
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +52 -18
M internal/ui/component/favorites_sidebar.go +7 -5
@@ -69,11 +69,13 @@ type FavoritesSidebar struct {
6969 confirmDelete bool
7070 confirmDeleteID entity.FavoriteID
7171
72- retainedCallbacks []any
73- tagCallbacks []any
74- ctx context.Context
75- cancel context.CancelFunc
76- mu sync.RWMutex
72+ retainedCallbacks []any
73+ tagCallbacks []any
74+ tagPromptCallbacks []any
75+ formTagMatchCallbacks []any
76+ ctx context.Context
77+ cancel context.CancelFunc
78+ mu sync.RWMutex
7779
7880 idleScheduler func(glib.SourceFunc)
7981 }
@@ -69,11 +69,13 @@ type FavoritesSidebar struct {
69 confirmDelete bool69 confirmDelete bool
70 confirmDeleteID entity.FavoriteID70 confirmDeleteID entity.FavoriteID
71 71
72- retainedCallbacks []any72+ retainedCallbacks []any
73- tagCallbacks []any73+ tagCallbacks []any
74- ctx context.Context74+ tagPromptCallbacks []any
75- cancel context.CancelFunc75+ formTagMatchCallbacks []any
76- mu sync.RWMutex76+ ctx context.Context
77+ cancel context.CancelFunc
78+ mu sync.RWMutex
77 79
78 idleScheduler func(glib.SourceFunc)80 idleScheduler func(glib.SourceFunc)
79 }81 }
M internal/ui/component/favorites_sidebar_form_tags.go +7 -1
@@ -65,6 +65,7 @@ func (fs *FavoritesSidebar) renderFormTagMatches(query string) {
6565 fs.mu.RUnlock()
6666
6767 clearBoxChildren(fs.formTagMatches)
68+ callbacks := make([]any, 0)
6869 for _, tag := range formTagCandidates(tags, selected, query) {
6970 t := *tag
7071 label := t.Name
@@ -77,10 +78,15 @@ func (fs *FavoritesSidebar) renderFormTagMatches(query string) {
7778 }
7879 button.AddCssClass("favorites-sidebar-form-tag-match")
7980 callback := func(_ gtk.Button) { fs.toggleFormTag(t.ID) }
80- fs.retainedCallbacks = append(fs.retainedCallbacks, callback)
81+ callbacks = append(callbacks, callback)
8182 button.ConnectClicked(&callback)
8283 fs.formTagMatches.Append(&button.Widget)
8384 }
85+ fs.mu.Lock()
86+ if !fs.destroyed {
87+ fs.formTagMatchCallbacks = callbacks
88+ }
89+ fs.mu.Unlock()
8490 }
8591
8692 func (fs *FavoritesSidebar) toggleFormTag(tagID entity.TagID) {
@@ -65,6 +65,7 @@ func (fs *FavoritesSidebar) renderFormTagMatches(query string) {
65 fs.mu.RUnlock()65 fs.mu.RUnlock()
66 66
67 clearBoxChildren(fs.formTagMatches)67 clearBoxChildren(fs.formTagMatches)
68+ callbacks := make([]any, 0)
68 for _, tag := range formTagCandidates(tags, selected, query) {69 for _, tag := range formTagCandidates(tags, selected, query) {
69 t := *tag70 t := *tag
70 label := t.Name71 label := t.Name
@@ -77,10 +78,15 @@ func (fs *FavoritesSidebar) renderFormTagMatches(query string) {
77 }78 }
78 button.AddCssClass("favorites-sidebar-form-tag-match")79 button.AddCssClass("favorites-sidebar-form-tag-match")
79 callback := func(_ gtk.Button) { fs.toggleFormTag(t.ID) }80 callback := func(_ gtk.Button) { fs.toggleFormTag(t.ID) }
80- fs.retainedCallbacks = append(fs.retainedCallbacks, callback)81+ callbacks = append(callbacks, callback)
81 button.ConnectClicked(&callback)82 button.ConnectClicked(&callback)
82 fs.formTagMatches.Append(&button.Widget)83 fs.formTagMatches.Append(&button.Widget)
83 }84 }
85+ fs.mu.Lock()
86+ if !fs.destroyed {
87+ fs.formTagMatchCallbacks = callbacks
88+ }
89+ fs.mu.Unlock()
84 }90 }
85 91
86 func (fs *FavoritesSidebar) toggleFormTag(tagID entity.TagID) {92 func (fs *FavoritesSidebar) toggleFormTag(tagID entity.TagID) {
M internal/ui/component/favorites_sidebar_forms.go +1 -1
@@ -181,7 +181,7 @@ func (fs *FavoritesSidebar) cancelManagement() bool {
181181 clearBoxChildren(fs.formBox)
182182 }
183183 if fs.tagPromptBox != nil {
184- clearBoxChildren(fs.tagPromptBox)
184+ fs.clearTagPromptContent()
185185 fs.tagPromptBox.SetVisible(false)
186186 fs.tagNameEntry = nil
187187 fs.tagPromptSaveBtn = nil
@@ -181,7 +181,7 @@ func (fs *FavoritesSidebar) cancelManagement() bool {
181 clearBoxChildren(fs.formBox)181 clearBoxChildren(fs.formBox)
182 }182 }
183 if fs.tagPromptBox != nil {183 if fs.tagPromptBox != nil {
184- clearBoxChildren(fs.tagPromptBox)184+ fs.clearTagPromptContent()
185 fs.tagPromptBox.SetVisible(false)185 fs.tagPromptBox.SetVisible(false)
186 fs.tagNameEntry = nil186 fs.tagNameEntry = nil
187 fs.tagPromptSaveBtn = nil187 fs.tagPromptSaveBtn = nil
M internal/ui/component/favorites_sidebar_keyboard.go +6 -1
@@ -507,7 +507,12 @@ func (fs *FavoritesSidebar) cycleTagControlFocus(reverse bool) bool {
507507 }
508508 index := fs.focusedTagControlIndex()
509509 if index < 0 {
510- index = 0
510+ if reverse {
511+ fs.focusTagControl(len(controls) - 1)
512+ } else {
513+ fs.focusTagControl(0)
514+ }
515+ return true
511516 }
512517 if reverse {
513518 if index > 0 {
@@ -507,7 +507,12 @@ func (fs *FavoritesSidebar) cycleTagControlFocus(reverse bool) bool {
507 }507 }
508 index := fs.focusedTagControlIndex()508 index := fs.focusedTagControlIndex()
509 if index < 0 {509 if index < 0 {
510- index = 0510+ if reverse {
511+ fs.focusTagControl(len(controls) - 1)
512+ } else {
513+ fs.focusTagControl(0)
514+ }
515+ return true
511 }516 }
512 if reverse {517 if reverse {
513 if index > 0 {518 if index > 0 {
M internal/ui/component/favorites_sidebar_tags.go +31 -6
@@ -12,7 +12,7 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() {
1212 if fs == nil || fs.tagPromptBox == nil {
1313 return
1414 }
15- clearBoxChildren(fs.tagPromptBox)
15+ fs.clearTagPromptContent()
1616 entry := gtk.NewEntry()
1717 if entry == nil {
1818 return
@@ -22,8 +22,9 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() {
2222 entry.AddCssClass("favorites-sidebar-tag-name")
2323 fs.trackTextInputFocus(&entry.Widget)
2424 entry.SetHexpand(true)
25+ callbacks := make([]any, 0, 2)
2526 activateCb := func(gtk.Entry) { fs.submitTagName(entry.GetText()) }
26- fs.retainedCallbacks = append(fs.retainedCallbacks, activateCb)
27+ callbacks = append(callbacks, activateCb)
2728 entry.ConnectActivate(&activateCb)
2829
2930 save := gtk.NewButtonWithLabel("Create")
@@ -32,11 +33,12 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() {
3233 }
3334 save.AddCssClass("favorites-sidebar-tag-create")
3435 cb := func(_ gtk.Button) { fs.createTag(entry.GetText()) }
35- fs.retainedCallbacks = append(fs.retainedCallbacks, cb)
36+ callbacks = append(callbacks, cb)
3637 save.ConnectClicked(&cb)
3738
3839 fs.tagNameEntry = entry
3940 fs.tagPromptSaveBtn = save
41+ fs.storeTagPromptCallbacks(callbacks)
4042 fs.tagPromptBox.Append(&entry.Widget)
4143 fs.tagPromptBox.Append(&save.Widget)
4244 fs.tagPromptBox.SetVisible(true)
@@ -88,7 +90,8 @@ func (fs *FavoritesSidebar) showTagBindingPicker() bool {
8890 fs.mu.RLock()
8991 tags := append([]*entity.Tag(nil), fs.allTags...)
9092 fs.mu.RUnlock()
91- clearBoxChildren(fs.tagPromptBox)
93+ fs.clearTagPromptContent()
94+ callbacks := make([]any, 0, len(tags))
9295 for _, tag := range tags {
9396 if tag == nil {
9497 continue
@@ -104,12 +107,13 @@ func (fs *FavoritesSidebar) showTagBindingPicker() bool {
104107 }
105108 button.AddCssClass("favorites-sidebar-tag-binding")
106109 cb := func(_ gtk.Button) { fs.toggleFavoriteTag(fav.ID, t.ID) }
107- fs.retainedCallbacks = append(fs.retainedCallbacks, cb)
110+ callbacks = append(callbacks, cb)
108111 button.ConnectClicked(&cb)
109112 fs.tagPromptBox.Append(&button.Widget)
110113 }
111114 fs.tagNameEntry = nil
112115 fs.tagPromptSaveBtn = nil
116+ fs.storeTagPromptCallbacks(callbacks)
113117 fs.tagPromptBox.SetVisible(true)
114118 fs.mu.Lock()
115119 if !fs.destroyed {
@@ -123,7 +127,7 @@ func (fs *FavoritesSidebar) hideTagPrompt() {
123127 if fs == nil || fs.tagPromptBox == nil {
124128 return
125129 }
126- clearBoxChildren(fs.tagPromptBox)
130+ fs.clearTagPromptContent()
127131 fs.tagPromptBox.SetVisible(false)
128132 fs.tagNameEntry = nil
129133 fs.tagPromptSaveBtn = nil
@@ -134,6 +138,27 @@ func (fs *FavoritesSidebar) hideTagPrompt() {
134138 fs.mu.Unlock()
135139 }
136140
141+func (fs *FavoritesSidebar) clearTagPromptContent() {
142+ if fs == nil || fs.tagPromptBox == nil {
143+ return
144+ }
145+ clearBoxChildren(fs.tagPromptBox)
146+ fs.mu.Lock()
147+ fs.tagPromptCallbacks = nil
148+ fs.mu.Unlock()
149+}
150+
151+func (fs *FavoritesSidebar) storeTagPromptCallbacks(callbacks []any) {
152+ if fs == nil {
153+ return
154+ }
155+ fs.mu.Lock()
156+ if !fs.destroyed {
157+ fs.tagPromptCallbacks = callbacks
158+ }
159+ fs.mu.Unlock()
160+}
161+
137162 func (fs *FavoritesSidebar) toggleFavoriteTag(favoriteID entity.FavoriteID, tagID entity.TagID) {
138163 fs.mu.RLock()
139164 uc, ctx := fs.favoritesUC, fs.ctx
@@ -12,7 +12,7 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() {
12 if fs == nil || fs.tagPromptBox == nil {12 if fs == nil || fs.tagPromptBox == nil {
13 return13 return
14 }14 }
15- clearBoxChildren(fs.tagPromptBox)15+ fs.clearTagPromptContent()
16 entry := gtk.NewEntry()16 entry := gtk.NewEntry()
17 if entry == nil {17 if entry == nil {
18 return18 return
@@ -22,8 +22,9 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() {
22 entry.AddCssClass("favorites-sidebar-tag-name")22 entry.AddCssClass("favorites-sidebar-tag-name")
23 fs.trackTextInputFocus(&entry.Widget)23 fs.trackTextInputFocus(&entry.Widget)
24 entry.SetHexpand(true)24 entry.SetHexpand(true)
25+ callbacks := make([]any, 0, 2)
25 activateCb := func(gtk.Entry) { fs.submitTagName(entry.GetText()) }26 activateCb := func(gtk.Entry) { fs.submitTagName(entry.GetText()) }
26- fs.retainedCallbacks = append(fs.retainedCallbacks, activateCb)27+ callbacks = append(callbacks, activateCb)
27 entry.ConnectActivate(&activateCb)28 entry.ConnectActivate(&activateCb)
28 29
29 save := gtk.NewButtonWithLabel("Create")30 save := gtk.NewButtonWithLabel("Create")
@@ -32,11 +33,12 @@ func (fs *FavoritesSidebar) showCreateTagPrompt() {
32 }33 }
33 save.AddCssClass("favorites-sidebar-tag-create")34 save.AddCssClass("favorites-sidebar-tag-create")
34 cb := func(_ gtk.Button) { fs.createTag(entry.GetText()) }35 cb := func(_ gtk.Button) { fs.createTag(entry.GetText()) }
35- fs.retainedCallbacks = append(fs.retainedCallbacks, cb)36+ callbacks = append(callbacks, cb)
36 save.ConnectClicked(&cb)37 save.ConnectClicked(&cb)
37 38
38 fs.tagNameEntry = entry39 fs.tagNameEntry = entry
39 fs.tagPromptSaveBtn = save40 fs.tagPromptSaveBtn = save
41+ fs.storeTagPromptCallbacks(callbacks)
40 fs.tagPromptBox.Append(&entry.Widget)42 fs.tagPromptBox.Append(&entry.Widget)
41 fs.tagPromptBox.Append(&save.Widget)43 fs.tagPromptBox.Append(&save.Widget)
42 fs.tagPromptBox.SetVisible(true)44 fs.tagPromptBox.SetVisible(true)
@@ -88,7 +90,8 @@ func (fs *FavoritesSidebar) showTagBindingPicker() bool {
88 fs.mu.RLock()90 fs.mu.RLock()
89 tags := append([]*entity.Tag(nil), fs.allTags...)91 tags := append([]*entity.Tag(nil), fs.allTags...)
90 fs.mu.RUnlock()92 fs.mu.RUnlock()
91- clearBoxChildren(fs.tagPromptBox)93+ fs.clearTagPromptContent()
94+ callbacks := make([]any, 0, len(tags))
92 for _, tag := range tags {95 for _, tag := range tags {
93 if tag == nil {96 if tag == nil {
94 continue97 continue
@@ -104,12 +107,13 @@ func (fs *FavoritesSidebar) showTagBindingPicker() bool {
104 }107 }
105 button.AddCssClass("favorites-sidebar-tag-binding")108 button.AddCssClass("favorites-sidebar-tag-binding")
106 cb := func(_ gtk.Button) { fs.toggleFavoriteTag(fav.ID, t.ID) }109 cb := func(_ gtk.Button) { fs.toggleFavoriteTag(fav.ID, t.ID) }
107- fs.retainedCallbacks = append(fs.retainedCallbacks, cb)110+ callbacks = append(callbacks, cb)
108 button.ConnectClicked(&cb)111 button.ConnectClicked(&cb)
109 fs.tagPromptBox.Append(&button.Widget)112 fs.tagPromptBox.Append(&button.Widget)
110 }113 }
111 fs.tagNameEntry = nil114 fs.tagNameEntry = nil
112 fs.tagPromptSaveBtn = nil115 fs.tagPromptSaveBtn = nil
116+ fs.storeTagPromptCallbacks(callbacks)
113 fs.tagPromptBox.SetVisible(true)117 fs.tagPromptBox.SetVisible(true)
114 fs.mu.Lock()118 fs.mu.Lock()
115 if !fs.destroyed {119 if !fs.destroyed {
@@ -123,7 +127,7 @@ func (fs *FavoritesSidebar) hideTagPrompt() {
123 if fs == nil || fs.tagPromptBox == nil {127 if fs == nil || fs.tagPromptBox == nil {
124 return128 return
125 }129 }
126- clearBoxChildren(fs.tagPromptBox)130+ fs.clearTagPromptContent()
127 fs.tagPromptBox.SetVisible(false)131 fs.tagPromptBox.SetVisible(false)
128 fs.tagNameEntry = nil132 fs.tagNameEntry = nil
129 fs.tagPromptSaveBtn = nil133 fs.tagPromptSaveBtn = nil
@@ -134,6 +138,27 @@ func (fs *FavoritesSidebar) hideTagPrompt() {
134 fs.mu.Unlock()138 fs.mu.Unlock()
135 }139 }
136 140
141+func (fs *FavoritesSidebar) clearTagPromptContent() {
142+ if fs == nil || fs.tagPromptBox == nil {
143+ return
144+ }
145+ clearBoxChildren(fs.tagPromptBox)
146+ fs.mu.Lock()
147+ fs.tagPromptCallbacks = nil
148+ fs.mu.Unlock()
149+}
150+
151+func (fs *FavoritesSidebar) storeTagPromptCallbacks(callbacks []any) {
152+ if fs == nil {
153+ return
154+ }
155+ fs.mu.Lock()
156+ if !fs.destroyed {
157+ fs.tagPromptCallbacks = callbacks
158+ }
159+ fs.mu.Unlock()
160+}
161+
137 func (fs *FavoritesSidebar) toggleFavoriteTag(favoriteID entity.FavoriteID, tagID entity.TagID) {162 func (fs *FavoritesSidebar) toggleFavoriteTag(favoriteID entity.FavoriteID, tagID entity.TagID) {
138 fs.mu.RLock()163 fs.mu.RLock()
139 uc, ctx := fs.favoritesUC, fs.ctx164 uc, ctx := fs.favoritesUC, fs.ctx
M internal/ui/theme/favorites_sidebar_css.go +0 -4
@@ -6,7 +6,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
66
77 .favorites-sidebar-tags {
88 padding: 0.25em 0.5em 0.375em;
9- spacing: 0.25em;
109 border-bottom: 0.0625em solid var(--border);
1110 background-color: var(--surface-variant);
1211 }
@@ -42,7 +41,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
4241
4342 .favorites-sidebar-tag-prompt {
4443 padding: 0.375em 0.5em;
45- spacing: 0.375em;
4644 border-bottom: 0.0625em solid var(--border);
4745 background-color: var(--surface-variant);
4846 }
@@ -83,7 +81,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
8381
8482 .favorites-sidebar-form {
8583 padding: 0.5em;
86- spacing: 0.375em;
8784 border-top: 0.0625em solid var(--border);
8885 background-color: var(--surface-variant);
8986 }
@@ -96,7 +93,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
9693 }
9794
9895 .favorites-sidebar-form-tag-matches {
99- spacing: 0.25em;
10096 padding: 0.125em 0;
10197 }
10298
@@ -6,7 +6,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
6 6
7 .favorites-sidebar-tags {7 .favorites-sidebar-tags {
8 padding: 0.25em 0.5em 0.375em;8 padding: 0.25em 0.5em 0.375em;
9- spacing: 0.25em;
10 border-bottom: 0.0625em solid var(--border);9 border-bottom: 0.0625em solid var(--border);
11 background-color: var(--surface-variant);10 background-color: var(--surface-variant);
12 }11 }
@@ -42,7 +41,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
42 41
43 .favorites-sidebar-tag-prompt {42 .favorites-sidebar-tag-prompt {
44 padding: 0.375em 0.5em;43 padding: 0.375em 0.5em;
45- spacing: 0.375em;
46 border-bottom: 0.0625em solid var(--border);44 border-bottom: 0.0625em solid var(--border);
47 background-color: var(--surface-variant);45 background-color: var(--surface-variant);
48 }46 }
@@ -83,7 +81,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
83 81
84 .favorites-sidebar-form {82 .favorites-sidebar-form {
85 padding: 0.5em;83 padding: 0.5em;
86- spacing: 0.375em;
87 border-top: 0.0625em solid var(--border);84 border-top: 0.0625em solid var(--border);
88 background-color: var(--surface-variant);85 background-color: var(--surface-variant);
89 }86 }
@@ -96,7 +93,6 @@ func generateFavoritesSidebarCSS(_ Palette) string {
96 }93 }
97 94
98 .favorites-sidebar-form-tag-matches {95 .favorites-sidebar-form-tag-matches {
99- spacing: 0.25em;
100 padding: 0.125em 0;96 padding: 0.125em 0;
101 }97 }
102 98

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