detached 1ee4e97

fix(history): clear PR blocker findings

commit
1ee4e97d496147797e0a36aede1f99d2da12e28e
author
brice · · 2026-06-13 09:17 UTC
parent
933fcaf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +90 -79
M internal/ui/browser_window_history_sidebar.go +7 -12
@@ -19,7 +19,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) {
1919 }
2020 log := logging.FromContext(ctx)
2121
22- cfg := a.buildHistorySidebarConfig(ctx, bw)
22+ cfg := a.buildHistorySidebarConfig(bw)
2323
2424 sidebar := component.NewHistorySidebar(ctx, cfg)
2525 if sidebar == nil {
@@ -42,7 +42,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) {
4242
4343 // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given
4444 // browser window. Extracted from initHistorySidebar for testability.
45-func (a *App) buildHistorySidebarConfig(ctx context.Context, bw *browserWindow) component.HistorySidebarConfig {
45+func (a *App) buildHistorySidebarConfig(bw *browserWindow) component.HistorySidebarConfig {
4646 var historyUC port.HistorySidebarHistory
4747 if a.deps != nil {
4848 historyUC = a.deps.HistoryUC
@@ -76,9 +76,8 @@ func (a *App) navigateHistorySidebarSelection(ctx context.Context, bw *browserWi
7676 return a.navigateFromBrowserWindow(ctx, bw, url)
7777 }
7878
79-// toggleHistorySidebar toggles sidebar visibility. An optional width config
80-// can be provided and is applied when showing the sidebar.
81-func (bw *browserWindow) toggleHistorySidebar(widthCfg ...window.SidebarWidthConfig) {
79+// toggleHistorySidebar toggles sidebar visibility.
80+func (bw *browserWindow) toggleHistorySidebar() {
8281 if bw == nil || bw.historySidebar == nil {
8382 return
8483 }
@@ -86,20 +85,16 @@ func (bw *browserWindow) toggleHistorySidebar(widthCfg ...window.SidebarWidthCon
8685 if bw.sidebarVisible {
8786 bw.hideHistorySidebar()
8887 } else {
89- bw.showHistorySidebar(widthCfg...)
88+ bw.showHistorySidebar()
9089 }
9190 }
9291
9392 // showHistorySidebar makes the sidebar visible and grabs focus for the search
94-// entry. An optional width config can be provided to override the default width.
95-func (bw *browserWindow) showHistorySidebar(widthCfg ...window.SidebarWidthConfig) {
93+// entry.
94+func (bw *browserWindow) showHistorySidebar() {
9695 if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil {
9796 return
9897 }
99- // Apply width config if provided
100- if len(widthCfg) > 0 {
101- bw.mainWindow.SetSidebarWidth(widthCfg[0])
102- }
10398 bw.historySidebar.Show()
10499 bw.mainWindow.SetSidebarVisible(true)
105100 bw.sidebarVisible = true
@@ -19,7 +19,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) {
19 }19 }
20 log := logging.FromContext(ctx)20 log := logging.FromContext(ctx)
21 21
22- cfg := a.buildHistorySidebarConfig(ctx, bw)22+ cfg := a.buildHistorySidebarConfig(bw)
23 23
24 sidebar := component.NewHistorySidebar(ctx, cfg)24 sidebar := component.NewHistorySidebar(ctx, cfg)
25 if sidebar == nil {25 if sidebar == nil {
@@ -42,7 +42,7 @@ func (bw *browserWindow) initHistorySidebar(ctx context.Context, a *App) {
42 42
43 // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given43 // buildHistorySidebarConfig constructs the HistorySidebarConfig for the given
44 // browser window. Extracted from initHistorySidebar for testability.44 // browser window. Extracted from initHistorySidebar for testability.
45-func (a *App) buildHistorySidebarConfig(ctx context.Context, bw *browserWindow) component.HistorySidebarConfig {45+func (a *App) buildHistorySidebarConfig(bw *browserWindow) component.HistorySidebarConfig {
46 var historyUC port.HistorySidebarHistory46 var historyUC port.HistorySidebarHistory
47 if a.deps != nil {47 if a.deps != nil {
48 historyUC = a.deps.HistoryUC48 historyUC = a.deps.HistoryUC
@@ -76,9 +76,8 @@ func (a *App) navigateHistorySidebarSelection(ctx context.Context, bw *browserWi
76 return a.navigateFromBrowserWindow(ctx, bw, url)76 return a.navigateFromBrowserWindow(ctx, bw, url)
77 }77 }
78 78
79-// toggleHistorySidebar toggles sidebar visibility. An optional width config79+// toggleHistorySidebar toggles sidebar visibility.
80-// can be provided and is applied when showing the sidebar.80+func (bw *browserWindow) toggleHistorySidebar() {
81-func (bw *browserWindow) toggleHistorySidebar(widthCfg ...window.SidebarWidthConfig) {
82 if bw == nil || bw.historySidebar == nil {81 if bw == nil || bw.historySidebar == nil {
83 return82 return
84 }83 }
@@ -86,20 +85,16 @@ func (bw *browserWindow) toggleHistorySidebar(widthCfg ...window.SidebarWidthCon
86 if bw.sidebarVisible {85 if bw.sidebarVisible {
87 bw.hideHistorySidebar()86 bw.hideHistorySidebar()
88 } else {87 } else {
89- bw.showHistorySidebar(widthCfg...)88+ bw.showHistorySidebar()
90 }89 }
91 }90 }
92 91
93 // showHistorySidebar makes the sidebar visible and grabs focus for the search92 // showHistorySidebar makes the sidebar visible and grabs focus for the search
94-// entry. An optional width config can be provided to override the default width.93+// entry.
95-func (bw *browserWindow) showHistorySidebar(widthCfg ...window.SidebarWidthConfig) {94+func (bw *browserWindow) showHistorySidebar() {
96 if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil {95 if bw == nil || bw.historySidebar == nil || bw.mainWindow == nil {
97 return96 return
98 }97 }
99- // Apply width config if provided
100- if len(widthCfg) > 0 {
101- bw.mainWindow.SetSidebarWidth(widthCfg[0])
102- }
103 bw.historySidebar.Show()98 bw.historySidebar.Show()
104 bw.mainWindow.SetSidebarVisible(true)99 bw.mainWindow.SetSidebarVisible(true)
105 bw.sidebarVisible = true100 bw.sidebarVisible = true
M internal/ui/browser_window_history_sidebar_test.go +11 -16
@@ -69,7 +69,7 @@ func TestHistorySidebarConfig_OnNavigateNavigatesActivePaneAndKeepsSidebar(t *te
6969 second.historySidebar = &component.HistorySidebar{}
7070 second.sidebarVisible = true
7171
72- cfg := app.buildHistorySidebarConfig(ctx, second)
72+ cfg := app.buildHistorySidebarConfig(second)
7373 navigateURL := "https://example.com"
7474 err := cfg.OnNavigate(ctx, navigateURL)
7575 require.NoError(t, err, "OnNavigate should succeed")
@@ -118,7 +118,7 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
118118 }
119119 app.tabs.Add(tab)
120120
121- cfg := app.buildHistorySidebarConfig(ctx, bw)
121+ cfg := app.buildHistorySidebarConfig(bw)
122122 navigateURL := "https://keep-open.com"
123123 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)
124124 require.NoError(t, err)
@@ -346,7 +346,6 @@ func TestApp_HistorySidebarToggleHandler_NilFocusedWindowIsNoOp(t *testing.T) {
346346 // TestHistorySidebarConfig_OnCloseHidesSidebar verifies that the OnClose
347347 // callback hides the sidebar for the owning browser window.
348348 func TestHistorySidebarConfig_OnCloseHidesSidebar(t *testing.T) {
349- ctx := t.Context()
350349 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1")))
351350 bwTabs := entity.NewTabList()
352351 bwTabs.Add(tab)
@@ -367,7 +366,7 @@ func TestHistorySidebarConfig_OnCloseHidesSidebar(t *testing.T) {
367366 }
368367 app.tabs.Add(tab)
369368
370- cfg := app.buildHistorySidebarConfig(ctx, bw)
369+ cfg := app.buildHistorySidebarConfig(bw)
371370 cfg.OnClose()
372371
373372 assert.False(t, bw.sidebarVisible, "sidebar must be hidden by OnClose")
@@ -456,7 +455,7 @@ func TestApp_HistorySidebar_ToggleThroughDispatcher_UnavailableReturnsError(t *t
456455
457456 err := kbDispatcher.Dispatch(ctx, input.ActionToggleHistorySystemView)
458457 require.Error(t, err)
459- assert.ErrorContains(t, err, "history sidebar unavailable")
458+ require.ErrorContains(t, err, "history sidebar unavailable")
460459 assert.False(t, bw.sidebarVisible, "sidebar must remain invisible when not wired")
461460 }
462461
@@ -483,7 +482,7 @@ func TestApp_HistorySidebar_ToggleThroughDispatcher_NilFocusedReturnsError(t *te
483482
484483 err := kbDispatcher.Dispatch(ctx, input.ActionToggleHistorySystemView)
485484 require.Error(t, err)
486- assert.ErrorContains(t, err, "history sidebar unavailable")
485+ require.ErrorContains(t, err, "history sidebar unavailable")
487486 }
488487
489488 // =============================================================================
@@ -527,7 +526,7 @@ func TestApp_HistorySidebarConfig_NavigateCallbackNavigates(t *testing.T) {
527526 app.tabs.Add(tab)
528527
529528 // Build the config using the extracted seam.
530- cfg := app.buildHistorySidebarConfig(ctx, bw)
529+ cfg := app.buildHistorySidebarConfig(bw)
531530 require.NotNil(t, cfg.OnNavigate, "OnNavigate callback must be non-nil")
532531
533532 // Invoke the OnNavigate callback.
@@ -623,7 +622,7 @@ func TestApp_HistorySidebarConfig_NavigateCallbackOwnership(t *testing.T) {
623622 app.tabs.Add(tab2)
624623
625624 // Build config for the SECOND window, even though first is globally focused.
626- cfg := app.buildHistorySidebarConfig(ctx, second)
625+ cfg := app.buildHistorySidebarConfig(second)
627626
628627 // Invoke OnNavigate — should navigate through second window's pane-2.
629628 err := cfg.OnNavigate(ctx, "https://ownership.com")
@@ -672,7 +671,7 @@ func TestApp_HistorySidebarConfig_KeepOpenCallback(t *testing.T) {
672671 }
673672 app.tabs.Add(tab)
674673
675- cfg := app.buildHistorySidebarConfig(ctx, bw)
674+ cfg := app.buildHistorySidebarConfig(bw)
676675
677676 // OnNavigateKeepOpen navigates but does NOT close the sidebar.
678677 navigateURL := "https://keep-open.com"
@@ -722,7 +721,7 @@ func TestApp_HistorySidebarConfig_OpenInNewPaneCallback(t *testing.T) {
722721 }
723722 app.tabs.Add(tab)
724723
725- cfg := app.buildHistorySidebarConfig(ctx, bw)
724+ cfg := app.buildHistorySidebarConfig(bw)
726725
727726 // OnOpenInNewPane should activate the owning window and split with URL.
728727 splitURL := "https://shift-enter.com"
@@ -748,8 +747,6 @@ func TestApp_HistorySidebarConfig_OpenInNewPaneCallback(t *testing.T) {
748747 // TestApp_HistorySidebarConfig_CloseCallback verifies that OnClose hides the
749748 // sidebar for the owning browser window and restores focus to the active pane.
750749 func TestApp_HistorySidebarConfig_CloseCallback(t *testing.T) {
751- ctx := t.Context()
752-
753750 paneID := entity.PaneID("pane-1")
754751 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(paneID))
755752 bwTabs := entity.NewTabList()
@@ -783,7 +780,7 @@ func TestApp_HistorySidebarConfig_CloseCallback(t *testing.T) {
783780 tab.ID: wsView,
784781 },
785782 }
786- cfg := app.buildHistorySidebarConfig(ctx, bw)
783+ cfg := app.buildHistorySidebarConfig(bw)
787784
788785 // OnClose hides the sidebar.
789786 cfg.OnClose()
@@ -795,8 +792,6 @@ func TestApp_HistorySidebarConfig_CloseCallback(t *testing.T) {
795792 // OnClose (hideAndRestoreFocusForBrowserWindow) is safe when the browser
796793 // window has no sidebar or is nil.
797794 func TestApp_HistorySidebarConfig_CloseWithNoSidebarIsSafe(t *testing.T) {
798- ctx := t.Context()
799-
800795 bw := &browserWindow{
801796 id: "no-sidebar",
802797 mainWindow: &window.MainWindow{},
@@ -808,7 +803,7 @@ func TestApp_HistorySidebarConfig_CloseWithNoSidebarIsSafe(t *testing.T) {
808803 browserWindows: map[string]*browserWindow{bw.id: bw},
809804 }
810805
811- cfg := app.buildHistorySidebarConfig(ctx, bw)
806+ cfg := app.buildHistorySidebarConfig(bw)
812807
813808 // Should not panic even with nil sidebar.
814809 require.NotPanics(t, func() { cfg.OnClose() })
@@ -69,7 +69,7 @@ func TestHistorySidebarConfig_OnNavigateNavigatesActivePaneAndKeepsSidebar(t *te
69 second.historySidebar = &component.HistorySidebar{}69 second.historySidebar = &component.HistorySidebar{}
70 second.sidebarVisible = true70 second.sidebarVisible = true
71 71
72- cfg := app.buildHistorySidebarConfig(ctx, second)72+ cfg := app.buildHistorySidebarConfig(second)
73 navigateURL := "https://example.com"73 navigateURL := "https://example.com"
74 err := cfg.OnNavigate(ctx, navigateURL)74 err := cfg.OnNavigate(ctx, navigateURL)
75 require.NoError(t, err, "OnNavigate should succeed")75 require.NoError(t, err, "OnNavigate should succeed")
@@ -118,7 +118,7 @@ func TestHistorySidebarConfig_OnNavigateKeepOpenNavigatesWithoutClosing(t *testi
118 }118 }
119 app.tabs.Add(tab)119 app.tabs.Add(tab)
120 120
121- cfg := app.buildHistorySidebarConfig(ctx, bw)121+ cfg := app.buildHistorySidebarConfig(bw)
122 navigateURL := "https://keep-open.com"122 navigateURL := "https://keep-open.com"
123 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)123 err := cfg.OnNavigateKeepOpen(ctx, navigateURL)
124 require.NoError(t, err)124 require.NoError(t, err)
@@ -346,7 +346,6 @@ func TestApp_HistorySidebarToggleHandler_NilFocusedWindowIsNoOp(t *testing.T) {
346 // TestHistorySidebarConfig_OnCloseHidesSidebar verifies that the OnClose346 // TestHistorySidebarConfig_OnCloseHidesSidebar verifies that the OnClose
347 // callback hides the sidebar for the owning browser window.347 // callback hides the sidebar for the owning browser window.
348 func TestHistorySidebarConfig_OnCloseHidesSidebar(t *testing.T) {348 func TestHistorySidebarConfig_OnCloseHidesSidebar(t *testing.T) {
349- ctx := t.Context()
350 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1")))349 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(entity.PaneID("pane-1")))
351 bwTabs := entity.NewTabList()350 bwTabs := entity.NewTabList()
352 bwTabs.Add(tab)351 bwTabs.Add(tab)
@@ -367,7 +366,7 @@ func TestHistorySidebarConfig_OnCloseHidesSidebar(t *testing.T) {
367 }366 }
368 app.tabs.Add(tab)367 app.tabs.Add(tab)
369 368
370- cfg := app.buildHistorySidebarConfig(ctx, bw)369+ cfg := app.buildHistorySidebarConfig(bw)
371 cfg.OnClose()370 cfg.OnClose()
372 371
373 assert.False(t, bw.sidebarVisible, "sidebar must be hidden by OnClose")372 assert.False(t, bw.sidebarVisible, "sidebar must be hidden by OnClose")
@@ -456,7 +455,7 @@ func TestApp_HistorySidebar_ToggleThroughDispatcher_UnavailableReturnsError(t *t
456 455
457 err := kbDispatcher.Dispatch(ctx, input.ActionToggleHistorySystemView)456 err := kbDispatcher.Dispatch(ctx, input.ActionToggleHistorySystemView)
458 require.Error(t, err)457 require.Error(t, err)
459- assert.ErrorContains(t, err, "history sidebar unavailable")458+ require.ErrorContains(t, err, "history sidebar unavailable")
460 assert.False(t, bw.sidebarVisible, "sidebar must remain invisible when not wired")459 assert.False(t, bw.sidebarVisible, "sidebar must remain invisible when not wired")
461 }460 }
462 461
@@ -483,7 +482,7 @@ func TestApp_HistorySidebar_ToggleThroughDispatcher_NilFocusedReturnsError(t *te
483 482
484 err := kbDispatcher.Dispatch(ctx, input.ActionToggleHistorySystemView)483 err := kbDispatcher.Dispatch(ctx, input.ActionToggleHistorySystemView)
485 require.Error(t, err)484 require.Error(t, err)
486- assert.ErrorContains(t, err, "history sidebar unavailable")485+ require.ErrorContains(t, err, "history sidebar unavailable")
487 }486 }
488 487
489 // =============================================================================488 // =============================================================================
@@ -527,7 +526,7 @@ func TestApp_HistorySidebarConfig_NavigateCallbackNavigates(t *testing.T) {
527 app.tabs.Add(tab)526 app.tabs.Add(tab)
528 527
529 // Build the config using the extracted seam.528 // Build the config using the extracted seam.
530- cfg := app.buildHistorySidebarConfig(ctx, bw)529+ cfg := app.buildHistorySidebarConfig(bw)
531 require.NotNil(t, cfg.OnNavigate, "OnNavigate callback must be non-nil")530 require.NotNil(t, cfg.OnNavigate, "OnNavigate callback must be non-nil")
532 531
533 // Invoke the OnNavigate callback.532 // Invoke the OnNavigate callback.
@@ -623,7 +622,7 @@ func TestApp_HistorySidebarConfig_NavigateCallbackOwnership(t *testing.T) {
623 app.tabs.Add(tab2)622 app.tabs.Add(tab2)
624 623
625 // Build config for the SECOND window, even though first is globally focused.624 // Build config for the SECOND window, even though first is globally focused.
626- cfg := app.buildHistorySidebarConfig(ctx, second)625+ cfg := app.buildHistorySidebarConfig(second)
627 626
628 // Invoke OnNavigate — should navigate through second window's pane-2.627 // Invoke OnNavigate — should navigate through second window's pane-2.
629 err := cfg.OnNavigate(ctx, "https://ownership.com")628 err := cfg.OnNavigate(ctx, "https://ownership.com")
@@ -672,7 +671,7 @@ func TestApp_HistorySidebarConfig_KeepOpenCallback(t *testing.T) {
672 }671 }
673 app.tabs.Add(tab)672 app.tabs.Add(tab)
674 673
675- cfg := app.buildHistorySidebarConfig(ctx, bw)674+ cfg := app.buildHistorySidebarConfig(bw)
676 675
677 // OnNavigateKeepOpen navigates but does NOT close the sidebar.676 // OnNavigateKeepOpen navigates but does NOT close the sidebar.
678 navigateURL := "https://keep-open.com"677 navigateURL := "https://keep-open.com"
@@ -722,7 +721,7 @@ func TestApp_HistorySidebarConfig_OpenInNewPaneCallback(t *testing.T) {
722 }721 }
723 app.tabs.Add(tab)722 app.tabs.Add(tab)
724 723
725- cfg := app.buildHistorySidebarConfig(ctx, bw)724+ cfg := app.buildHistorySidebarConfig(bw)
726 725
727 // OnOpenInNewPane should activate the owning window and split with URL.726 // OnOpenInNewPane should activate the owning window and split with URL.
728 splitURL := "https://shift-enter.com"727 splitURL := "https://shift-enter.com"
@@ -748,8 +747,6 @@ func TestApp_HistorySidebarConfig_OpenInNewPaneCallback(t *testing.T) {
748 // TestApp_HistorySidebarConfig_CloseCallback verifies that OnClose hides the747 // TestApp_HistorySidebarConfig_CloseCallback verifies that OnClose hides the
749 // sidebar for the owning browser window and restores focus to the active pane.748 // sidebar for the owning browser window and restores focus to the active pane.
750 func TestApp_HistorySidebarConfig_CloseCallback(t *testing.T) {749 func TestApp_HistorySidebarConfig_CloseCallback(t *testing.T) {
751- ctx := t.Context()
752-
753 paneID := entity.PaneID("pane-1")750 paneID := entity.PaneID("pane-1")
754 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(paneID))751 tab := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("ws-1"), entity.NewPane(paneID))
755 bwTabs := entity.NewTabList()752 bwTabs := entity.NewTabList()
@@ -783,7 +780,7 @@ func TestApp_HistorySidebarConfig_CloseCallback(t *testing.T) {
783 tab.ID: wsView,780 tab.ID: wsView,
784 },781 },
785 }782 }
786- cfg := app.buildHistorySidebarConfig(ctx, bw)783+ cfg := app.buildHistorySidebarConfig(bw)
787 784
788 // OnClose hides the sidebar.785 // OnClose hides the sidebar.
789 cfg.OnClose()786 cfg.OnClose()
@@ -795,8 +792,6 @@ func TestApp_HistorySidebarConfig_CloseCallback(t *testing.T) {
795 // OnClose (hideAndRestoreFocusForBrowserWindow) is safe when the browser792 // OnClose (hideAndRestoreFocusForBrowserWindow) is safe when the browser
796 // window has no sidebar or is nil.793 // window has no sidebar or is nil.
797 func TestApp_HistorySidebarConfig_CloseWithNoSidebarIsSafe(t *testing.T) {794 func TestApp_HistorySidebarConfig_CloseWithNoSidebarIsSafe(t *testing.T) {
798- ctx := t.Context()
799-
800 bw := &browserWindow{795 bw := &browserWindow{
801 id: "no-sidebar",796 id: "no-sidebar",
802 mainWindow: &window.MainWindow{},797 mainWindow: &window.MainWindow{},
@@ -808,7 +803,7 @@ func TestApp_HistorySidebarConfig_CloseWithNoSidebarIsSafe(t *testing.T) {
808 browserWindows: map[string]*browserWindow{bw.id: bw},803 browserWindows: map[string]*browserWindow{bw.id: bw},
809 }804 }
810 805
811- cfg := app.buildHistorySidebarConfig(ctx, bw)806+ cfg := app.buildHistorySidebarConfig(bw)
812 807
813 // Should not panic even with nil sidebar.808 // Should not panic even with nil sidebar.
814 require.NotPanics(t, func() { cfg.OnClose() })809 require.NotPanics(t, func() { cfg.OnClose() })
M internal/ui/component/history_model.go +9 -8
@@ -28,6 +28,7 @@ const (
2828 dayLabelToday = "Today"
2929 dayLabelYesterday = "Yesterday"
3030 dayLabelOtherYearFormat = "January 2, 2006"
31+ hoursPerDay = 24
3132 )
3233
3334 // groupHistoryByDay groups entries by calendar day, newest-first.
@@ -73,7 +74,7 @@ func groupHistoryByDay(entries []*entity.HistoryEntry) []historyGroup {
7374 return groups
7475 }
7576
76-func dayLabelForKey(key dayKey, todayStart time.Time, now time.Time) string {
77+func dayLabelForKey(key dayKey, todayStart, now time.Time) string {
7778 dayStart := time.Date(key.year, key.month, key.day, 0, 0, 0, 0, now.Location())
7879 switch {
7980 case dayStart.Equal(todayStart):
@@ -119,14 +120,14 @@ func relativeTime(t time.Time) string {
119120 return "1m ago"
120121 }
121122 return strconv.Itoa(m) + "m ago"
122- case d < 24*time.Hour:
123+ case d < hoursPerDay*time.Hour:
123124 h := int(d.Hours())
124125 if h < 2 {
125126 return "1h ago"
126127 }
127128 return strconv.Itoa(h) + "h ago"
128- case d < 7*24*time.Hour:
129- days := int(d.Hours() / 24)
129+ case d < 7*hoursPerDay*time.Hour:
130+ days := int(d.Hours() / hoursPerDay)
130131 if days < 2 {
131132 return "1d ago"
132133 }
@@ -231,13 +232,13 @@ func (m keyboardNavModel) groupIndexAt(index int) int {
231232 }
232233
233234 func (m keyboardNavModel) maxGroupIndex() int {
234- max := -1
235+ maxIndex := -1
235236 for _, row := range m.rows {
236- if row.GroupIndex > max {
237- max = row.GroupIndex
237+ if row.GroupIndex > maxIndex {
238+ maxIndex = row.GroupIndex
238239 }
239240 }
240- return max
241+ return maxIndex
241242 }
242243
243244 func (m keyboardNavModel) cumulativeOffsetAtGroup(gi int) int {
@@ -28,6 +28,7 @@ const (
28 dayLabelToday = "Today"28 dayLabelToday = "Today"
29 dayLabelYesterday = "Yesterday"29 dayLabelYesterday = "Yesterday"
30 dayLabelOtherYearFormat = "January 2, 2006"30 dayLabelOtherYearFormat = "January 2, 2006"
31+ hoursPerDay = 24
31 )32 )
32 33
33 // groupHistoryByDay groups entries by calendar day, newest-first.34 // groupHistoryByDay groups entries by calendar day, newest-first.
@@ -73,7 +74,7 @@ func groupHistoryByDay(entries []*entity.HistoryEntry) []historyGroup {
73 return groups74 return groups
74 }75 }
75 76
76-func dayLabelForKey(key dayKey, todayStart time.Time, now time.Time) string {77+func dayLabelForKey(key dayKey, todayStart, now time.Time) string {
77 dayStart := time.Date(key.year, key.month, key.day, 0, 0, 0, 0, now.Location())78 dayStart := time.Date(key.year, key.month, key.day, 0, 0, 0, 0, now.Location())
78 switch {79 switch {
79 case dayStart.Equal(todayStart):80 case dayStart.Equal(todayStart):
@@ -119,14 +120,14 @@ func relativeTime(t time.Time) string {
119 return "1m ago"120 return "1m ago"
120 }121 }
121 return strconv.Itoa(m) + "m ago"122 return strconv.Itoa(m) + "m ago"
122- case d < 24*time.Hour:123+ case d < hoursPerDay*time.Hour:
123 h := int(d.Hours())124 h := int(d.Hours())
124 if h < 2 {125 if h < 2 {
125 return "1h ago"126 return "1h ago"
126 }127 }
127 return strconv.Itoa(h) + "h ago"128 return strconv.Itoa(h) + "h ago"
128- case d < 7*24*time.Hour:129+ case d < 7*hoursPerDay*time.Hour:
129- days := int(d.Hours() / 24)130+ days := int(d.Hours() / hoursPerDay)
130 if days < 2 {131 if days < 2 {
131 return "1d ago"132 return "1d ago"
132 }133 }
@@ -231,13 +232,13 @@ func (m keyboardNavModel) groupIndexAt(index int) int {
231 }232 }
232 233
233 func (m keyboardNavModel) maxGroupIndex() int {234 func (m keyboardNavModel) maxGroupIndex() int {
234- max := -1235+ maxIndex := -1
235 for _, row := range m.rows {236 for _, row := range m.rows {
236- if row.GroupIndex > max {237+ if row.GroupIndex > maxIndex {
237- max = row.GroupIndex238+ maxIndex = row.GroupIndex
238 }239 }
239 }240 }
240- return max241+ return maxIndex
241 }242 }
242 243
243 func (m keyboardNavModel) cumulativeOffsetAtGroup(gi int) int {244 func (m keyboardNavModel) cumulativeOffsetAtGroup(gi int) int {
M internal/ui/component/history_model_test.go +10 -12
@@ -270,7 +270,7 @@ func TestKeyboardNavModel_EmptyGroups(t *testing.T) {
270270 assert.Equal(t, -1, m.firstSelectableIndex())
271271 assert.Equal(t, -1, m.lastSelectableIndex())
272272 assert.Nil(t, m.entryAt(0))
273- assert.Equal(t, "", m.entryURLAt(0))
273+ assert.Empty(t, m.entryURLAt(0))
274274
275275 m = newKeyboardNavModel([]historyGroup{})
276276 assert.Equal(t, 0, m.totalRows())
@@ -285,7 +285,7 @@ func TestKeyboardNavModel_SingleGroup(t *testing.T) {
285285 // Row 0: header (not selectable)
286286 assert.False(t, m.isSelectable(0))
287287 assert.Nil(t, m.entryAt(0))
288- assert.Equal(t, "", m.entryURLAt(0))
288+ assert.Empty(t, m.entryURLAt(0))
289289
290290 // Rows 1-3: entries (selectable)
291291 for i := 1; i <= 3; i++ {
@@ -395,13 +395,13 @@ func TestKeyboardNavModel_EntryAtURL(t *testing.T) {
395395
396396 // Header rows return nil / ""
397397 assert.Nil(t, m.entryAt(0)) // H0
398- assert.Equal(t, "", m.entryURLAt(0))
398+ assert.Empty(t, m.entryURLAt(0))
399399 assert.Nil(t, m.entryAt(2)) // H1
400- assert.Equal(t, "", m.entryURLAt(2))
400+ assert.Empty(t, m.entryURLAt(2))
401401
402402 // Out of range
403403 assert.Nil(t, m.entryAt(99))
404- assert.Equal(t, "", m.entryURLAt(99))
404+ assert.Empty(t, m.entryURLAt(99))
405405 assert.Nil(t, m.entryAt(-1))
406406 }
407407
@@ -463,7 +463,7 @@ func TestKeyboardNavModelFromRows_UsesExplicitDisplayRows(t *testing.T) {
463463
464464 func TestTransitionSearchState_EmptyQuery(t *testing.T) {
465465 next := transitionSearchState("", 0)
466- assert.Equal(t, "", next.Query)
466+ assert.Empty(t, next.Query)
467467 assert.False(t, next.HasSearchDone)
468468 assert.False(t, next.HasResults)
469469 assert.Equal(t, 0, next.ResultCount)
@@ -495,7 +495,7 @@ func TestTransitionSearchState_QueryToQuery(t *testing.T) {
495495
496496 func TestTransitionSearchState_QueryToEmpty(t *testing.T) {
497497 next := transitionSearchState("", 0)
498- assert.Equal(t, "", next.Query)
498+ assert.Empty(t, next.Query)
499499 assert.False(t, next.HasSearchDone)
500500 assert.False(t, next.HasResults)
501501 assert.Equal(t, 0, next.ResultCount)
@@ -507,7 +507,7 @@ func TestTransitionSearchState_QueryToEmpty(t *testing.T) {
507507
508508 func TestApplyReloadState_WithoutQuery(t *testing.T) {
509509 s := applyReloadState("")
510- assert.Equal(t, "", s.PreservedQuery)
510+ assert.Empty(t, s.PreservedQuery)
511511 assert.True(t, s.ResetBrowse)
512512 assert.False(t, s.ClearSearch)
513513 }
@@ -667,10 +667,8 @@ func TestTransitionSearchState_SequentialSearches(t *testing.T) {
667667 // TestTransitionSearchState_SearchThenClearThenReSearch verifies the
668668 // transition from search -> empty -> new search produces correct state.
669669 func TestTransitionSearchState_SearchThenClearThenReSearch(t *testing.T) {
670- s := searchStateSnapshot{}
671-
672670 // Search "term" -> 2 results
673- s = transitionSearchState("term", 2)
671+ s := transitionSearchState("term", 2)
674672 assert.Equal(t, "term", s.Query)
675673 assert.True(t, s.HasSearchDone)
676674 assert.True(t, s.HasResults)
@@ -678,7 +676,7 @@ func TestTransitionSearchState_SearchThenClearThenReSearch(t *testing.T) {
678676
679677 // Clear: query becomes ""
680678 s = transitionSearchState("", 0)
681- assert.Equal(t, "", s.Query)
679+ assert.Empty(t, s.Query)
682680 assert.False(t, s.HasSearchDone)
683681 assert.False(t, s.HasResults)
684682 assert.Equal(t, 0, s.ResultCount)
@@ -270,7 +270,7 @@ func TestKeyboardNavModel_EmptyGroups(t *testing.T) {
270 assert.Equal(t, -1, m.firstSelectableIndex())270 assert.Equal(t, -1, m.firstSelectableIndex())
271 assert.Equal(t, -1, m.lastSelectableIndex())271 assert.Equal(t, -1, m.lastSelectableIndex())
272 assert.Nil(t, m.entryAt(0))272 assert.Nil(t, m.entryAt(0))
273- assert.Equal(t, "", m.entryURLAt(0))273+ assert.Empty(t, m.entryURLAt(0))
274 274
275 m = newKeyboardNavModel([]historyGroup{})275 m = newKeyboardNavModel([]historyGroup{})
276 assert.Equal(t, 0, m.totalRows())276 assert.Equal(t, 0, m.totalRows())
@@ -285,7 +285,7 @@ func TestKeyboardNavModel_SingleGroup(t *testing.T) {
285 // Row 0: header (not selectable)285 // Row 0: header (not selectable)
286 assert.False(t, m.isSelectable(0))286 assert.False(t, m.isSelectable(0))
287 assert.Nil(t, m.entryAt(0))287 assert.Nil(t, m.entryAt(0))
288- assert.Equal(t, "", m.entryURLAt(0))288+ assert.Empty(t, m.entryURLAt(0))
289 289
290 // Rows 1-3: entries (selectable)290 // Rows 1-3: entries (selectable)
291 for i := 1; i <= 3; i++ {291 for i := 1; i <= 3; i++ {
@@ -395,13 +395,13 @@ func TestKeyboardNavModel_EntryAtURL(t *testing.T) {
395 395
396 // Header rows return nil / ""396 // Header rows return nil / ""
397 assert.Nil(t, m.entryAt(0)) // H0397 assert.Nil(t, m.entryAt(0)) // H0
398- assert.Equal(t, "", m.entryURLAt(0))398+ assert.Empty(t, m.entryURLAt(0))
399 assert.Nil(t, m.entryAt(2)) // H1399 assert.Nil(t, m.entryAt(2)) // H1
400- assert.Equal(t, "", m.entryURLAt(2))400+ assert.Empty(t, m.entryURLAt(2))
401 401
402 // Out of range402 // Out of range
403 assert.Nil(t, m.entryAt(99))403 assert.Nil(t, m.entryAt(99))
404- assert.Equal(t, "", m.entryURLAt(99))404+ assert.Empty(t, m.entryURLAt(99))
405 assert.Nil(t, m.entryAt(-1))405 assert.Nil(t, m.entryAt(-1))
406 }406 }
407 407
@@ -463,7 +463,7 @@ func TestKeyboardNavModelFromRows_UsesExplicitDisplayRows(t *testing.T) {
463 463
464 func TestTransitionSearchState_EmptyQuery(t *testing.T) {464 func TestTransitionSearchState_EmptyQuery(t *testing.T) {
465 next := transitionSearchState("", 0)465 next := transitionSearchState("", 0)
466- assert.Equal(t, "", next.Query)466+ assert.Empty(t, next.Query)
467 assert.False(t, next.HasSearchDone)467 assert.False(t, next.HasSearchDone)
468 assert.False(t, next.HasResults)468 assert.False(t, next.HasResults)
469 assert.Equal(t, 0, next.ResultCount)469 assert.Equal(t, 0, next.ResultCount)
@@ -495,7 +495,7 @@ func TestTransitionSearchState_QueryToQuery(t *testing.T) {
495 495
496 func TestTransitionSearchState_QueryToEmpty(t *testing.T) {496 func TestTransitionSearchState_QueryToEmpty(t *testing.T) {
497 next := transitionSearchState("", 0)497 next := transitionSearchState("", 0)
498- assert.Equal(t, "", next.Query)498+ assert.Empty(t, next.Query)
499 assert.False(t, next.HasSearchDone)499 assert.False(t, next.HasSearchDone)
500 assert.False(t, next.HasResults)500 assert.False(t, next.HasResults)
501 assert.Equal(t, 0, next.ResultCount)501 assert.Equal(t, 0, next.ResultCount)
@@ -507,7 +507,7 @@ func TestTransitionSearchState_QueryToEmpty(t *testing.T) {
507 507
508 func TestApplyReloadState_WithoutQuery(t *testing.T) {508 func TestApplyReloadState_WithoutQuery(t *testing.T) {
509 s := applyReloadState("")509 s := applyReloadState("")
510- assert.Equal(t, "", s.PreservedQuery)510+ assert.Empty(t, s.PreservedQuery)
511 assert.True(t, s.ResetBrowse)511 assert.True(t, s.ResetBrowse)
512 assert.False(t, s.ClearSearch)512 assert.False(t, s.ClearSearch)
513 }513 }
@@ -667,10 +667,8 @@ func TestTransitionSearchState_SequentialSearches(t *testing.T) {
667 // TestTransitionSearchState_SearchThenClearThenReSearch verifies the667 // TestTransitionSearchState_SearchThenClearThenReSearch verifies the
668 // transition from search -> empty -> new search produces correct state.668 // transition from search -> empty -> new search produces correct state.
669 func TestTransitionSearchState_SearchThenClearThenReSearch(t *testing.T) {669 func TestTransitionSearchState_SearchThenClearThenReSearch(t *testing.T) {
670- s := searchStateSnapshot{}
671-
672 // Search "term" -> 2 results670 // Search "term" -> 2 results
673- s = transitionSearchState("term", 2)671+ s := transitionSearchState("term", 2)
674 assert.Equal(t, "term", s.Query)672 assert.Equal(t, "term", s.Query)
675 assert.True(t, s.HasSearchDone)673 assert.True(t, s.HasSearchDone)
676 assert.True(t, s.HasResults)674 assert.True(t, s.HasResults)
@@ -678,7 +676,7 @@ func TestTransitionSearchState_SearchThenClearThenReSearch(t *testing.T) {
678 676
679 // Clear: query becomes ""677 // Clear: query becomes ""
680 s = transitionSearchState("", 0)678 s = transitionSearchState("", 0)
681- assert.Equal(t, "", s.Query)679+ assert.Empty(t, s.Query)
682 assert.False(t, s.HasSearchDone)680 assert.False(t, s.HasSearchDone)
683 assert.False(t, s.HasResults)681 assert.False(t, s.HasResults)
684 assert.Equal(t, 0, s.ResultCount)682 assert.Equal(t, 0, s.ResultCount)
M internal/ui/component/history_sidebar.go +2 -2
@@ -234,14 +234,14 @@ func (hs *HistorySidebar) Widget() *gtk.Widget {
234234 // current recent visits, not stale data from initialization.
235235 func (hs *HistorySidebar) Show() {
236236 hs.mu.Lock()
237- defer hs.mu.Unlock()
238-
239237 if hs.outerBox == nil || hs.destroyed {
238+ hs.mu.Unlock()
240239 return
241240 }
242241
243242 hs.outerBox.SetVisible(true)
244243 hs.visible = true
244+ hs.mu.Unlock()
245245
246246 // Schedule a background reload so the sidebar shows fresh data
247247 // when it becomes visible, not stale data captured at init time.
@@ -234,14 +234,14 @@ func (hs *HistorySidebar) Widget() *gtk.Widget {
234 // current recent visits, not stale data from initialization.234 // current recent visits, not stale data from initialization.
235 func (hs *HistorySidebar) Show() {235 func (hs *HistorySidebar) Show() {
236 hs.mu.Lock()236 hs.mu.Lock()
237- defer hs.mu.Unlock()
238-
239 if hs.outerBox == nil || hs.destroyed {237 if hs.outerBox == nil || hs.destroyed {
238+ hs.mu.Unlock()
240 return239 return
241 }240 }
242 241
243 hs.outerBox.SetVisible(true)242 hs.outerBox.SetVisible(true)
244 hs.visible = true243 hs.visible = true
244+ hs.mu.Unlock()
245 245
246 // Schedule a background reload so the sidebar shows fresh data246 // Schedule a background reload so the sidebar shows fresh data
247 // when it becomes visible, not stale data captured at init time.247 // when it becomes visible, not stale data captured at init time.
M internal/ui/component/history_sidebar_keyboard.go +5 -5
@@ -40,7 +40,7 @@ func (hs *HistorySidebar) setupKeyboardNavigation() {
4040
4141 // --- Enter variants ---
4242 case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):
43- return hs.handleEnterKey(keyval, state)
43+ return hs.handleEnterKey(state)
4444
4545 // --- Delete: remove selected entry ---
4646 case uint(gdk.KEY_Delete), uint(gdk.KEY_KP_Delete):
@@ -94,7 +94,7 @@ func (hs *HistorySidebar) setupKeyboardNavigation() {
9494
9595 // handleEnterKey processes Enter, Ctrl+Enter, and Shift+Enter on a selected row.
9696 // Returns true if the key was consumed.
97-func (hs *HistorySidebar) handleEnterKey(keyval uint, state gdk.ModifierType) bool {
97+func (hs *HistorySidebar) handleEnterKey(state gdk.ModifierType) bool {
9898 // Determine activation mode from modifiers
9999 var action HistorySidebarKeyboardAction
100100
@@ -175,7 +175,7 @@ func (hs *HistorySidebar) handleDeleteKey() bool {
175175 hs.mu.Unlock()
176176 return false
177177 }
178- hs.applyDeletedEntryLocked(url, entryID, nextSelectedURL)
178+ hs.applyDeletedEntryLocked(entryID, nextSelectedURL)
179179 hs.mu.Unlock()
180180
181181 hs.rebuildList()
@@ -189,7 +189,7 @@ func (hs *HistorySidebar) handleDeleteKey() bool {
189189
190190 // applyDeletedEntryLocked updates local sidebar state after a successful
191191 // history delete. Must be called with hs.mu write lock held.
192-func (hs *HistorySidebar) applyDeletedEntryLocked(url string, entryID int64, nextSelectedURL string) {
192+func (hs *HistorySidebar) applyDeletedEntryLocked(entryID int64, nextSelectedURL string) {
193193 hs.preserveScrollAndSelection()
194194 hs.prevSelectedURL = nextSelectedURL
195195 hs.searchGen++
@@ -428,7 +428,7 @@ func (hs *HistorySidebar) selectAdjacentRow(direction int) {
428428
429429 hs.mu.RLock()
430430 model := newKeyboardNavModelFromRows(hs.displayRows)
431- target := -1
431+ var target int
432432 if current < 0 {
433433 if direction > 0 {
434434 target = model.firstSelectableIndex()
@@ -40,7 +40,7 @@ func (hs *HistorySidebar) setupKeyboardNavigation() {
40 40
41 // --- Enter variants ---41 // --- Enter variants ---
42 case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):42 case uint(gdk.KEY_Return), uint(gdk.KEY_KP_Enter):
43- return hs.handleEnterKey(keyval, state)43+ return hs.handleEnterKey(state)
44 44
45 // --- Delete: remove selected entry ---45 // --- Delete: remove selected entry ---
46 case uint(gdk.KEY_Delete), uint(gdk.KEY_KP_Delete):46 case uint(gdk.KEY_Delete), uint(gdk.KEY_KP_Delete):
@@ -94,7 +94,7 @@ func (hs *HistorySidebar) setupKeyboardNavigation() {
94 94
95 // handleEnterKey processes Enter, Ctrl+Enter, and Shift+Enter on a selected row.95 // handleEnterKey processes Enter, Ctrl+Enter, and Shift+Enter on a selected row.
96 // Returns true if the key was consumed.96 // Returns true if the key was consumed.
97-func (hs *HistorySidebar) handleEnterKey(keyval uint, state gdk.ModifierType) bool {97+func (hs *HistorySidebar) handleEnterKey(state gdk.ModifierType) bool {
98 // Determine activation mode from modifiers98 // Determine activation mode from modifiers
99 var action HistorySidebarKeyboardAction99 var action HistorySidebarKeyboardAction
100 100
@@ -175,7 +175,7 @@ func (hs *HistorySidebar) handleDeleteKey() bool {
175 hs.mu.Unlock()175 hs.mu.Unlock()
176 return false176 return false
177 }177 }
178- hs.applyDeletedEntryLocked(url, entryID, nextSelectedURL)178+ hs.applyDeletedEntryLocked(entryID, nextSelectedURL)
179 hs.mu.Unlock()179 hs.mu.Unlock()
180 180
181 hs.rebuildList()181 hs.rebuildList()
@@ -189,7 +189,7 @@ func (hs *HistorySidebar) handleDeleteKey() bool {
189 189
190 // applyDeletedEntryLocked updates local sidebar state after a successful190 // applyDeletedEntryLocked updates local sidebar state after a successful
191 // history delete. Must be called with hs.mu write lock held.191 // history delete. Must be called with hs.mu write lock held.
192-func (hs *HistorySidebar) applyDeletedEntryLocked(url string, entryID int64, nextSelectedURL string) {192+func (hs *HistorySidebar) applyDeletedEntryLocked(entryID int64, nextSelectedURL string) {
193 hs.preserveScrollAndSelection()193 hs.preserveScrollAndSelection()
194 hs.prevSelectedURL = nextSelectedURL194 hs.prevSelectedURL = nextSelectedURL
195 hs.searchGen++195 hs.searchGen++
@@ -428,7 +428,7 @@ func (hs *HistorySidebar) selectAdjacentRow(direction int) {
428 428
429 hs.mu.RLock()429 hs.mu.RLock()
430 model := newKeyboardNavModelFromRows(hs.displayRows)430 model := newKeyboardNavModelFromRows(hs.displayRows)
431- target := -1431+ var target int
432 if current < 0 {432 if current < 0 {
433 if direction > 0 {433 if direction > 0 {
434 target = model.firstSelectableIndex()434 target = model.firstSelectableIndex()
M internal/ui/component/history_sidebar_loading_search.go +2 -2
@@ -48,7 +48,7 @@ func (hs *HistorySidebar) fetchPage(offset int, gen uint64) {
4848 hs.scheduleRebuild()
4949 return false
5050 })
51- glib.IdleAdd(&cb, 0)
51+ hs.scheduleIdle(cb)
5252 return
5353 }
5454
@@ -105,7 +105,7 @@ func (hs *HistorySidebar) fetchPage(offset int, gen uint64) {
105105 hs.rebuildList()
106106 return false
107107 })
108- glib.IdleAdd(&cb, 0)
108+ hs.scheduleIdle(cb)
109109 }
110110
111111 // LoadMore fetches the next page and appends it to the existing entries.
@@ -48,7 +48,7 @@ func (hs *HistorySidebar) fetchPage(offset int, gen uint64) {
48 hs.scheduleRebuild()48 hs.scheduleRebuild()
49 return false49 return false
50 })50 })
51- glib.IdleAdd(&cb, 0)51+ hs.scheduleIdle(cb)
52 return52 return
53 }53 }
54 54
@@ -105,7 +105,7 @@ func (hs *HistorySidebar) fetchPage(offset int, gen uint64) {
105 hs.rebuildList()105 hs.rebuildList()
106 return false106 return false
107 })107 })
108- glib.IdleAdd(&cb, 0)108+ hs.scheduleIdle(cb)
109 }109 }
110 110
111 // LoadMore fetches the next page and appends it to the existing entries.111 // LoadMore fetches the next page and appends it to the existing entries.
M internal/ui/component/history_sidebar_rendering.go +2 -2
@@ -78,7 +78,7 @@ func (hs *HistorySidebar) showLoadingOrEmpty(query string, searchDone bool) {
7878 case query != "" && !searchDone:
7979 label.SetText("Searching...")
8080 case query != "":
81- label.SetText(fmt.Sprintf("No results for \"%s\"", query))
81+ label.SetText(fmt.Sprintf("No results for %q", query))
8282 default:
8383 label.SetText("No browsing history")
8484 }
@@ -105,7 +105,7 @@ func (hs *HistorySidebar) showEmptyState(query string) {
105105 label.AddCssClass("history-sidebar-empty")
106106
107107 if query != "" {
108- label.SetText(fmt.Sprintf("No results for \"%s\"", query))
108+ label.SetText(fmt.Sprintf("No results for %q", query))
109109 } else {
110110 label.SetText("No browsing history")
111111 }
@@ -78,7 +78,7 @@ func (hs *HistorySidebar) showLoadingOrEmpty(query string, searchDone bool) {
78 case query != "" && !searchDone:78 case query != "" && !searchDone:
79 label.SetText("Searching...")79 label.SetText("Searching...")
80 case query != "":80 case query != "":
81- label.SetText(fmt.Sprintf("No results for \"%s\"", query))81+ label.SetText(fmt.Sprintf("No results for %q", query))
82 default:82 default:
83 label.SetText("No browsing history")83 label.SetText("No browsing history")
84 }84 }
@@ -105,7 +105,7 @@ func (hs *HistorySidebar) showEmptyState(query string) {
105 label.AddCssClass("history-sidebar-empty")105 label.AddCssClass("history-sidebar-empty")
106 106
107 if query != "" {107 if query != "" {
108- label.SetText(fmt.Sprintf("No results for \"%s\"", query))108+ label.SetText(fmt.Sprintf("No results for %q", query))
109 } else {109 } else {
110 label.SetText("No browsing history")110 label.SetText("No browsing history")
111 }111 }
M internal/ui/component/history_sidebar_search_test.go +4 -4
@@ -40,7 +40,7 @@ func TestApplySearchResults_NonStaleApplied(t *testing.T) {
4040 applied := hs.applySearchResults(entries, 1, nil)
4141 assert.True(t, applied, "non-stale results must be applied")
4242 assert.True(t, hs.searchDone)
43- assert.Nil(t, hs.searchErr)
43+ require.NoError(t, hs.searchErr)
4444 require.NotNil(t, hs.searchResults)
4545 assert.Len(t, hs.searchResults, 1)
4646 assert.Equal(t, "https://example.com", hs.searchResults[0].URL)
@@ -100,7 +100,7 @@ func TestApplySearchResults_ErrorStored(t *testing.T) {
100100 applied := hs.applySearchResults(entries, 1, wantErr)
101101 assert.True(t, applied)
102102 assert.True(t, hs.searchDone)
103- assert.ErrorIs(t, hs.searchErr, wantErr)
103+ require.ErrorIs(t, hs.searchErr, wantErr)
104104 require.NotNil(t, hs.searchResults)
105105 assert.Empty(t, hs.searchResults)
106106 }
@@ -112,7 +112,7 @@ func TestApplySearchResults_EmptyResultsApplied(t *testing.T) {
112112 applied := hs.applySearchResults([]*entity.HistoryEntry{}, 1, nil)
113113 assert.True(t, applied)
114114 assert.True(t, hs.searchDone)
115- assert.Nil(t, hs.searchErr)
115+ require.NoError(t, hs.searchErr)
116116 require.NotNil(t, hs.searchResults)
117117 assert.Empty(t, hs.searchResults)
118118 // Groups from empty entries should be nil or empty
@@ -135,7 +135,7 @@ func TestApplyDeletedEntryLocked_RecomputesBrowseState(t *testing.T) {
135135 hs.setDisplayGroupsLocked(groupHistoryByDay(hs.allEntries))
136136
137137 hs.mu.Lock()
138- hs.applyDeletedEntryLocked(deleteMe.URL, deleteMe.ID, keepB.URL)
138+ hs.applyDeletedEntryLocked(deleteMe.ID, keepB.URL)
139139 hs.mu.Unlock()
140140
141141 assert.Equal(t, uint64(8), hs.loadGen, "deletes must invalidate in-flight browse loads")
@@ -40,7 +40,7 @@ func TestApplySearchResults_NonStaleApplied(t *testing.T) {
40 applied := hs.applySearchResults(entries, 1, nil)40 applied := hs.applySearchResults(entries, 1, nil)
41 assert.True(t, applied, "non-stale results must be applied")41 assert.True(t, applied, "non-stale results must be applied")
42 assert.True(t, hs.searchDone)42 assert.True(t, hs.searchDone)
43- assert.Nil(t, hs.searchErr)43+ require.NoError(t, hs.searchErr)
44 require.NotNil(t, hs.searchResults)44 require.NotNil(t, hs.searchResults)
45 assert.Len(t, hs.searchResults, 1)45 assert.Len(t, hs.searchResults, 1)
46 assert.Equal(t, "https://example.com", hs.searchResults[0].URL)46 assert.Equal(t, "https://example.com", hs.searchResults[0].URL)
@@ -100,7 +100,7 @@ func TestApplySearchResults_ErrorStored(t *testing.T) {
100 applied := hs.applySearchResults(entries, 1, wantErr)100 applied := hs.applySearchResults(entries, 1, wantErr)
101 assert.True(t, applied)101 assert.True(t, applied)
102 assert.True(t, hs.searchDone)102 assert.True(t, hs.searchDone)
103- assert.ErrorIs(t, hs.searchErr, wantErr)103+ require.ErrorIs(t, hs.searchErr, wantErr)
104 require.NotNil(t, hs.searchResults)104 require.NotNil(t, hs.searchResults)
105 assert.Empty(t, hs.searchResults)105 assert.Empty(t, hs.searchResults)
106 }106 }
@@ -112,7 +112,7 @@ func TestApplySearchResults_EmptyResultsApplied(t *testing.T) {
112 applied := hs.applySearchResults([]*entity.HistoryEntry{}, 1, nil)112 applied := hs.applySearchResults([]*entity.HistoryEntry{}, 1, nil)
113 assert.True(t, applied)113 assert.True(t, applied)
114 assert.True(t, hs.searchDone)114 assert.True(t, hs.searchDone)
115- assert.Nil(t, hs.searchErr)115+ require.NoError(t, hs.searchErr)
116 require.NotNil(t, hs.searchResults)116 require.NotNil(t, hs.searchResults)
117 assert.Empty(t, hs.searchResults)117 assert.Empty(t, hs.searchResults)
118 // Groups from empty entries should be nil or empty118 // Groups from empty entries should be nil or empty
@@ -135,7 +135,7 @@ func TestApplyDeletedEntryLocked_RecomputesBrowseState(t *testing.T) {
135 hs.setDisplayGroupsLocked(groupHistoryByDay(hs.allEntries))135 hs.setDisplayGroupsLocked(groupHistoryByDay(hs.allEntries))
136 136
137 hs.mu.Lock()137 hs.mu.Lock()
138- hs.applyDeletedEntryLocked(deleteMe.URL, deleteMe.ID, keepB.URL)138+ hs.applyDeletedEntryLocked(deleteMe.ID, keepB.URL)
139 hs.mu.Unlock()139 hs.mu.Unlock()
140 140
141 assert.Equal(t, uint64(8), hs.loadGen, "deletes must invalidate in-flight browse loads")141 assert.Equal(t, uint64(8), hs.loadGen, "deletes must invalidate in-flight browse loads")
M internal/ui/component/history_sidebar_widgets.go +1 -1
@@ -76,7 +76,7 @@ func (hs *HistorySidebar) initListArea() error {
7676 hs.listBox.SetActivateOnSingleClick(true)
7777 hs.listBox.SetSelectionMode(gtk.SelectionSingleValue)
7878
79- // Connect row activation (Enter or double-click)
79+ // Connect row activation (single-click and keyboard activation)
8080 rowActivatedCb := func(_ gtk.ListBox, rowPtr uintptr) {
8181 row := gtk.ListBoxRowNewFromInternalPtr(rowPtr)
8282 if row == nil {
@@ -76,7 +76,7 @@ func (hs *HistorySidebar) initListArea() error {
76 hs.listBox.SetActivateOnSingleClick(true)76 hs.listBox.SetActivateOnSingleClick(true)
77 hs.listBox.SetSelectionMode(gtk.SelectionSingleValue)77 hs.listBox.SetSelectionMode(gtk.SelectionSingleValue)
78 78
79- // Connect row activation (Enter or double-click)79+ // Connect row activation (single-click and keyboard activation)
80 rowActivatedCb := func(_ gtk.ListBox, rowPtr uintptr) {80 rowActivatedCb := func(_ gtk.ListBox, rowPtr uintptr) {
81 row := gtk.ListBoxRowNewFromInternalPtr(rowPtr)81 row := gtk.ListBoxRowNewFromInternalPtr(rowPtr)
82 if row == nil {82 if row == nil {
M internal/ui/window/main_window.go +37 -15
@@ -61,10 +61,26 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
6161 mw.window.SetTitle(&title)
6262 mw.window.SetDefaultSize(defaultWidth, defaultHeight)
6363
64+ if err := mw.initLayoutWidgets(); err != nil {
65+ return nil, err
66+ }
67+
68+ mw.contentAreaBox.Append(&mw.mainContentBox.Widget)
69+ mw.contentAreaBox.Append(&mw.sidebarBox.Widget)
70+ mw.contentOverlay.SetChild(&mw.contentAreaBox.Widget)
71+
72+ mw.assembleLayout()
73+
74+ mw.window.SetChild(&mw.rootBox.Widget)
75+
76+ return mw, nil
77+}
78+
79+func (mw *MainWindow) initLayoutWidgets() error {
6480 mw.rootBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
6581 if mw.rootBox == nil {
6682 mw.window.Unref()
67- return nil, ErrWidgetCreationFailed("rootBox")
83+ return ErrWidgetCreationFailed("rootBox")
6884 }
6985 mw.rootBox.SetHexpand(true)
7086 mw.rootBox.SetVexpand(true)
@@ -74,15 +90,22 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
7490 if mw.tabBar == nil {
7591 mw.rootBox.Unref()
7692 mw.window.Unref()
77- return nil, ErrWidgetCreationFailed("tabBar")
93+ return ErrWidgetCreationFailed("tabBar")
94+ }
95+
96+ if err := mw.initContentWidgets(); err != nil {
97+ return err
7898 }
99+ return nil
100+}
79101
102+func (mw *MainWindow) initContentWidgets() error {
80103 mw.contentOverlay = gtk.NewOverlay()
81104 if mw.contentOverlay == nil {
82105 mw.tabBar.Destroy()
83106 mw.rootBox.Unref()
84107 mw.window.Unref()
85- return nil, ErrWidgetCreationFailed("contentOverlay")
108+ return ErrWidgetCreationFailed("contentOverlay")
86109 }
87110 mw.contentOverlay.SetHexpand(true)
88111 mw.contentOverlay.SetVexpand(true)
@@ -95,12 +118,19 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
95118 mw.tabBar.Destroy()
96119 mw.rootBox.Unref()
97120 mw.window.Unref()
98- return nil, ErrWidgetCreationFailed("contentAreaBox")
121+ return ErrWidgetCreationFailed("contentAreaBox")
99122 }
100123 mw.contentAreaBox.SetHexpand(true)
101124 mw.contentAreaBox.SetVexpand(true)
102125 mw.contentAreaBox.SetVisible(true)
103126
127+ if err := mw.initContentBoxes(); err != nil {
128+ return err
129+ }
130+ return nil
131+}
132+
133+func (mw *MainWindow) initContentBoxes() error {
104134 // Main content box (vertical) for workspace content.
105135 mw.mainContentBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
106136 if mw.mainContentBox == nil {
@@ -109,7 +139,7 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
109139 mw.tabBar.Destroy()
110140 mw.rootBox.Unref()
111141 mw.window.Unref()
112- return nil, ErrWidgetCreationFailed("mainContentBox")
142+ return ErrWidgetCreationFailed("mainContentBox")
113143 }
114144 mw.mainContentBox.SetHexpand(true)
115145 mw.mainContentBox.SetVexpand(true)
@@ -124,21 +154,13 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
124154 mw.tabBar.Destroy()
125155 mw.rootBox.Unref()
126156 mw.window.Unref()
127- return nil, ErrWidgetCreationFailed("sidebarBox")
157+ return ErrWidgetCreationFailed("sidebarBox")
128158 }
129159 mw.sidebarBox.SetHexpand(false)
130160 mw.sidebarBox.SetVexpand(true)
131161 mw.sidebarBox.SetVisible(false)
132162
133- mw.contentAreaBox.Append(&mw.mainContentBox.Widget)
134- mw.contentAreaBox.Append(&mw.sidebarBox.Widget)
135- mw.contentOverlay.SetChild(&mw.contentAreaBox.Widget)
136-
137- mw.assembleLayout()
138-
139- mw.window.SetChild(&mw.rootBox.Widget)
140-
141- return mw, nil
163+ return nil
142164 }
143165
144166 func (mw *MainWindow) assembleLayout() {
@@ -61,10 +61,26 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
61 mw.window.SetTitle(&title)61 mw.window.SetTitle(&title)
62 mw.window.SetDefaultSize(defaultWidth, defaultHeight)62 mw.window.SetDefaultSize(defaultWidth, defaultHeight)
63 63
64+ if err := mw.initLayoutWidgets(); err != nil {
65+ return nil, err
66+ }
67+
68+ mw.contentAreaBox.Append(&mw.mainContentBox.Widget)
69+ mw.contentAreaBox.Append(&mw.sidebarBox.Widget)
70+ mw.contentOverlay.SetChild(&mw.contentAreaBox.Widget)
71+
72+ mw.assembleLayout()
73+
74+ mw.window.SetChild(&mw.rootBox.Widget)
75+
76+ return mw, nil
77+}
78+
79+func (mw *MainWindow) initLayoutWidgets() error {
64 mw.rootBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)80 mw.rootBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
65 if mw.rootBox == nil {81 if mw.rootBox == nil {
66 mw.window.Unref()82 mw.window.Unref()
67- return nil, ErrWidgetCreationFailed("rootBox")83+ return ErrWidgetCreationFailed("rootBox")
68 }84 }
69 mw.rootBox.SetHexpand(true)85 mw.rootBox.SetHexpand(true)
70 mw.rootBox.SetVexpand(true)86 mw.rootBox.SetVexpand(true)
@@ -74,15 +90,22 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
74 if mw.tabBar == nil {90 if mw.tabBar == nil {
75 mw.rootBox.Unref()91 mw.rootBox.Unref()
76 mw.window.Unref()92 mw.window.Unref()
77- return nil, ErrWidgetCreationFailed("tabBar")93+ return ErrWidgetCreationFailed("tabBar")
94+ }
95+
96+ if err := mw.initContentWidgets(); err != nil {
97+ return err
78 }98 }
99+ return nil
100+}
79 101
102+func (mw *MainWindow) initContentWidgets() error {
80 mw.contentOverlay = gtk.NewOverlay()103 mw.contentOverlay = gtk.NewOverlay()
81 if mw.contentOverlay == nil {104 if mw.contentOverlay == nil {
82 mw.tabBar.Destroy()105 mw.tabBar.Destroy()
83 mw.rootBox.Unref()106 mw.rootBox.Unref()
84 mw.window.Unref()107 mw.window.Unref()
85- return nil, ErrWidgetCreationFailed("contentOverlay")108+ return ErrWidgetCreationFailed("contentOverlay")
86 }109 }
87 mw.contentOverlay.SetHexpand(true)110 mw.contentOverlay.SetHexpand(true)
88 mw.contentOverlay.SetVexpand(true)111 mw.contentOverlay.SetVexpand(true)
@@ -95,12 +118,19 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
95 mw.tabBar.Destroy()118 mw.tabBar.Destroy()
96 mw.rootBox.Unref()119 mw.rootBox.Unref()
97 mw.window.Unref()120 mw.window.Unref()
98- return nil, ErrWidgetCreationFailed("contentAreaBox")121+ return ErrWidgetCreationFailed("contentAreaBox")
99 }122 }
100 mw.contentAreaBox.SetHexpand(true)123 mw.contentAreaBox.SetHexpand(true)
101 mw.contentAreaBox.SetVexpand(true)124 mw.contentAreaBox.SetVexpand(true)
102 mw.contentAreaBox.SetVisible(true)125 mw.contentAreaBox.SetVisible(true)
103 126
127+ if err := mw.initContentBoxes(); err != nil {
128+ return err
129+ }
130+ return nil
131+}
132+
133+func (mw *MainWindow) initContentBoxes() error {
104 // Main content box (vertical) for workspace content.134 // Main content box (vertical) for workspace content.
105 mw.mainContentBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)135 mw.mainContentBox = gtk.NewBox(gtk.OrientationVerticalValue, 0)
106 if mw.mainContentBox == nil {136 if mw.mainContentBox == nil {
@@ -109,7 +139,7 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
109 mw.tabBar.Destroy()139 mw.tabBar.Destroy()
110 mw.rootBox.Unref()140 mw.rootBox.Unref()
111 mw.window.Unref()141 mw.window.Unref()
112- return nil, ErrWidgetCreationFailed("mainContentBox")142+ return ErrWidgetCreationFailed("mainContentBox")
113 }143 }
114 mw.mainContentBox.SetHexpand(true)144 mw.mainContentBox.SetHexpand(true)
115 mw.mainContentBox.SetVexpand(true)145 mw.mainContentBox.SetVexpand(true)
@@ -124,21 +154,13 @@ func New(ctx context.Context, app *gtk.Application, tabBarPosition string) (*Mai
124 mw.tabBar.Destroy()154 mw.tabBar.Destroy()
125 mw.rootBox.Unref()155 mw.rootBox.Unref()
126 mw.window.Unref()156 mw.window.Unref()
127- return nil, ErrWidgetCreationFailed("sidebarBox")157+ return ErrWidgetCreationFailed("sidebarBox")
128 }158 }
129 mw.sidebarBox.SetHexpand(false)159 mw.sidebarBox.SetHexpand(false)
130 mw.sidebarBox.SetVexpand(true)160 mw.sidebarBox.SetVexpand(true)
131 mw.sidebarBox.SetVisible(false)161 mw.sidebarBox.SetVisible(false)
132 162
133- mw.contentAreaBox.Append(&mw.mainContentBox.Widget)163+ return nil
134- mw.contentAreaBox.Append(&mw.sidebarBox.Widget)
135- mw.contentOverlay.SetChild(&mw.contentAreaBox.Widget)
136-
137- mw.assembleLayout()
138-
139- mw.window.SetChild(&mw.rootBox.Widget)
140-
141- return mw, nil
142 }164 }
143 165
144 func (mw *MainWindow) assembleLayout() {166 func (mw *MainWindow) assembleLayout() {

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