detached 49e42b7

fix(favorites): satisfy sidebar lint

commit
49e42b7080b8fe2a20d15fa4add66153d1515894
author
brice · · 2026-06-28 16:24 UTC
parent
4991fbe
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +75 -50
M internal/application/usecase/favorite_sidebar_filter.go +16 -7
@@ -9,6 +9,15 @@ import (
99 "github.com/bnema/dumber/internal/domain/entity"
1010 )
1111
12+const (
13+ favoriteSidebarExactMatchScore = 100
14+ favoriteSidebarTitlePrefixScore = 90
15+ favoriteSidebarHostPrefixScore = 80
16+ favoriteSidebarTagNameScore = 70
17+ favoriteSidebarTitleContainsScore = 60
18+ favoriteSidebarURLContainsScore = 50
19+)
20+
1221 // FavoriteSidebarQuery describes the pure filtering inputs for the native favorites sidebar.
1322 type FavoriteSidebarQuery struct {
1423 Text string
@@ -118,17 +127,17 @@ func favoriteSidebarMatchScore(favorite *entity.Favorite, text string) (int, boo
118127
119128 switch {
120129 case title == text || favoriteURL == text:
121- return 100, true
130+ return favoriteSidebarExactMatchScore, true
122131 case strings.HasPrefix(title, text):
123- return 90, true
132+ return favoriteSidebarTitlePrefixScore, true
124133 case favoriteSidebarHostOrDomainHasPrefix(host, text):
125- return 80, true
134+ return favoriteSidebarHostPrefixScore, true
126135 case favoriteSidebarTagNameContains(favorite, text):
127- return 70, true
136+ return favoriteSidebarTagNameScore, true
128137 case strings.Contains(title, text):
129- return 60, true
138+ return favoriteSidebarTitleContainsScore, true
130139 case strings.Contains(favoriteURL, text):
131- return 50, true
140+ return favoriteSidebarURLContainsScore, true
132141 default:
133142 return 0, false
134143 }
@@ -143,7 +152,7 @@ func favoriteSidebarTagNameContains(favorite *entity.Favorite, text string) bool
143152 return false
144153 }
145154
146-func favoriteSidebarHostOrDomainHasPrefix(host string, text string) bool {
155+func favoriteSidebarHostOrDomainHasPrefix(host, text string) bool {
147156 host = strings.TrimPrefix(host, "www.")
148157 for host != "" {
149158 if strings.HasPrefix(host, text) {
@@ -9,6 +9,15 @@ import (
9 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
10 )10 )
11 11
12+const (
13+ favoriteSidebarExactMatchScore = 100
14+ favoriteSidebarTitlePrefixScore = 90
15+ favoriteSidebarHostPrefixScore = 80
16+ favoriteSidebarTagNameScore = 70
17+ favoriteSidebarTitleContainsScore = 60
18+ favoriteSidebarURLContainsScore = 50
19+)
20+
12 // FavoriteSidebarQuery describes the pure filtering inputs for the native favorites sidebar.21 // FavoriteSidebarQuery describes the pure filtering inputs for the native favorites sidebar.
13 type FavoriteSidebarQuery struct {22 type FavoriteSidebarQuery struct {
14 Text string23 Text string
@@ -118,17 +127,17 @@ func favoriteSidebarMatchScore(favorite *entity.Favorite, text string) (int, boo
118 127
119 switch {128 switch {
120 case title == text || favoriteURL == text:129 case title == text || favoriteURL == text:
121- return 100, true130+ return favoriteSidebarExactMatchScore, true
122 case strings.HasPrefix(title, text):131 case strings.HasPrefix(title, text):
123- return 90, true132+ return favoriteSidebarTitlePrefixScore, true
124 case favoriteSidebarHostOrDomainHasPrefix(host, text):133 case favoriteSidebarHostOrDomainHasPrefix(host, text):
125- return 80, true134+ return favoriteSidebarHostPrefixScore, true
126 case favoriteSidebarTagNameContains(favorite, text):135 case favoriteSidebarTagNameContains(favorite, text):
127- return 70, true136+ return favoriteSidebarTagNameScore, true
128 case strings.Contains(title, text):137 case strings.Contains(title, text):
129- return 60, true138+ return favoriteSidebarTitleContainsScore, true
130 case strings.Contains(favoriteURL, text):139 case strings.Contains(favoriteURL, text):
131- return 50, true140+ return favoriteSidebarURLContainsScore, true
132 default:141 default:
133 return 0, false142 return 0, false
134 }143 }
@@ -143,7 +152,7 @@ func favoriteSidebarTagNameContains(favorite *entity.Favorite, text string) bool
143 return false152 return false
144 }153 }
145 154
146-func favoriteSidebarHostOrDomainHasPrefix(host string, text string) bool {155+func favoriteSidebarHostOrDomainHasPrefix(host, text string) bool {
147 host = strings.TrimPrefix(host, "www.")156 host = strings.TrimPrefix(host, "www.")
148 for host != "" {157 for host != "" {
149 if strings.HasPrefix(host, text) {158 if strings.HasPrefix(host, text) {
M internal/application/usecase/favorite_sidebar_filter_test.go +1 -1
@@ -129,7 +129,7 @@ func TestFilterFavoritesForSidebarUsesPositionAndURLTieBreakers(t *testing.T) {
129129 assertFavoriteSidebarIDs(t, got, []entity.FavoriteID{3, 2, 1})
130130 }
131131
132-func favoriteSidebarTestFavorite(id entity.FavoriteID, rawURL string, title string, position int, tags ...entity.Tag) *entity.Favorite {
132+func favoriteSidebarTestFavorite(id entity.FavoriteID, rawURL, title string, position int, tags ...entity.Tag) *entity.Favorite {
133133 return &entity.Favorite{
134134 ID: id,
135135 URL: rawURL,
@@ -129,7 +129,7 @@ func TestFilterFavoritesForSidebarUsesPositionAndURLTieBreakers(t *testing.T) {
129 assertFavoriteSidebarIDs(t, got, []entity.FavoriteID{3, 2, 1})129 assertFavoriteSidebarIDs(t, got, []entity.FavoriteID{3, 2, 1})
130 }130 }
131 131
132-func favoriteSidebarTestFavorite(id entity.FavoriteID, rawURL string, title string, position int, tags ...entity.Tag) *entity.Favorite {132+func favoriteSidebarTestFavorite(id entity.FavoriteID, rawURL, title string, position int, tags ...entity.Tag) *entity.Favorite {
133 return &entity.Favorite{133 return &entity.Favorite{
134 ID: id,134 ID: id,
135 URL: rawURL,135 URL: rawURL,
M internal/ui/browser_window_favorites_sidebar.go +1 -0
@@ -31,6 +31,7 @@ func (bw *browserWindow) initFavoritesSidebar(ctx context.Context, a *App) {
3131 log.Debug().Msg("favorites sidebar initialized")
3232 }
3333
34+//nolint:dupl // Favorites and history sidebar configs intentionally mirror each other for separate component types.
3435 func (a *App) buildFavoritesSidebarConfig(bw *browserWindow) component.FavoritesSidebarConfig {
3536 var favoritesUC port.FavoritesSidebarFavorites
3637 if a.deps != nil {
@@ -31,6 +31,7 @@ func (bw *browserWindow) initFavoritesSidebar(ctx context.Context, a *App) {
31 log.Debug().Msg("favorites sidebar initialized")31 log.Debug().Msg("favorites sidebar initialized")
32 }32 }
33 33
34+//nolint:dupl // Favorites and history sidebar configs intentionally mirror each other for separate component types.
34 func (a *App) buildFavoritesSidebarConfig(bw *browserWindow) component.FavoritesSidebarConfig {35 func (a *App) buildFavoritesSidebarConfig(bw *browserWindow) component.FavoritesSidebarConfig {
35 var favoritesUC port.FavoritesSidebarFavorites36 var favoritesUC port.FavoritesSidebarFavorites
36 if a.deps != nil {37 if a.deps != nil {
M internal/ui/browser_window_favorites_sidebar_test.go +2 -2
@@ -112,10 +112,10 @@ func TestApp_ToggleCurrentPageFavoriteTogglesActiveWebViewURI(t *testing.T) {
112112
113113 func TestApp_ToggleCurrentPageFavoriteErrorCases(t *testing.T) {
114114 ctx := context.Background()
115- assert.ErrorContains(t, (&App{}).toggleCurrentPageFavoriteAction(ctx), "usecase not configured")
115+ require.ErrorContains(t, (&App{}).toggleCurrentPageFavoriteAction(ctx), "usecase not configured")
116116
117117 app := &App{deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(newMemoryFavoriteRepo(), &memoryTagRepo{})}}
118- assert.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "no active webview")
118+ require.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "no active webview")
119119
120120 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1")))
121121 tabs := entity.NewTabList()
@@ -112,10 +112,10 @@ func TestApp_ToggleCurrentPageFavoriteTogglesActiveWebViewURI(t *testing.T) {
112 112
113 func TestApp_ToggleCurrentPageFavoriteErrorCases(t *testing.T) {113 func TestApp_ToggleCurrentPageFavoriteErrorCases(t *testing.T) {
114 ctx := context.Background()114 ctx := context.Background()
115- assert.ErrorContains(t, (&App{}).toggleCurrentPageFavoriteAction(ctx), "usecase not configured")115+ require.ErrorContains(t, (&App{}).toggleCurrentPageFavoriteAction(ctx), "usecase not configured")
116 116
117 app := &App{deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(newMemoryFavoriteRepo(), &memoryTagRepo{})}}117 app := &App{deps: &Dependencies{FavoritesUC: usecase.NewManageFavoritesUseCase(newMemoryFavoriteRepo(), &memoryTagRepo{})}}
118- assert.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "no active webview")118+ require.ErrorContains(t, app.toggleCurrentPageFavoriteAction(ctx), "no active webview")
119 119
120 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1")))120 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1")))
121 tabs := entity.NewTabList()121 tabs := entity.NewTabList()
M internal/ui/browser_window_history_sidebar.go +2 -0
@@ -45,6 +45,8 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) {
4545
4646 // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given
4747 // browser window. Extracted from initHistorySidebar for testability.
48+//
49+//nolint:dupl // History and favorites sidebar configs intentionally mirror each other for separate component types.
4850 func (a *App) buildHistorySidebarConfig(bw *browserWindow) component.HistorySidebarConfig {
4951 var historyUC port.HistorySidebarHistory
5052 if a.deps != nil {
@@ -45,6 +45,8 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) {
45 45
46 // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given46 // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given
47 // browser window. Extracted from initHistorySidebar for testability.47 // browser window. Extracted from initHistorySidebar for testability.
48+//
49+//nolint:dupl // History and favorites sidebar configs intentionally mirror each other for separate component types.
48 func (a *App) buildHistorySidebarConfig(bw *browserWindow) component.HistorySidebarConfig {50 func (a *App) buildHistorySidebarConfig(bw *browserWindow) component.HistorySidebarConfig {
49 var historyUC port.HistorySidebarHistory51 var historyUC port.HistorySidebarHistory
50 if a.deps != nil {52 if a.deps != nil {
M internal/ui/component/favorites_sidebar.go +3 -1
@@ -13,6 +13,8 @@ import (
1313 "github.com/bnema/dumber/internal/domain/entity"
1414 )
1515
16+const favoritesSidebarOuterSpacing = 6
17+
1618 // FavoritesSidebarConfig holds configuration for creating a FavoritesSidebar.
1719 type FavoritesSidebarConfig struct {
1820 FavoritesUC port.FavoritesSidebarFavorites
@@ -152,7 +154,7 @@ func (fs *FavoritesSidebar) Destroy() {
152154 }
153155
154156 func (fs *FavoritesSidebar) createWidgets() error {
155- fs.outerBox = gtk.NewBox(gtk.OrientationVerticalValue, 6)
157+ fs.outerBox = gtk.NewBox(gtk.OrientationVerticalValue, favoritesSidebarOuterSpacing)
156158 if fs.outerBox == nil {
157159 return fmt.Errorf("favorites sidebar: outer box creation failed")
158160 }
@@ -13,6 +13,8 @@ import (
13 "github.com/bnema/dumber/internal/domain/entity"13 "github.com/bnema/dumber/internal/domain/entity"
14 )14 )
15 15
16+const favoritesSidebarOuterSpacing = 6
17+
16 // FavoritesSidebarConfig holds configuration for creating a FavoritesSidebar.18 // FavoritesSidebarConfig holds configuration for creating a FavoritesSidebar.
17 type FavoritesSidebarConfig struct {19 type FavoritesSidebarConfig struct {
18 FavoritesUC port.FavoritesSidebarFavorites20 FavoritesUC port.FavoritesSidebarFavorites
@@ -152,7 +154,7 @@ func (fs *FavoritesSidebar) Destroy() {
152 }154 }
153 155
154 func (fs *FavoritesSidebar) createWidgets() error {156 func (fs *FavoritesSidebar) createWidgets() error {
155- fs.outerBox = gtk.NewBox(gtk.OrientationVerticalValue, 6)157+ fs.outerBox = gtk.NewBox(gtk.OrientationVerticalValue, favoritesSidebarOuterSpacing)
156 if fs.outerBox == nil {158 if fs.outerBox == nil {
157 return fmt.Errorf("favorites sidebar: outer box creation failed")159 return fmt.Errorf("favorites sidebar: outer box creation failed")
158 }160 }
M internal/ui/component/favorites_sidebar_keyboard.go +4 -3
@@ -8,6 +8,7 @@ import (
88 "github.com/bnema/dumber/internal/domain/entity"
99 )
1010
11+//nolint:gocyclo,funlen // Centralized local key routing keeps sidebar keyboard behavior easy to audit.
1112 func (fs *FavoritesSidebar) setupKeyboardNavigation() {
1213 if fs == nil || fs.outerBox == nil {
1314 return
@@ -73,7 +74,7 @@ func (fs *FavoritesSidebar) setupKeyboardNavigation() {
7374 if fs.inTextEditContext() {
7475 return false
7576 }
76- searchFocused := searchEntry != nil && searchEntry.Widget.HasFocus()
77+ searchFocused := searchEntry != nil && searchEntry.HasFocus()
7778 if !shouldFocusSearchForSlash(searchFocused) {
7879 return false
7980 }
@@ -263,11 +264,11 @@ func (fs *FavoritesSidebar) inTextEditContext() bool {
263264 if fs == nil {
264265 return false
265266 }
266- if fs.searchEntry != nil && fs.searchEntry.Widget.HasFocus() {
267+ if fs.searchEntry != nil && fs.searchEntry.HasFocus() {
267268 return true
268269 }
269270 for _, entry := range []*gtk.SearchEntry{fs.formURLEntry, fs.formTitleEntry, fs.formTagsEntry, fs.formShortcutEntry} {
270- if entry != nil && entry.Widget.HasFocus() {
271+ if entry != nil && entry.HasFocus() {
271272 return true
272273 }
273274 }
@@ -8,6 +8,7 @@ import (
8 "github.com/bnema/dumber/internal/domain/entity"8 "github.com/bnema/dumber/internal/domain/entity"
9 )9 )
10 10
11+//nolint:gocyclo,funlen // Centralized local key routing keeps sidebar keyboard behavior easy to audit.
11 func (fs *FavoritesSidebar) setupKeyboardNavigation() {12 func (fs *FavoritesSidebar) setupKeyboardNavigation() {
12 if fs == nil || fs.outerBox == nil {13 if fs == nil || fs.outerBox == nil {
13 return14 return
@@ -73,7 +74,7 @@ func (fs *FavoritesSidebar) setupKeyboardNavigation() {
73 if fs.inTextEditContext() {74 if fs.inTextEditContext() {
74 return false75 return false
75 }76 }
76- searchFocused := searchEntry != nil && searchEntry.Widget.HasFocus()77+ searchFocused := searchEntry != nil && searchEntry.HasFocus()
77 if !shouldFocusSearchForSlash(searchFocused) {78 if !shouldFocusSearchForSlash(searchFocused) {
78 return false79 return false
79 }80 }
@@ -263,11 +264,11 @@ func (fs *FavoritesSidebar) inTextEditContext() bool {
263 if fs == nil {264 if fs == nil {
264 return false265 return false
265 }266 }
266- if fs.searchEntry != nil && fs.searchEntry.Widget.HasFocus() {267+ if fs.searchEntry != nil && fs.searchEntry.HasFocus() {
267 return true268 return true
268 }269 }
269 for _, entry := range []*gtk.SearchEntry{fs.formURLEntry, fs.formTitleEntry, fs.formTagsEntry, fs.formShortcutEntry} {270 for _, entry := range []*gtk.SearchEntry{fs.formURLEntry, fs.formTitleEntry, fs.formTagsEntry, fs.formShortcutEntry} {
270- if entry != nil && entry.Widget.HasFocus() {271+ if entry != nil && entry.HasFocus() {
271 return true272 return true
272 }273 }
273 }274 }
M internal/ui/component/favorites_sidebar_rows.go +41 -31
@@ -234,7 +234,24 @@ func (fs *FavoritesSidebar) appendFavoriteRow(listBox *gtk.ListBox, fav *entity.
234234 return
235235 }
236236 rowBox.SetHexpand(true)
237+ appendFavoriteRowTitle(rowBox, fav)
238+ appendFavoriteRowSubtitle(rowBox, fav)
239+ appendFavoriteRowTags(rowBox, fav.Tags)
237240
241+ row := gtk.NewListBoxRow()
242+ if row == nil {
243+ return
244+ }
245+ row.AddCssClass("favorites-sidebar-row")
246+ row.SetSelectable(true)
247+ row.SetActivatable(true)
248+ row.SetCanFocus(true)
249+ row.SetFocusOnClick(true)
250+ row.SetChild(&rowBox.Widget)
251+ listBox.Append(&row.Widget)
252+}
253+
254+func appendFavoriteRowTitle(rowBox *gtk.Box, fav *entity.Favorite) {
238255 title := gtk.NewLabel(nil)
239256 if title == nil {
240257 return
@@ -245,22 +262,23 @@ func (fs *FavoritesSidebar) appendFavoriteRow(listBox *gtk.ListBox, fav *entity.
245262 title.SetHexpand(true)
246263 title.SetEllipsize(pango.EllipsizeEndValue)
247264 rowBox.Append(&title.Widget)
265+}
248266
267+func appendFavoriteRowSubtitle(rowBox *gtk.Box, fav *entity.Favorite) {
249268 sub := gtk.NewBox(gtk.OrientationHorizontalValue, 4)
250269 if sub == nil {
251270 return
252271 }
253272 sub.SetHexpand(true)
254273 url := gtk.NewLabel(nil)
255- if url == nil {
256- return
274+ if url != nil {
275+ url.AddCssClass("favorites-sidebar-row-subtitle")
276+ url.SetText(readableURL(fav.URL))
277+ url.SetXalign(0.0)
278+ url.SetHexpand(true)
279+ url.SetEllipsize(pango.EllipsizeEndValue)
280+ sub.Append(&url.Widget)
257281 }
258- url.AddCssClass("favorites-sidebar-row-subtitle")
259- url.SetText(readableURL(fav.URL))
260- url.SetXalign(0.0)
261- url.SetHexpand(true)
262- url.SetEllipsize(pango.EllipsizeEndValue)
263- sub.Append(&url.Widget)
264282 if fav.ShortcutKey != nil {
265283 badge := fmt.Sprintf("Shortcut %d", *fav.ShortcutKey)
266284 badgeLabel := gtk.NewLabel(&badge)
@@ -270,33 +288,25 @@ func (fs *FavoritesSidebar) appendFavoriteRow(listBox *gtk.ListBox, fav *entity.
270288 }
271289 }
272290 rowBox.Append(&sub.Widget)
291+}
273292
274- if len(fav.Tags) > 0 {
275- tags := gtk.NewBox(gtk.OrientationHorizontalValue, 3)
276- if tags != nil {
277- for _, tag := range fav.Tags {
278- name := tag.Name
279- label := gtk.NewLabel(&name)
280- if label != nil {
281- label.AddCssClass("favorites-sidebar-tag-chip")
282- tags.Append(&label.Widget)
283- }
284- }
285- rowBox.Append(&tags.Widget)
286- }
293+func appendFavoriteRowTags(rowBox *gtk.Box, favoriteTags []entity.Tag) {
294+ if len(favoriteTags) == 0 {
295+ return
287296 }
288-
289- row := gtk.NewListBoxRow()
290- if row == nil {
297+ tags := gtk.NewBox(gtk.OrientationHorizontalValue, 3)
298+ if tags == nil {
291299 return
292300 }
293- row.AddCssClass("favorites-sidebar-row")
294- row.SetSelectable(true)
295- row.SetActivatable(true)
296- row.SetCanFocus(true)
297- row.SetFocusOnClick(true)
298- row.SetChild(&rowBox.Widget)
299- listBox.Append(&row.Widget)
301+ for _, tag := range favoriteTags {
302+ name := tag.Name
303+ label := gtk.NewLabel(&name)
304+ if label != nil {
305+ label.AddCssClass("favorites-sidebar-tag-chip")
306+ tags.Append(&label.Widget)
307+ }
308+ }
309+ rowBox.Append(&tags.Widget)
300310 }
301311
302312 func (fs *FavoritesSidebar) ensureAtLeastOneSelectionInListBox(listBox *gtk.ListBox) {
@@ -234,7 +234,24 @@ func (fs *FavoritesSidebar) appendFavoriteRow(listBox *gtk.ListBox, fav *entity.
234 return234 return
235 }235 }
236 rowBox.SetHexpand(true)236 rowBox.SetHexpand(true)
237+ appendFavoriteRowTitle(rowBox, fav)
238+ appendFavoriteRowSubtitle(rowBox, fav)
239+ appendFavoriteRowTags(rowBox, fav.Tags)
237 240
241+ row := gtk.NewListBoxRow()
242+ if row == nil {
243+ return
244+ }
245+ row.AddCssClass("favorites-sidebar-row")
246+ row.SetSelectable(true)
247+ row.SetActivatable(true)
248+ row.SetCanFocus(true)
249+ row.SetFocusOnClick(true)
250+ row.SetChild(&rowBox.Widget)
251+ listBox.Append(&row.Widget)
252+}
253+
254+func appendFavoriteRowTitle(rowBox *gtk.Box, fav *entity.Favorite) {
238 title := gtk.NewLabel(nil)255 title := gtk.NewLabel(nil)
239 if title == nil {256 if title == nil {
240 return257 return
@@ -245,22 +262,23 @@ func (fs *FavoritesSidebar) appendFavoriteRow(listBox *gtk.ListBox, fav *entity.
245 title.SetHexpand(true)262 title.SetHexpand(true)
246 title.SetEllipsize(pango.EllipsizeEndValue)263 title.SetEllipsize(pango.EllipsizeEndValue)
247 rowBox.Append(&title.Widget)264 rowBox.Append(&title.Widget)
265+}
248 266
267+func appendFavoriteRowSubtitle(rowBox *gtk.Box, fav *entity.Favorite) {
249 sub := gtk.NewBox(gtk.OrientationHorizontalValue, 4)268 sub := gtk.NewBox(gtk.OrientationHorizontalValue, 4)
250 if sub == nil {269 if sub == nil {
251 return270 return
252 }271 }
253 sub.SetHexpand(true)272 sub.SetHexpand(true)
254 url := gtk.NewLabel(nil)273 url := gtk.NewLabel(nil)
255- if url == nil {274+ if url != nil {
256- return275+ url.AddCssClass("favorites-sidebar-row-subtitle")
276+ url.SetText(readableURL(fav.URL))
277+ url.SetXalign(0.0)
278+ url.SetHexpand(true)
279+ url.SetEllipsize(pango.EllipsizeEndValue)
280+ sub.Append(&url.Widget)
257 }281 }
258- url.AddCssClass("favorites-sidebar-row-subtitle")
259- url.SetText(readableURL(fav.URL))
260- url.SetXalign(0.0)
261- url.SetHexpand(true)
262- url.SetEllipsize(pango.EllipsizeEndValue)
263- sub.Append(&url.Widget)
264 if fav.ShortcutKey != nil {282 if fav.ShortcutKey != nil {
265 badge := fmt.Sprintf("Shortcut %d", *fav.ShortcutKey)283 badge := fmt.Sprintf("Shortcut %d", *fav.ShortcutKey)
266 badgeLabel := gtk.NewLabel(&badge)284 badgeLabel := gtk.NewLabel(&badge)
@@ -270,33 +288,25 @@ func (fs *FavoritesSidebar) appendFavoriteRow(listBox *gtk.ListBox, fav *entity.
270 }288 }
271 }289 }
272 rowBox.Append(&sub.Widget)290 rowBox.Append(&sub.Widget)
291+}
273 292
274- if len(fav.Tags) > 0 {293+func appendFavoriteRowTags(rowBox *gtk.Box, favoriteTags []entity.Tag) {
275- tags := gtk.NewBox(gtk.OrientationHorizontalValue, 3)294+ if len(favoriteTags) == 0 {
276- if tags != nil {295+ return
277- for _, tag := range fav.Tags {
278- name := tag.Name
279- label := gtk.NewLabel(&name)
280- if label != nil {
281- label.AddCssClass("favorites-sidebar-tag-chip")
282- tags.Append(&label.Widget)
283- }
284- }
285- rowBox.Append(&tags.Widget)
286- }
287 }296 }
288-297+ tags := gtk.NewBox(gtk.OrientationHorizontalValue, 3)
289- row := gtk.NewListBoxRow()298+ if tags == nil {
290- if row == nil {
291 return299 return
292 }300 }
293- row.AddCssClass("favorites-sidebar-row")301+ for _, tag := range favoriteTags {
294- row.SetSelectable(true)302+ name := tag.Name
295- row.SetActivatable(true)303+ label := gtk.NewLabel(&name)
296- row.SetCanFocus(true)304+ if label != nil {
297- row.SetFocusOnClick(true)305+ label.AddCssClass("favorites-sidebar-tag-chip")
298- row.SetChild(&rowBox.Widget)306+ tags.Append(&label.Widget)
299- listBox.Append(&row.Widget)307+ }
308+ }
309+ rowBox.Append(&tags.Widget)
300 }310 }
301 311
302 func (fs *FavoritesSidebar) ensureAtLeastOneSelectionInListBox(listBox *gtk.ListBox) {312 func (fs *FavoritesSidebar) ensureAtLeastOneSelectionInListBox(listBox *gtk.ListBox) {
M internal/ui/component/favorites_sidebar_test.go +1 -1
@@ -230,7 +230,7 @@ func TestFavoritesSidebarRenderingModelTagCallbackRetentionAndListRows(t *testin
230230 {ID: 2, URL: "", Title: "Missing URL"},
231231 })
232232
233- assert.Equal(t, 2, len(fs.allTags))
233+ assert.Len(t, fs.allTags, 2)
234234 require.Len(t, fs.displayRows, 2)
235235 assert.True(t, fs.displayRows[0].Selectable)
236236 assert.False(t, fs.displayRows[1].Selectable)
@@ -230,7 +230,7 @@ func TestFavoritesSidebarRenderingModelTagCallbackRetentionAndListRows(t *testin
230 {ID: 2, URL: "", Title: "Missing URL"},230 {ID: 2, URL: "", Title: "Missing URL"},
231 })231 })
232 232
233- assert.Equal(t, 2, len(fs.allTags))233+ assert.Len(t, fs.allTags, 2)
234 require.Len(t, fs.displayRows, 2)234 require.Len(t, fs.displayRows, 2)
235 assert.True(t, fs.displayRows[0].Selectable)235 assert.True(t, fs.displayRows[0].Selectable)
236 assert.False(t, fs.displayRows[1].Selectable)236 assert.False(t, fs.displayRows[1].Selectable)
M internal/ui/dispatcher/keyboard_test.go +4 -4
@@ -121,13 +121,13 @@ func TestKeyboardDispatcher_ToggleFavoritesSidebarCallsCallbackAndPropagatesErro
121121
122122 missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)
123123 require.Error(t, missingErr)
124- assert.ErrorContains(t, missingErr, "favorites sidebar unavailable")
124+ require.ErrorContains(t, missingErr, "favorites sidebar unavailable")
125125
126126 wantErr := fmt.Errorf("favorites failed")
127127 d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr })
128128 err := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)
129129 require.Error(t, err)
130- assert.ErrorIs(t, err, wantErr)
130+ require.ErrorIs(t, err, wantErr)
131131
132132 var called bool
133133 d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil })
@@ -141,13 +141,13 @@ func TestKeyboardDispatcher_ToggleCurrentPageFavoriteCallsCallbackAndPropagatesE
141141
142142 missingErr := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)
143143 require.Error(t, missingErr)
144- assert.ErrorContains(t, missingErr, "toggle current page handler not wired")
144+ require.ErrorContains(t, missingErr, "toggle current page handler not wired")
145145
146146 wantErr := fmt.Errorf("toggle failed")
147147 d.SetOnToggleCurrentPageFavorite(func(context.Context) error { return wantErr })
148148 err := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)
149149 require.Error(t, err)
150- assert.ErrorIs(t, err, wantErr)
150+ require.ErrorIs(t, err, wantErr)
151151
152152 var called bool
153153 d.SetOnToggleCurrentPageFavorite(func(context.Context) error { called = true; return nil })
@@ -121,13 +121,13 @@ func TestKeyboardDispatcher_ToggleFavoritesSidebarCallsCallbackAndPropagatesErro
121 121
122 missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)122 missingErr := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)
123 require.Error(t, missingErr)123 require.Error(t, missingErr)
124- assert.ErrorContains(t, missingErr, "favorites sidebar unavailable")124+ require.ErrorContains(t, missingErr, "favorites sidebar unavailable")
125 125
126 wantErr := fmt.Errorf("favorites failed")126 wantErr := fmt.Errorf("favorites failed")
127 d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr })127 d.SetOnToggleFavoritesSidebar(func(context.Context) error { return wantErr })
128 err := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)128 err := d.Dispatch(ctx, input.ActionToggleFavoritesSystemView)
129 require.Error(t, err)129 require.Error(t, err)
130- assert.ErrorIs(t, err, wantErr)130+ require.ErrorIs(t, err, wantErr)
131 131
132 var called bool132 var called bool
133 d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil })133 d.SetOnToggleFavoritesSidebar(func(context.Context) error { called = true; return nil })
@@ -141,13 +141,13 @@ func TestKeyboardDispatcher_ToggleCurrentPageFavoriteCallsCallbackAndPropagatesE
141 141
142 missingErr := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)142 missingErr := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)
143 require.Error(t, missingErr)143 require.Error(t, missingErr)
144- assert.ErrorContains(t, missingErr, "toggle current page handler not wired")144+ require.ErrorContains(t, missingErr, "toggle current page handler not wired")
145 145
146 wantErr := fmt.Errorf("toggle failed")146 wantErr := fmt.Errorf("toggle failed")
147 d.SetOnToggleCurrentPageFavorite(func(context.Context) error { return wantErr })147 d.SetOnToggleCurrentPageFavorite(func(context.Context) error { return wantErr })
148 err := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)148 err := d.Dispatch(ctx, input.ActionToggleCurrentPageFavorite)
149 require.Error(t, err)149 require.Error(t, err)
150- assert.ErrorIs(t, err, wantErr)150+ require.ErrorIs(t, err, wantErr)
151 151
152 var called bool152 var called bool
153 d.SetOnToggleCurrentPageFavorite(func(context.Context) error { called = true; return nil })153 d.SetOnToggleCurrentPageFavorite(func(context.Context) error { called = true; return nil })

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