detached a6b10e0

Merge pull request #118 from bnema/fix/review-issues-117

fix: address code review issues from PR #117
commit
a6b10e03e05e17a4131df4bb9eeb6e97d5d2f8ba
author
bnema · · 2026-01-08 17:44 UTC
committer
GitHub · · 2026-01-08 17:44 UTC
merge of
dd58ad7 + 9a68bc3 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
4 files · +29 -18
M internal/infrastructure/webkit/download_handler.go +16 -9
@@ -33,6 +33,13 @@ func NewDownloadHandler(downloadPath string, handler port.DownloadEventHandler)
3333 }
3434 }
3535
36+// downloadState tracks the state of a single download to prevent duplicate notifications.
37+// This is allocated per-download to ensure proper isolation between concurrent downloads.
38+type downloadState struct {
39+ failed bool
40+ mu sync.Mutex
41+}
42+
3643 // HandleDownload sets up signal handlers for a new download.
3744 func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.Download) {
3845 log := logging.FromContext(ctx)
@@ -42,9 +49,9 @@ func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.D
4249 eventHandler := h.eventHandler
4350 h.mu.RUnlock()
4451
45- // Track if download failed (to avoid duplicate finished notifications).
46- var downloadFailed bool
47- var failedMu sync.Mutex
52+ // Track download state in a dedicated struct to ensure proper isolation.
53+ // Each download gets its own state to prevent interference between concurrent downloads.
54+ state := &downloadState{}
4855
4956 // Handle decide-destination signal to set download path.
5057 //nolint:unparam // bool return is required by WebKit's signal signature (false = handled synchronously)
@@ -89,9 +96,9 @@ func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.D
8996
9097 // Handle failed signal.
9198 failedCb := func(d webkit.Download, _ uintptr) {
92- failedMu.Lock()
93- downloadFailed = true
94- failedMu.Unlock()
99+ state.mu.Lock()
100+ state.failed = true
101+ state.mu.Unlock()
95102
96103 dest := d.GetDestination()
97104 filename := extractFilename(dest)
@@ -114,9 +121,9 @@ func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.D
114121
115122 // Handle finished signal.
116123 finishedCb := func(d webkit.Download) {
117- failedMu.Lock()
118- failed := downloadFailed
119- failedMu.Unlock()
124+ state.mu.Lock()
125+ failed := state.failed
126+ state.mu.Unlock()
120127
121128 // Don't send finished event if we already sent a failed event.
122129 if failed {
@@ -33,6 +33,13 @@ func NewDownloadHandler(downloadPath string, handler port.DownloadEventHandler)
33 }33 }
34 }34 }
35 35
36+// downloadState tracks the state of a single download to prevent duplicate notifications.
37+// This is allocated per-download to ensure proper isolation between concurrent downloads.
38+type downloadState struct {
39+ failed bool
40+ mu sync.Mutex
41+}
42+
36 // HandleDownload sets up signal handlers for a new download.43 // HandleDownload sets up signal handlers for a new download.
37 func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.Download) {44 func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.Download) {
38 log := logging.FromContext(ctx)45 log := logging.FromContext(ctx)
@@ -42,9 +49,9 @@ func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.D
42 eventHandler := h.eventHandler49 eventHandler := h.eventHandler
43 h.mu.RUnlock()50 h.mu.RUnlock()
44 51
45- // Track if download failed (to avoid duplicate finished notifications).52+ // Track download state in a dedicated struct to ensure proper isolation.
46- var downloadFailed bool53+ // Each download gets its own state to prevent interference between concurrent downloads.
47- var failedMu sync.Mutex54+ state := &downloadState{}
48 55
49 // Handle decide-destination signal to set download path.56 // Handle decide-destination signal to set download path.
50 //nolint:unparam // bool return is required by WebKit's signal signature (false = handled synchronously)57 //nolint:unparam // bool return is required by WebKit's signal signature (false = handled synchronously)
@@ -89,9 +96,9 @@ func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.D
89 96
90 // Handle failed signal.97 // Handle failed signal.
91 failedCb := func(d webkit.Download, _ uintptr) {98 failedCb := func(d webkit.Download, _ uintptr) {
92- failedMu.Lock()99+ state.mu.Lock()
93- downloadFailed = true100+ state.failed = true
94- failedMu.Unlock()101+ state.mu.Unlock()
95 102
96 dest := d.GetDestination()103 dest := d.GetDestination()
97 filename := extractFilename(dest)104 filename := extractFilename(dest)
@@ -114,9 +121,9 @@ func (h *DownloadHandler) HandleDownload(ctx context.Context, download *webkit.D
114 121
115 // Handle finished signal.122 // Handle finished signal.
116 finishedCb := func(d webkit.Download) {123 finishedCb := func(d webkit.Download) {
117- failedMu.Lock()124+ state.mu.Lock()
118- failed := downloadFailed125+ failed := state.failed
119- failedMu.Unlock()126+ state.mu.Unlock()
120 127
121 // Don't send finished event if we already sent a failed event.128 // Don't send finished event if we already sent a failed event.
122 if failed {129 if failed {
M internal/infrastructure/webkit/webview.go +2 -2
@@ -778,7 +778,7 @@ func (wv *WebView) GoBack(ctx context.Context) error {
778778 // Fall back to JavaScript history.back() for SPA navigation
779779 // This handles pushState/replaceState history that WebKit's BackForwardList doesn't track
780780 wv.RunJavaScript(ctx, "history.back()", "")
781- log.Debug().Int("webview_id", int(wv.id)).Msg("webview go back (js)")
781+ log.Debug().Int("webview_id", int(wv.id)).Msg("webview go back (js fallback)")
782782 return nil
783783 }
784784
@@ -800,7 +800,7 @@ func (wv *WebView) GoForward(ctx context.Context) error {
800800 // Fall back to JavaScript history.forward() for SPA navigation
801801 // This handles pushState/replaceState history that WebKit's BackForwardList doesn't track
802802 wv.RunJavaScript(ctx, "history.forward()", "")
803- log.Debug().Int("webview_id", int(wv.id)).Msg("webview go forward (js)")
803+ log.Debug().Int("webview_id", int(wv.id)).Msg("webview go forward (js fallback)")
804804 return nil
805805 }
806806
@@ -778,7 +778,7 @@ func (wv *WebView) GoBack(ctx context.Context) error {
778 // Fall back to JavaScript history.back() for SPA navigation778 // Fall back to JavaScript history.back() for SPA navigation
779 // This handles pushState/replaceState history that WebKit's BackForwardList doesn't track779 // This handles pushState/replaceState history that WebKit's BackForwardList doesn't track
780 wv.RunJavaScript(ctx, "history.back()", "")780 wv.RunJavaScript(ctx, "history.back()", "")
781- log.Debug().Int("webview_id", int(wv.id)).Msg("webview go back (js)")781+ log.Debug().Int("webview_id", int(wv.id)).Msg("webview go back (js fallback)")
782 return nil782 return nil
783 }783 }
784 784
@@ -800,7 +800,7 @@ func (wv *WebView) GoForward(ctx context.Context) error {
800 // Fall back to JavaScript history.forward() for SPA navigation800 // Fall back to JavaScript history.forward() for SPA navigation
801 // This handles pushState/replaceState history that WebKit's BackForwardList doesn't track801 // This handles pushState/replaceState history that WebKit's BackForwardList doesn't track
802 wv.RunJavaScript(ctx, "history.forward()", "")802 wv.RunJavaScript(ctx, "history.forward()", "")
803- log.Debug().Int("webview_id", int(wv.id)).Msg("webview go forward (js)")803+ log.Debug().Int("webview_id", int(wv.id)).Msg("webview go forward (js fallback)")
804 return nil804 return nil
805 }805 }
806 806
M internal/ui/app.go +8 -3
@@ -476,9 +476,14 @@ func (a *App) initDownloadHandler(ctx context.Context) {
476476 }
477477 }
478478 if downloadPath == "" {
479- // Final fallback to ~/Downloads.
480- home, _ := os.UserHomeDir()
481- downloadPath = filepath.Join(home, "Downloads")
479+ // Final fallback to ~/Downloads, with /tmp as last resort.
480+ home, err := os.UserHomeDir()
481+ if err != nil {
482+ log.Warn().Err(err).Msg("failed to get home dir, using /tmp for downloads")
483+ downloadPath = "/tmp"
484+ } else {
485+ downloadPath = filepath.Join(home, "Downloads")
486+ }
482487 }
483488
484489 // Create download event adapter to show toasts.
@@ -476,9 +476,14 @@ func (a *App) initDownloadHandler(ctx context.Context) {
476 }476 }
477 }477 }
478 if downloadPath == "" {478 if downloadPath == "" {
479- // Final fallback to ~/Downloads.479+ // Final fallback to ~/Downloads, with /tmp as last resort.
480- home, _ := os.UserHomeDir()480+ home, err := os.UserHomeDir()
481- downloadPath = filepath.Join(home, "Downloads")481+ if err != nil {
482+ log.Warn().Err(err).Msg("failed to get home dir, using /tmp for downloads")
483+ downloadPath = "/tmp"
484+ } else {
485+ downloadPath = filepath.Join(home, "Downloads")
486+ }
482 }487 }
483 488
484 // Create download event adapter to show toasts.489 // Create download event adapter to show toasts.
M internal/ui/coordinator/update.go +3 -4
@@ -99,11 +99,10 @@ func (c *UpdateCoordinator) checkAsync(ctx context.Context) {
9999 // showUpdateNotification displays a toast notification about the update.
100100 func (c *UpdateCoordinator) showUpdateNotification(ctx context.Context, result *usecase.CheckUpdateOutput) {
101101 var msg string
102- if result.CanAutoUpdate && c.config.Update.AutoDownload {
102+ switch {
103+ case result.CanAutoUpdate && c.config.Update.AutoDownload:
103104 msg = fmt.Sprintf("Downloading update %s...", result.LatestVersion)
104- } else if result.CanAutoUpdate {
105- msg = fmt.Sprintf("Update %s available", result.LatestVersion)
106- } else {
105+ default:
107106 msg = fmt.Sprintf("Update %s available", result.LatestVersion)
108107 }
109108
@@ -99,11 +99,10 @@ func (c *UpdateCoordinator) checkAsync(ctx context.Context) {
99 // showUpdateNotification displays a toast notification about the update.99 // showUpdateNotification displays a toast notification about the update.
100 func (c *UpdateCoordinator) showUpdateNotification(ctx context.Context, result *usecase.CheckUpdateOutput) {100 func (c *UpdateCoordinator) showUpdateNotification(ctx context.Context, result *usecase.CheckUpdateOutput) {
101 var msg string101 var msg string
102- if result.CanAutoUpdate && c.config.Update.AutoDownload {102+ switch {
103+ case result.CanAutoUpdate && c.config.Update.AutoDownload:
103 msg = fmt.Sprintf("Downloading update %s...", result.LatestVersion)104 msg = fmt.Sprintf("Downloading update %s...", result.LatestVersion)
104- } else if result.CanAutoUpdate {105+ default:
105- msg = fmt.Sprintf("Update %s available", result.LatestVersion)
106- } else {
107 msg = fmt.Sprintf("Update %s available", result.LatestVersion)106 msg = fmt.Sprintf("Update %s available", result.LatestVersion)
108 }107 }
109 108

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