detached 1c2f59f

feat(history): normalize URLs to prevent duplicate entries

commit
1c2f59f00ecc660fca744af513cfd74b4153cbe7
author
bnema · · 2025-12-16 21:41 UTC
parent
b9f594c
signature
unsigned
1 file · +25 -0
M internal/application/usecase/navigate.go +25 -0
@@ -3,6 +3,7 @@ package usecase
33 import (
44 "context"
55 "fmt"
6+ "strings"
67
78 "github.com/bnema/dumber/internal/application/port"
89 "github.com/bnema/dumber/internal/domain/entity"
@@ -99,6 +100,9 @@ func (uc *NavigateUseCase) Execute(ctx context.Context, input NavigateInput) (*N
99100 func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) {
100101 log := logging.FromContext(ctx)
101102
103+ // Normalize URL to avoid duplicates (e.g., github.com vs github.com/)
104+ url = normalizeURLForHistory(url)
105+
102106 // Check if entry exists
103107 existing, err := uc.historyRepo.FindByURL(ctx, url)
104108 if err != nil {
@@ -123,6 +127,9 @@ func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) {
123127 // UpdateHistoryTitle updates the title of a history entry after page load.
124128 func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error {
125129 log := logging.FromContext(ctx)
130+
131+ // Normalize URL to match storage (e.g., github.com/ -> github.com)
132+ url = normalizeURLForHistory(url)
126133 log.Debug().Str("url", url).Str("title", title).Msg("updating history title")
127134
128135 entry, err := uc.historyRepo.FindByURL(ctx, url)
@@ -191,3 +198,21 @@ func (uc *NavigateUseCase) Stop(ctx context.Context, webview port.WebView) error
191198 log.Debug().Msg("stopping page load")
192199 return webview.Stop(ctx)
193200 }
201+
202+// normalizeURLForHistory normalizes a URL for history storage.
203+// Strips trailing slash to avoid duplicates like github.com vs github.com/
204+func normalizeURLForHistory(url string) string {
205+ // Don't strip if URL is just the protocol + domain (e.g., "https://github.com")
206+ // Only strip trailing slash from paths
207+ if strings.HasSuffix(url, "/") {
208+ // Check if it's just domain with trailing slash (e.g., "https://github.com/")
209+ // by counting slashes - protocol has 2, domain adds 0, path adds more
210+ if strings.Count(url, "/") == 3 {
211+ // It's "https://domain.com/" - strip the trailing slash
212+ return strings.TrimSuffix(url, "/")
213+ }
214+ // For paths like "https://github.com/user/" - also strip
215+ return strings.TrimSuffix(url, "/")
216+ }
217+ return url
218+}
@@ -3,6 +3,7 @@ package usecase
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "strings"
6 7
7 "github.com/bnema/dumber/internal/application/port"8 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"9 "github.com/bnema/dumber/internal/domain/entity"
@@ -99,6 +100,9 @@ func (uc *NavigateUseCase) Execute(ctx context.Context, input NavigateInput) (*N
99 func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) {100 func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) {
100 log := logging.FromContext(ctx)101 log := logging.FromContext(ctx)
101 102
103+ // Normalize URL to avoid duplicates (e.g., github.com vs github.com/)
104+ url = normalizeURLForHistory(url)
105+
102 // Check if entry exists106 // Check if entry exists
103 existing, err := uc.historyRepo.FindByURL(ctx, url)107 existing, err := uc.historyRepo.FindByURL(ctx, url)
104 if err != nil {108 if err != nil {
@@ -123,6 +127,9 @@ func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) {
123 // UpdateHistoryTitle updates the title of a history entry after page load.127 // UpdateHistoryTitle updates the title of a history entry after page load.
124 func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error {128 func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error {
125 log := logging.FromContext(ctx)129 log := logging.FromContext(ctx)
130+
131+ // Normalize URL to match storage (e.g., github.com/ -> github.com)
132+ url = normalizeURLForHistory(url)
126 log.Debug().Str("url", url).Str("title", title).Msg("updating history title")133 log.Debug().Str("url", url).Str("title", title).Msg("updating history title")
127 134
128 entry, err := uc.historyRepo.FindByURL(ctx, url)135 entry, err := uc.historyRepo.FindByURL(ctx, url)
@@ -191,3 +198,21 @@ func (uc *NavigateUseCase) Stop(ctx context.Context, webview port.WebView) error
191 log.Debug().Msg("stopping page load")198 log.Debug().Msg("stopping page load")
192 return webview.Stop(ctx)199 return webview.Stop(ctx)
193 }200 }
201+
202+// normalizeURLForHistory normalizes a URL for history storage.
203+// Strips trailing slash to avoid duplicates like github.com vs github.com/
204+func normalizeURLForHistory(url string) string {
205+ // Don't strip if URL is just the protocol + domain (e.g., "https://github.com")
206+ // Only strip trailing slash from paths
207+ if strings.HasSuffix(url, "/") {
208+ // Check if it's just domain with trailing slash (e.g., "https://github.com/")
209+ // by counting slashes - protocol has 2, domain adds 0, path adds more
210+ if strings.Count(url, "/") == 3 {
211+ // It's "https://domain.com/" - strip the trailing slash
212+ return strings.TrimSuffix(url, "/")
213+ }
214+ // For paths like "https://github.com/user/" - also strip
215+ return strings.TrimSuffix(url, "/")
216+ }
217+ return url
218+}

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