detached 4baeb0b

feat(ui): integrate download handling with toaster notifications

commit
4baeb0b429b004ca8d1a5f208d85d5960facdece
author
brice · · 2026-01-07 09:55 UTC
parent
d9269b5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +69 -0
M cmd/dumber/main.go +2 -0
@@ -25,6 +25,7 @@ import (
2525 "github.com/bnema/dumber/internal/infrastructure/idle"
2626 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
2727 "github.com/bnema/dumber/internal/infrastructure/updater"
28+ "github.com/bnema/dumber/internal/infrastructure/xdg"
2829 "github.com/bnema/dumber/internal/logging"
2930 "github.com/bnema/dumber/internal/ui"
3031 "github.com/bnema/dumber/internal/ui/theme"
@@ -428,6 +429,7 @@ func buildUIDependencies(
428429 Theme: themeManager,
429430 ColorResolver: colorResolver,
430431 AdwaitaDetector: adwaitaDetector,
432+ XDG: xdg.New(),
431433 WebContext: stack.Context,
432434 Pool: stack.Pool,
433435 Settings: stack.Settings,
@@ -25,6 +25,7 @@ import (
25 "github.com/bnema/dumber/internal/infrastructure/idle"25 "github.com/bnema/dumber/internal/infrastructure/idle"
26 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"26 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
27 "github.com/bnema/dumber/internal/infrastructure/updater"27 "github.com/bnema/dumber/internal/infrastructure/updater"
28+ "github.com/bnema/dumber/internal/infrastructure/xdg"
28 "github.com/bnema/dumber/internal/logging"29 "github.com/bnema/dumber/internal/logging"
29 "github.com/bnema/dumber/internal/ui"30 "github.com/bnema/dumber/internal/ui"
30 "github.com/bnema/dumber/internal/ui/theme"31 "github.com/bnema/dumber/internal/ui/theme"
@@ -428,6 +429,7 @@ func buildUIDependencies(
428 Theme: themeManager,429 Theme: themeManager,
429 ColorResolver: colorResolver,430 ColorResolver: colorResolver,
430 AdwaitaDetector: adwaitaDetector,431 AdwaitaDetector: adwaitaDetector,
432+ XDG: xdg.New(),
431 WebContext: stack.Context,433 WebContext: stack.Context,
432 Pool: stack.Pool,434 Pool: stack.Pool,
433 Settings: stack.Settings,435 Settings: stack.Settings,
M internal/ui/app.go +64 -0
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "fmt"
77 "os"
8+ "path/filepath"
89 "sync"
910
1011 "github.com/bnema/dumber/internal/application/port"
@@ -240,6 +241,7 @@ func (a *App) onActivate(ctx context.Context) {
240241 a.initAppToasterOverlay()
241242 a.initFocusAndBorderOverlay()
242243 a.initAccentPicker(ctx)
244+ a.initDownloadHandler(ctx)
243245
244246 a.initCoordinators(ctx)
245247 a.initKeyboardHandler(ctx)
@@ -453,6 +455,68 @@ func (a *App) initAccentPicker(ctx context.Context) {
453455 log.Debug().Msg("accent picker initialized")
454456 }
455457
458+func (a *App) initDownloadHandler(ctx context.Context) {
459+ log := logging.FromContext(ctx)
460+
461+ if a.deps == nil || a.deps.WebContext == nil {
462+ log.Debug().Msg("WebContext not available, skipping download handler")
463+ return
464+ }
465+
466+ // Determine download path from config, fallback to XDG.
467+ downloadPath := ""
468+ if a.deps.Config != nil {
469+ downloadPath = a.deps.Config.Downloads.Path
470+ }
471+ if downloadPath == "" && a.deps.XDG != nil {
472+ var err error
473+ downloadPath, err = a.deps.XDG.DownloadDir()
474+ if err != nil {
475+ log.Warn().Err(err).Msg("failed to get XDG download dir")
476+ }
477+ }
478+ if downloadPath == "" {
479+ // Final fallback to ~/Downloads.
480+ home, _ := os.UserHomeDir()
481+ downloadPath = filepath.Join(home, "Downloads")
482+ }
483+
484+ // Create download event adapter to show toasts.
485+ eventAdapter := &downloadEventAdapter{app: a, ctx: ctx}
486+
487+ // Create and wire the download handler.
488+ handler := webkit.NewDownloadHandler(downloadPath, eventAdapter)
489+ a.deps.WebContext.SetDownloadHandler(ctx, handler)
490+
491+ log.Info().Str("path", downloadPath).Msg("download handler initialized")
492+}
493+
494+// downloadEventAdapter implements port.DownloadEventHandler and shows toasts.
495+type downloadEventAdapter struct {
496+ app *App
497+ ctx context.Context
498+}
499+
500+func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.DownloadEvent) {
501+ // Must schedule on GTK main thread.
502+ cb := glib.SourceFunc(func(_ uintptr) bool {
503+ if d.app.appToaster == nil {
504+ return false
505+ }
506+
507+ switch event.Type {
508+ case port.DownloadEventStarted:
509+ d.app.appToaster.Show(ctx, "Download started: "+event.Filename, component.ToastInfo)
510+ case port.DownloadEventFinished:
511+ d.app.appToaster.Show(ctx, "Download complete: "+event.Filename, component.ToastSuccess)
512+ case port.DownloadEventFailed:
513+ d.app.appToaster.Show(ctx, "Download failed: "+event.Filename, component.ToastError)
514+ }
515+ return false
516+ })
517+ glib.IdleAdd(&cb, 0)
518+}
519+
456520 func (a *App) initKeyboardHandler(ctx context.Context) {
457521 if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil {
458522 return
@@ -5,6 +5,7 @@ import (
5 "errors"5 "errors"
6 "fmt"6 "fmt"
7 "os"7 "os"
8+ "path/filepath"
8 "sync"9 "sync"
9 10
10 "github.com/bnema/dumber/internal/application/port"11 "github.com/bnema/dumber/internal/application/port"
@@ -240,6 +241,7 @@ func (a *App) onActivate(ctx context.Context) {
240 a.initAppToasterOverlay()241 a.initAppToasterOverlay()
241 a.initFocusAndBorderOverlay()242 a.initFocusAndBorderOverlay()
242 a.initAccentPicker(ctx)243 a.initAccentPicker(ctx)
244+ a.initDownloadHandler(ctx)
243 245
244 a.initCoordinators(ctx)246 a.initCoordinators(ctx)
245 a.initKeyboardHandler(ctx)247 a.initKeyboardHandler(ctx)
@@ -453,6 +455,68 @@ func (a *App) initAccentPicker(ctx context.Context) {
453 log.Debug().Msg("accent picker initialized")455 log.Debug().Msg("accent picker initialized")
454 }456 }
455 457
458+func (a *App) initDownloadHandler(ctx context.Context) {
459+ log := logging.FromContext(ctx)
460+
461+ if a.deps == nil || a.deps.WebContext == nil {
462+ log.Debug().Msg("WebContext not available, skipping download handler")
463+ return
464+ }
465+
466+ // Determine download path from config, fallback to XDG.
467+ downloadPath := ""
468+ if a.deps.Config != nil {
469+ downloadPath = a.deps.Config.Downloads.Path
470+ }
471+ if downloadPath == "" && a.deps.XDG != nil {
472+ var err error
473+ downloadPath, err = a.deps.XDG.DownloadDir()
474+ if err != nil {
475+ log.Warn().Err(err).Msg("failed to get XDG download dir")
476+ }
477+ }
478+ if downloadPath == "" {
479+ // Final fallback to ~/Downloads.
480+ home, _ := os.UserHomeDir()
481+ downloadPath = filepath.Join(home, "Downloads")
482+ }
483+
484+ // Create download event adapter to show toasts.
485+ eventAdapter := &downloadEventAdapter{app: a, ctx: ctx}
486+
487+ // Create and wire the download handler.
488+ handler := webkit.NewDownloadHandler(downloadPath, eventAdapter)
489+ a.deps.WebContext.SetDownloadHandler(ctx, handler)
490+
491+ log.Info().Str("path", downloadPath).Msg("download handler initialized")
492+}
493+
494+// downloadEventAdapter implements port.DownloadEventHandler and shows toasts.
495+type downloadEventAdapter struct {
496+ app *App
497+ ctx context.Context
498+}
499+
500+func (d *downloadEventAdapter) OnDownloadEvent(ctx context.Context, event port.DownloadEvent) {
501+ // Must schedule on GTK main thread.
502+ cb := glib.SourceFunc(func(_ uintptr) bool {
503+ if d.app.appToaster == nil {
504+ return false
505+ }
506+
507+ switch event.Type {
508+ case port.DownloadEventStarted:
509+ d.app.appToaster.Show(ctx, "Download started: "+event.Filename, component.ToastInfo)
510+ case port.DownloadEventFinished:
511+ d.app.appToaster.Show(ctx, "Download complete: "+event.Filename, component.ToastSuccess)
512+ case port.DownloadEventFailed:
513+ d.app.appToaster.Show(ctx, "Download failed: "+event.Filename, component.ToastError)
514+ }
515+ return false
516+ })
517+ glib.IdleAdd(&cb, 0)
518+}
519+
456 func (a *App) initKeyboardHandler(ctx context.Context) {520 func (a *App) initKeyboardHandler(ctx context.Context) {
457 if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil {521 if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil {
458 return522 return
M internal/ui/deps.go +3 -0
@@ -31,6 +31,9 @@ type Dependencies struct {
3131 ColorResolver port.ColorSchemeResolver
3232 AdwaitaDetector *colorscheme.AdwaitaDetector
3333
34+ // XDG paths
35+ XDG port.XDGPaths
36+
3437 // WebKit infrastructure
3538 WebContext *webkit.WebKitContext
3639 Pool *webkit.WebViewPool
@@ -31,6 +31,9 @@ type Dependencies struct {
31 ColorResolver port.ColorSchemeResolver31 ColorResolver port.ColorSchemeResolver
32 AdwaitaDetector *colorscheme.AdwaitaDetector32 AdwaitaDetector *colorscheme.AdwaitaDetector
33 33
34+ // XDG paths
35+ XDG port.XDGPaths
36+
34 // WebKit infrastructure37 // WebKit infrastructure
35 WebContext *webkit.WebKitContext38 WebContext *webkit.WebKitContext
36 Pool *webkit.WebViewPool39 Pool *webkit.WebViewPool

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