detached 79c0bb8

fix(ui): synchronize favorites sidebar notices

commit
79c0bb88eae9cba56a4783c64fa05e3dd7eec48e
author
brice · · 2026-07-14 07:02 UTC
parent
68e8ab6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +30 -11
M internal/ui/component/favorites_sidebar.go +15 -4
@@ -55,6 +55,7 @@ type FavoritesSidebar struct {
5555 displayRows []favoriteSidebarDisplayRow
5656 currentQuery string
5757 notice string
58+ noticeLoadGen uint64
5859 loadGen uint64
5960 destroyed bool
6061 visible bool
@@ -263,6 +264,7 @@ func (fs *FavoritesSidebar) startLoad() {
263264 ctx := fs.ctx
264265 visible := fs.visible
265266 fs.notice = "Loading favorites..."
267+ fs.noticeLoadGen = gen
266268 fs.mu.Unlock()
267269 if visible {
268270 fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool {
@@ -311,10 +313,13 @@ func (fs *FavoritesSidebar) applyLoadedData(favorites []*entity.Favorite, tags [
311313 }
312314 fs.allFavorites = favorites
313315 fs.allTags = tags
314- if err != nil {
315- fs.notice = err.Error()
316- } else {
317- fs.notice = ""
316+ if fs.noticeLoadGen == gen {
317+ fs.noticeLoadGen = 0
318+ if err != nil {
319+ fs.notice = err.Error()
320+ } else {
321+ fs.notice = ""
322+ }
318323 }
319324 fs.rebuildDisplayRowsLocked()
320325 fs.mu.Unlock()
@@ -363,6 +368,12 @@ func (fs *FavoritesSidebar) rebuildDisplayRowsLocked() {
363368 fs.displayRows = buildFavoriteSidebarDisplayRows(filtered)
364369 }
365370
371+// setNoticeLocked records a notice not owned by an in-flight load. fs.mu must be held.
372+func (fs *FavoritesSidebar) setNoticeLocked(notice string) {
373+ fs.notice = notice
374+ fs.noticeLoadGen = 0
375+}
376+
366377 func (fs *FavoritesSidebar) scheduleIdle(cb glib.SourceFunc) {
367378 if fs != nil && fs.idleScheduler != nil {
368379 fs.idleScheduler(cb)
@@ -55,6 +55,7 @@ type FavoritesSidebar struct {
55 displayRows []favoriteSidebarDisplayRow55 displayRows []favoriteSidebarDisplayRow
56 currentQuery string56 currentQuery string
57 notice string57 notice string
58+ noticeLoadGen uint64
58 loadGen uint6459 loadGen uint64
59 destroyed bool60 destroyed bool
60 visible bool61 visible bool
@@ -263,6 +264,7 @@ func (fs *FavoritesSidebar) startLoad() {
263 ctx := fs.ctx264 ctx := fs.ctx
264 visible := fs.visible265 visible := fs.visible
265 fs.notice = "Loading favorites..."266 fs.notice = "Loading favorites..."
267+ fs.noticeLoadGen = gen
266 fs.mu.Unlock()268 fs.mu.Unlock()
267 if visible {269 if visible {
268 fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool {270 fs.scheduleIdle(glib.SourceFunc(func(uintptr) bool {
@@ -311,10 +313,13 @@ func (fs *FavoritesSidebar) applyLoadedData(favorites []*entity.Favorite, tags [
311 }313 }
312 fs.allFavorites = favorites314 fs.allFavorites = favorites
313 fs.allTags = tags315 fs.allTags = tags
314- if err != nil {316+ if fs.noticeLoadGen == gen {
315- fs.notice = err.Error()317+ fs.noticeLoadGen = 0
316- } else {318+ if err != nil {
317- fs.notice = ""319+ fs.notice = err.Error()
320+ } else {
321+ fs.notice = ""
322+ }
318 }323 }
319 fs.rebuildDisplayRowsLocked()324 fs.rebuildDisplayRowsLocked()
320 fs.mu.Unlock()325 fs.mu.Unlock()
@@ -363,6 +368,12 @@ func (fs *FavoritesSidebar) rebuildDisplayRowsLocked() {
363 fs.displayRows = buildFavoriteSidebarDisplayRows(filtered)368 fs.displayRows = buildFavoriteSidebarDisplayRows(filtered)
364 }369 }
365 370
371+// setNoticeLocked records a notice not owned by an in-flight load. fs.mu must be held.
372+func (fs *FavoritesSidebar) setNoticeLocked(notice string) {
373+ fs.notice = notice
374+ fs.noticeLoadGen = 0
375+}
376+
366 func (fs *FavoritesSidebar) scheduleIdle(cb glib.SourceFunc) {377 func (fs *FavoritesSidebar) scheduleIdle(cb glib.SourceFunc) {
367 if fs != nil && fs.idleScheduler != nil {378 if fs != nil && fs.idleScheduler != nil {
368 fs.idleScheduler(cb)379 fs.idleScheduler(cb)
M internal/ui/component/favorites_sidebar_forms.go +6 -6
@@ -42,7 +42,7 @@ func (fs *FavoritesSidebar) beginAddForm() {
4242 fs.editingID = 0
4343 fs.confirmDelete = false
4444 fs.confirmDeleteID = 0
45- fs.notice = "Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save."
45+ fs.setNoticeLocked("Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save.")
4646 fs.mu.Unlock()
4747 fs.renderForm(nil)
4848 fs.rebuildList()
@@ -63,7 +63,7 @@ func (fs *FavoritesSidebar) beginEditForm() {
6363 fs.editingID = fav.ID
6464 fs.confirmDelete = false
6565 fs.confirmDeleteID = 0
66- fs.notice = "Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save."
66+ fs.setNoticeLocked("Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save.")
6767 fs.mu.Unlock()
6868 fs.renderForm(fav)
6969 fs.rebuildList()
@@ -170,7 +170,7 @@ func (fs *FavoritesSidebar) cancelManagement() bool {
170170 fs.confirmDelete = false
171171 fs.confirmDeleteID = 0
172172 if active {
173- fs.notice = ""
173+ fs.setNoticeLocked("")
174174 }
175175 fs.mu.Unlock()
176176 if fs.formBox != nil {
@@ -250,7 +250,7 @@ func (fs *FavoritesSidebar) setModeNotice(mode favoritesSidebarMode, notice stri
250250 fs.mode = mode
251251 fs.confirmDelete = false
252252 fs.confirmDeleteID = 0
253- fs.notice = notice
253+ fs.setNoticeLocked(notice)
254254 }
255255 fs.mu.Unlock()
256256 fs.rebuildList()
@@ -324,7 +324,7 @@ func (fs *FavoritesSidebar) requestDeleteConfirmation() bool {
324324 if !fs.destroyed {
325325 fs.confirmDelete = true
326326 fs.confirmDeleteID = fav.ID
327- fs.notice = "Delete selected favorite? Press Delete again to confirm, Esc to cancel."
327+ fs.setNoticeLocked("Delete selected favorite? Press Delete again to confirm, Esc to cancel.")
328328 }
329329 fs.mu.Unlock()
330330 fs.rebuildList()
@@ -396,7 +396,7 @@ func (fs *FavoritesSidebar) selectedFavorite() *entity.Favorite {
396396 func (fs *FavoritesSidebar) setNotice(notice string) {
397397 fs.mu.Lock()
398398 if !fs.destroyed {
399- fs.notice = notice
399+ fs.setNoticeLocked(notice)
400400 }
401401 fs.mu.Unlock()
402402 fs.rebuildList()
@@ -42,7 +42,7 @@ func (fs *FavoritesSidebar) beginAddForm() {
42 fs.editingID = 042 fs.editingID = 0
43 fs.confirmDelete = false43 fs.confirmDelete = false
44 fs.confirmDeleteID = 044 fs.confirmDeleteID = 0
45- fs.notice = "Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save."45+ fs.setNoticeLocked("Add favorite: URL, title, comma-separated tag IDs, shortcut 1-9. Press Ctrl+Enter or Save.")
46 fs.mu.Unlock()46 fs.mu.Unlock()
47 fs.renderForm(nil)47 fs.renderForm(nil)
48 fs.rebuildList()48 fs.rebuildList()
@@ -63,7 +63,7 @@ func (fs *FavoritesSidebar) beginEditForm() {
63 fs.editingID = fav.ID63 fs.editingID = fav.ID
64 fs.confirmDelete = false64 fs.confirmDelete = false
65 fs.confirmDeleteID = 065 fs.confirmDeleteID = 0
66- fs.notice = "Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save."66+ fs.setNoticeLocked("Edit favorite: URL is read-only; use tag mode for tags. Press Ctrl+Enter or Save.")
67 fs.mu.Unlock()67 fs.mu.Unlock()
68 fs.renderForm(fav)68 fs.renderForm(fav)
69 fs.rebuildList()69 fs.rebuildList()
@@ -170,7 +170,7 @@ func (fs *FavoritesSidebar) cancelManagement() bool {
170 fs.confirmDelete = false170 fs.confirmDelete = false
171 fs.confirmDeleteID = 0171 fs.confirmDeleteID = 0
172 if active {172 if active {
173- fs.notice = ""173+ fs.setNoticeLocked("")
174 }174 }
175 fs.mu.Unlock()175 fs.mu.Unlock()
176 if fs.formBox != nil {176 if fs.formBox != nil {
@@ -250,7 +250,7 @@ func (fs *FavoritesSidebar) setModeNotice(mode favoritesSidebarMode, notice stri
250 fs.mode = mode250 fs.mode = mode
251 fs.confirmDelete = false251 fs.confirmDelete = false
252 fs.confirmDeleteID = 0252 fs.confirmDeleteID = 0
253- fs.notice = notice253+ fs.setNoticeLocked(notice)
254 }254 }
255 fs.mu.Unlock()255 fs.mu.Unlock()
256 fs.rebuildList()256 fs.rebuildList()
@@ -324,7 +324,7 @@ func (fs *FavoritesSidebar) requestDeleteConfirmation() bool {
324 if !fs.destroyed {324 if !fs.destroyed {
325 fs.confirmDelete = true325 fs.confirmDelete = true
326 fs.confirmDeleteID = fav.ID326 fs.confirmDeleteID = fav.ID
327- fs.notice = "Delete selected favorite? Press Delete again to confirm, Esc to cancel."327+ fs.setNoticeLocked("Delete selected favorite? Press Delete again to confirm, Esc to cancel.")
328 }328 }
329 fs.mu.Unlock()329 fs.mu.Unlock()
330 fs.rebuildList()330 fs.rebuildList()
@@ -396,7 +396,7 @@ func (fs *FavoritesSidebar) selectedFavorite() *entity.Favorite {
396 func (fs *FavoritesSidebar) setNotice(notice string) {396 func (fs *FavoritesSidebar) setNotice(notice string) {
397 fs.mu.Lock()397 fs.mu.Lock()
398 if !fs.destroyed {398 if !fs.destroyed {
399- fs.notice = notice399+ fs.setNoticeLocked(notice)
400 }400 }
401 fs.mu.Unlock()401 fs.mu.Unlock()
402 fs.rebuildList()402 fs.rebuildList()
M internal/ui/component/favorites_sidebar_keyboard.go +1 -1
@@ -463,7 +463,7 @@ func (fs *FavoritesSidebar) handleNavigationError(err error) {
463463 fs.mu.Unlock()
464464 return
465465 }
466- fs.notice = err.Error()
466+ fs.setNoticeLocked(err.Error())
467467 fs.mu.Unlock()
468468 fs.rebuildList()
469469 }
@@ -463,7 +463,7 @@ func (fs *FavoritesSidebar) handleNavigationError(err error) {
463 fs.mu.Unlock()463 fs.mu.Unlock()
464 return464 return
465 }465 }
466- fs.notice = err.Error()466+ fs.setNoticeLocked(err.Error())
467 fs.mu.Unlock()467 fs.mu.Unlock()
468 fs.rebuildList()468 fs.rebuildList()
469 }469 }
M internal/ui/component/favorites_sidebar_test.go +8 -0
@@ -486,6 +486,8 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) {
486486 uc := &fakeFavoritesSidebarUC{errOnSetShortcut: errors.New("shortcut failed")}
487487 fs := newFavoritesSidebarHarness(nil, nil)
488488 fs.favoritesUC = uc
489+ loaded := make(chan glib.SourceFunc, 1)
490+ fs.idleScheduler = func(cb glib.SourceFunc) { loaded <- cb }
489491 fs.formURL = "https://add.test"
490492 fs.formTitle = "Added"
491493 fs.formShortcut = "3"
@@ -493,6 +495,12 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) {
493495
494496 assert.True(t, fs.submitForm())
495497 require.Len(t, uc.addInputs, 1)
498+ select {
499+ case cb := <-loaded:
500+ cb(0)
501+ case <-time.After(time.Second):
502+ t.Fatal("timed out waiting for favorites reload")
503+ }
496504 assert.Equal(t, favoritesSidebarModeNone, fs.mode)
497505 assert.Contains(t, fs.notice, "failed to set shortcut")
498506 }
@@ -486,6 +486,8 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) {
486 uc := &fakeFavoritesSidebarUC{errOnSetShortcut: errors.New("shortcut failed")}486 uc := &fakeFavoritesSidebarUC{errOnSetShortcut: errors.New("shortcut failed")}
487 fs := newFavoritesSidebarHarness(nil, nil)487 fs := newFavoritesSidebarHarness(nil, nil)
488 fs.favoritesUC = uc488 fs.favoritesUC = uc
489+ loaded := make(chan glib.SourceFunc, 1)
490+ fs.idleScheduler = func(cb glib.SourceFunc) { loaded <- cb }
489 fs.formURL = "https://add.test"491 fs.formURL = "https://add.test"
490 fs.formTitle = "Added"492 fs.formTitle = "Added"
491 fs.formShortcut = "3"493 fs.formShortcut = "3"
@@ -493,6 +495,12 @@ func TestFavoritesSidebarAddShortcutFailureClosesFormAfterCreate(t *testing.T) {
493 495
494 assert.True(t, fs.submitForm())496 assert.True(t, fs.submitForm())
495 require.Len(t, uc.addInputs, 1)497 require.Len(t, uc.addInputs, 1)
498+ select {
499+ case cb := <-loaded:
500+ cb(0)
501+ case <-time.After(time.Second):
502+ t.Fatal("timed out waiting for favorites reload")
503+ }
496 assert.Equal(t, favoritesSidebarModeNone, fs.mode)504 assert.Equal(t, favoritesSidebarModeNone, fs.mode)
497 assert.Contains(t, fs.notice, "failed to set shortcut")505 assert.Contains(t, fs.notice, "failed to set shortcut")
498 }506 }

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