detached 4622b8c

feat(keyboard): implement zoom actions (in/out/reset) with omnibox integration

commit
4622b8cf594b4dfae003605ca4d109c75eb59308
author
brice · · 2025-12-15 18:41 UTC
committer
brice · · 2025-12-15 22:45 UTC
parent
f163db7
signature
unsigned
3 files · +82 -4
M internal/ui/app.go +2 -0
@@ -196,6 +196,7 @@ func (a *App) onActivate(ctx context.Context) {
196196 Shortcuts: a.deps.Config.SearchShortcuts,
197197 DefaultSearch: a.deps.Config.DefaultSearchEngine,
198198 InitialBehavior: a.deps.Config.Omnibox.InitialBehavior,
199+ UIScale: a.deps.Config.DefaultUIScale,
199200 })
200201 if a.omnibox != nil {
201202 a.omnibox.SetOnNavigate(func(url string) {
@@ -299,6 +300,7 @@ func (a *App) initCoordinators(ctx context.Context) {
299300 a.tabCoord,
300301 a.wsCoord,
301302 a.navCoord,
303+ a.deps.ZoomUC,
302304 )
303305 a.kbDispatcher.SetOnQuit(a.Quit)
304306
@@ -196,6 +196,7 @@ func (a *App) onActivate(ctx context.Context) {
196 Shortcuts: a.deps.Config.SearchShortcuts,196 Shortcuts: a.deps.Config.SearchShortcuts,
197 DefaultSearch: a.deps.Config.DefaultSearchEngine,197 DefaultSearch: a.deps.Config.DefaultSearchEngine,
198 InitialBehavior: a.deps.Config.Omnibox.InitialBehavior,198 InitialBehavior: a.deps.Config.Omnibox.InitialBehavior,
199+ UIScale: a.deps.Config.DefaultUIScale,
199 })200 })
200 if a.omnibox != nil {201 if a.omnibox != nil {
201 a.omnibox.SetOnNavigate(func(url string) {202 a.omnibox.SetOnNavigate(func(url string) {
@@ -299,6 +300,7 @@ func (a *App) initCoordinators(ctx context.Context) {
299 a.tabCoord,300 a.tabCoord,
300 a.wsCoord,301 a.wsCoord,
301 a.navCoord,302 a.navCoord,
303+ a.deps.ZoomUC,
302 )304 )
303 a.kbDispatcher.SetOnQuit(a.Quit)305 a.kbDispatcher.SetOnQuit(a.Quit)
304 306
M internal/ui/coordinator/navigation.go +12 -0
@@ -204,3 +204,15 @@ func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID e
204204 log.Warn().Err(err).Str("url", url).Msg("failed to update history title")
205205 }
206206 }
207+
208+// ActiveWebView returns the WebView for the active pane (for zoom operations).
209+func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
210+ return c.contentCoord.ActiveWebView(ctx)
211+}
212+
213+// NotifyZoomChanged updates the omnibox zoom indicator.
214+func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
215+ if c.omnibox != nil {
216+ c.omnibox.UpdateZoomIndicator(factor)
217+ }
218+}
@@ -204,3 +204,15 @@ func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID e
204 log.Warn().Err(err).Str("url", url).Msg("failed to update history title")204 log.Warn().Err(err).Str("url", url).Msg("failed to update history title")
205 }205 }
206 }206 }
207+
208+// ActiveWebView returns the WebView for the active pane (for zoom operations).
209+func (c *NavigationCoordinator) ActiveWebView(ctx context.Context) *webkit.WebView {
210+ return c.contentCoord.ActiveWebView(ctx)
211+}
212+
213+// NotifyZoomChanged updates the omnibox zoom indicator.
214+func (c *NavigationCoordinator) NotifyZoomChanged(ctx context.Context, factor float64) {
215+ if c.omnibox != nil {
216+ c.omnibox.UpdateZoomIndicator(factor)
217+ }
218+}
M internal/ui/dispatcher/keyboard.go +68 -4
@@ -4,6 +4,7 @@ import (
44 "context"
55
66 "github.com/bnema/dumber/internal/application/usecase"
7+ "github.com/bnema/dumber/internal/domain/entity"
78 "github.com/bnema/dumber/internal/logging"
89 "github.com/bnema/dumber/internal/ui/coordinator"
910 "github.com/bnema/dumber/internal/ui/input"
@@ -14,6 +15,7 @@ type KeyboardDispatcher struct {
1415 tabCoord *coordinator.TabCoordinator
1516 wsCoord *coordinator.WorkspaceCoordinator
1617 navCoord *coordinator.NavigationCoordinator
18+ zoomUC *usecase.ManageZoomUseCase
1719 onQuit func()
1820 }
1921
@@ -23,6 +25,7 @@ func NewKeyboardDispatcher(
2325 tabCoord *coordinator.TabCoordinator,
2426 wsCoord *coordinator.WorkspaceCoordinator,
2527 navCoord *coordinator.NavigationCoordinator,
28+ zoomUC *usecase.ManageZoomUseCase,
2629 ) *KeyboardDispatcher {
2730 log := logging.FromContext(ctx)
2831 log.Debug().Msg("creating keyboard dispatcher")
@@ -31,6 +34,7 @@ func NewKeyboardDispatcher(
3134 tabCoord: tabCoord,
3235 wsCoord: wsCoord,
3336 navCoord: navCoord,
37+ zoomUC: zoomUC,
3438 }
3539 }
3640
@@ -94,13 +98,13 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action)
9498 case input.ActionHardReload:
9599 return d.navCoord.HardReload(ctx)
96100
97- // Zoom (stub - will be implemented later)
101+ // Zoom actions
98102 case input.ActionZoomIn:
99- log.Debug().Msg("zoom in action (not yet implemented)")
103+ return d.handleZoom(ctx, "in")
100104 case input.ActionZoomOut:
101- log.Debug().Msg("zoom out action (not yet implemented)")
105+ return d.handleZoom(ctx, "out")
102106 case input.ActionZoomReset:
103- log.Debug().Msg("zoom reset action (not yet implemented)")
107+ return d.handleZoom(ctx, "reset")
104108
105109 // UI
106110 case input.ActionOpenOmnibox:
@@ -122,3 +126,63 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action)
122126
123127 return nil
124128 }
129+
130+// handleZoom processes zoom in/out/reset actions for the active WebView.
131+func (d *KeyboardDispatcher) handleZoom(ctx context.Context, action string) error {
132+ log := logging.FromContext(ctx)
133+
134+ if d.zoomUC == nil {
135+ log.Warn().Msg("zoom use case not available")
136+ return nil
137+ }
138+
139+ wv := d.navCoord.ActiveWebView(ctx)
140+ if wv == nil {
141+ log.Debug().Msg("no active webview for zoom")
142+ return nil
143+ }
144+
145+ domain, err := usecase.ExtractDomain(wv.URI())
146+ if err != nil {
147+ log.Debug().Str("uri", wv.URI()).Msg("cannot extract domain for zoom")
148+ return nil
149+ }
150+
151+ current := wv.GetZoomLevel()
152+ var newZoom *entity.ZoomLevel
153+
154+ switch action {
155+ case "in":
156+ newZoom, err = d.zoomUC.ZoomIn(ctx, domain, current)
157+ case "out":
158+ newZoom, err = d.zoomUC.ZoomOut(ctx, domain, current)
159+ case "reset":
160+ err = d.zoomUC.ResetZoom(ctx, domain)
161+ if err == nil {
162+ newZoom = entity.NewZoomLevel(domain, d.zoomUC.DefaultZoom())
163+ }
164+ }
165+
166+ if err != nil {
167+ log.Error().Err(err).Str("action", action).Msg("zoom action failed")
168+ return err
169+ }
170+
171+ if newZoom != nil {
172+ if setErr := wv.SetZoomLevel(ctx, newZoom.ZoomFactor); setErr != nil {
173+ log.Error().Err(setErr).Float64("zoom", newZoom.ZoomFactor).Msg("failed to apply zoom")
174+ return setErr
175+ }
176+
177+ // Notify omnibox to update zoom indicator
178+ d.navCoord.NotifyZoomChanged(ctx, newZoom.ZoomFactor)
179+
180+ log.Debug().
181+ Str("domain", domain).
182+ Str("action", action).
183+ Float64("zoom", newZoom.ZoomFactor).
184+ Msg("zoom applied")
185+ }
186+
187+ return nil
188+}
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 5
6 "github.com/bnema/dumber/internal/application/usecase"6 "github.com/bnema/dumber/internal/application/usecase"
7+ "github.com/bnema/dumber/internal/domain/entity"
7 "github.com/bnema/dumber/internal/logging"8 "github.com/bnema/dumber/internal/logging"
8 "github.com/bnema/dumber/internal/ui/coordinator"9 "github.com/bnema/dumber/internal/ui/coordinator"
9 "github.com/bnema/dumber/internal/ui/input"10 "github.com/bnema/dumber/internal/ui/input"
@@ -14,6 +15,7 @@ type KeyboardDispatcher struct {
14 tabCoord *coordinator.TabCoordinator15 tabCoord *coordinator.TabCoordinator
15 wsCoord *coordinator.WorkspaceCoordinator16 wsCoord *coordinator.WorkspaceCoordinator
16 navCoord *coordinator.NavigationCoordinator17 navCoord *coordinator.NavigationCoordinator
18+ zoomUC *usecase.ManageZoomUseCase
17 onQuit func()19 onQuit func()
18 }20 }
19 21
@@ -23,6 +25,7 @@ func NewKeyboardDispatcher(
23 tabCoord *coordinator.TabCoordinator,25 tabCoord *coordinator.TabCoordinator,
24 wsCoord *coordinator.WorkspaceCoordinator,26 wsCoord *coordinator.WorkspaceCoordinator,
25 navCoord *coordinator.NavigationCoordinator,27 navCoord *coordinator.NavigationCoordinator,
28+ zoomUC *usecase.ManageZoomUseCase,
26 ) *KeyboardDispatcher {29 ) *KeyboardDispatcher {
27 log := logging.FromContext(ctx)30 log := logging.FromContext(ctx)
28 log.Debug().Msg("creating keyboard dispatcher")31 log.Debug().Msg("creating keyboard dispatcher")
@@ -31,6 +34,7 @@ func NewKeyboardDispatcher(
31 tabCoord: tabCoord,34 tabCoord: tabCoord,
32 wsCoord: wsCoord,35 wsCoord: wsCoord,
33 navCoord: navCoord,36 navCoord: navCoord,
37+ zoomUC: zoomUC,
34 }38 }
35 }39 }
36 40
@@ -94,13 +98,13 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action)
94 case input.ActionHardReload:98 case input.ActionHardReload:
95 return d.navCoord.HardReload(ctx)99 return d.navCoord.HardReload(ctx)
96 100
97- // Zoom (stub - will be implemented later)101+ // Zoom actions
98 case input.ActionZoomIn:102 case input.ActionZoomIn:
99- log.Debug().Msg("zoom in action (not yet implemented)")103+ return d.handleZoom(ctx, "in")
100 case input.ActionZoomOut:104 case input.ActionZoomOut:
101- log.Debug().Msg("zoom out action (not yet implemented)")105+ return d.handleZoom(ctx, "out")
102 case input.ActionZoomReset:106 case input.ActionZoomReset:
103- log.Debug().Msg("zoom reset action (not yet implemented)")107+ return d.handleZoom(ctx, "reset")
104 108
105 // UI109 // UI
106 case input.ActionOpenOmnibox:110 case input.ActionOpenOmnibox:
@@ -122,3 +126,63 @@ func (d *KeyboardDispatcher) Dispatch(ctx context.Context, action input.Action)
122 126
123 return nil127 return nil
124 }128 }
129+
130+// handleZoom processes zoom in/out/reset actions for the active WebView.
131+func (d *KeyboardDispatcher) handleZoom(ctx context.Context, action string) error {
132+ log := logging.FromContext(ctx)
133+
134+ if d.zoomUC == nil {
135+ log.Warn().Msg("zoom use case not available")
136+ return nil
137+ }
138+
139+ wv := d.navCoord.ActiveWebView(ctx)
140+ if wv == nil {
141+ log.Debug().Msg("no active webview for zoom")
142+ return nil
143+ }
144+
145+ domain, err := usecase.ExtractDomain(wv.URI())
146+ if err != nil {
147+ log.Debug().Str("uri", wv.URI()).Msg("cannot extract domain for zoom")
148+ return nil
149+ }
150+
151+ current := wv.GetZoomLevel()
152+ var newZoom *entity.ZoomLevel
153+
154+ switch action {
155+ case "in":
156+ newZoom, err = d.zoomUC.ZoomIn(ctx, domain, current)
157+ case "out":
158+ newZoom, err = d.zoomUC.ZoomOut(ctx, domain, current)
159+ case "reset":
160+ err = d.zoomUC.ResetZoom(ctx, domain)
161+ if err == nil {
162+ newZoom = entity.NewZoomLevel(domain, d.zoomUC.DefaultZoom())
163+ }
164+ }
165+
166+ if err != nil {
167+ log.Error().Err(err).Str("action", action).Msg("zoom action failed")
168+ return err
169+ }
170+
171+ if newZoom != nil {
172+ if setErr := wv.SetZoomLevel(ctx, newZoom.ZoomFactor); setErr != nil {
173+ log.Error().Err(setErr).Float64("zoom", newZoom.ZoomFactor).Msg("failed to apply zoom")
174+ return setErr
175+ }
176+
177+ // Notify omnibox to update zoom indicator
178+ d.navCoord.NotifyZoomChanged(ctx, newZoom.ZoomFactor)
179+
180+ log.Debug().
181+ Str("domain", domain).
182+ Str("action", action).
183+ Float64("zoom", newZoom.ZoomFactor).
184+ Msg("zoom applied")
185+ }
186+
187+ return nil
188+}

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