detached 058f890

feat(shortcuts): add Ctrl+Shift+P print page shortcut

commit
058f890e1111790c4b562ab33f365accc12961ed
author
bnema · · 2026-01-06 18:12 UTC
committer
bnema · · 2026-01-06 19:04 UTC
parent
6a8b220
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +38 -0
M internal/infrastructure/webkit/webview.go +15 -0
@@ -950,6 +950,21 @@ func (wv *WebView) ShowDevTools() error {
950950 return nil
951951 }
952952
953+// Print opens the print dialog for the current page.
954+func (wv *WebView) Print() error {
955+ if wv.destroyed.Load() {
956+ return fmt.Errorf("webview %d is destroyed", wv.id)
957+ }
958+ printOp := webkit.NewPrintOperation(wv.inner)
959+ if printOp == nil {
960+ return fmt.Errorf("failed to create print operation for webview %d", wv.id)
961+ }
962+ // RunDialog with nil parent window - GTK will use the widget's toplevel
963+ printOp.RunDialog(nil)
964+ wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("print dialog opened")
965+ return nil
966+}
967+
953968 // IsDestroyed returns true if the WebView has been destroyed.
954969 func (wv *WebView) IsDestroyed() bool {
955970 return wv.destroyed.Load()
@@ -950,6 +950,21 @@ func (wv *WebView) ShowDevTools() error {
950 return nil950 return nil
951 }951 }
952 952
953+// Print opens the print dialog for the current page.
954+func (wv *WebView) Print() error {
955+ if wv.destroyed.Load() {
956+ return fmt.Errorf("webview %d is destroyed", wv.id)
957+ }
958+ printOp := webkit.NewPrintOperation(wv.inner)
959+ if printOp == nil {
960+ return fmt.Errorf("failed to create print operation for webview %d", wv.id)
961+ }
962+ // RunDialog with nil parent window - GTK will use the widget's toplevel
963+ printOp.RunDialog(nil)
964+ wv.logger.Debug().Uint64("id", uint64(wv.id)).Msg("print dialog opened")
965+ return nil
966+}
967+
953 // IsDestroyed returns true if the WebView has been destroyed.968 // IsDestroyed returns true if the WebView has been destroyed.
954 func (wv *WebView) IsDestroyed() bool {969 func (wv *WebView) IsDestroyed() bool {
955 return wv.destroyed.Load()970 return wv.destroyed.Load()
M internal/ui/coordinator/navigation.go +20 -0
@@ -200,6 +200,26 @@ func (c *NavigationCoordinator) OpenDevTools(ctx context.Context) error {
200200 return fmt.Errorf("webview does not support devtools")
201201 }
202202
203+// PrintPage opens the print dialog for the active WebView.
204+func (c *NavigationCoordinator) PrintPage(ctx context.Context) error {
205+ log := logging.FromContext(ctx)
206+
207+ wv := c.contentCoord.ActiveWebView(ctx)
208+ if wv == nil {
209+ log.Warn().Msg("no active webview for print")
210+ return fmt.Errorf("no active webview")
211+ }
212+
213+ log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening print dialog")
214+
215+ // Type assert to access Print (not in port.WebView interface)
216+ if webkitWV, ok := interface{}(wv).(*webkit.WebView); ok {
217+ return webkitWV.Print()
218+ }
219+
220+ return fmt.Errorf("webview does not support printing")
221+}
222+
203223 // UpdateHistoryTitle updates the title of a history entry after page load.
204224 func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
205225 log := logging.FromContext(ctx)
@@ -200,6 +200,26 @@ func (c *NavigationCoordinator) OpenDevTools(ctx context.Context) error {
200 return fmt.Errorf("webview does not support devtools")200 return fmt.Errorf("webview does not support devtools")
201 }201 }
202 202
203+// PrintPage opens the print dialog for the active WebView.
204+func (c *NavigationCoordinator) PrintPage(ctx context.Context) error {
205+ log := logging.FromContext(ctx)
206+
207+ wv := c.contentCoord.ActiveWebView(ctx)
208+ if wv == nil {
209+ log.Warn().Msg("no active webview for print")
210+ return fmt.Errorf("no active webview")
211+ }
212+
213+ log.Debug().Uint64("webview_id", uint64(wv.ID())).Msg("opening print dialog")
214+
215+ // Type assert to access Print (not in port.WebView interface)
216+ if webkitWV, ok := interface{}(wv).(*webkit.WebView); ok {
217+ return webkitWV.Print()
218+ }
219+
220+ return fmt.Errorf("webview does not support printing")
221+}
222+
203 // UpdateHistoryTitle updates the title of a history entry after page load.223 // UpdateHistoryTitle updates the title of a history entry after page load.
204 func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {224 func (c *NavigationCoordinator) UpdateHistoryTitle(ctx context.Context, paneID entity.PaneID, url, title string) {
205 log := logging.FromContext(ctx)225 log := logging.FromContext(ctx)
M internal/ui/dispatcher/keyboard.go +1 -0
@@ -179,6 +179,7 @@ func (d *KeyboardDispatcher) initActionHandlers() {
179179 input.ActionGoForward: d.navCoord.GoForward,
180180 input.ActionReload: d.navCoord.Reload,
181181 input.ActionHardReload: d.navCoord.HardReload,
182+ input.ActionPrintPage: d.navCoord.PrintPage,
182183 // Zoom actions
183184 input.ActionZoomIn: func(ctx context.Context) error { return d.handleZoom(ctx, "in") },
184185 input.ActionZoomOut: func(ctx context.Context) error { return d.handleZoom(ctx, "out") },
@@ -179,6 +179,7 @@ func (d *KeyboardDispatcher) initActionHandlers() {
179 input.ActionGoForward: d.navCoord.GoForward,179 input.ActionGoForward: d.navCoord.GoForward,
180 input.ActionReload: d.navCoord.Reload,180 input.ActionReload: d.navCoord.Reload,
181 input.ActionHardReload: d.navCoord.HardReload,181 input.ActionHardReload: d.navCoord.HardReload,
182+ input.ActionPrintPage: d.navCoord.PrintPage,
182 // Zoom actions183 // Zoom actions
183 input.ActionZoomIn: func(ctx context.Context) error { return d.handleZoom(ctx, "in") },184 input.ActionZoomIn: func(ctx context.Context) error { return d.handleZoom(ctx, "in") },
184 input.ActionZoomOut: func(ctx context.Context) error { return d.handleZoom(ctx, "out") },185 input.ActionZoomOut: func(ctx context.Context) error { return d.handleZoom(ctx, "out") },
M internal/ui/input/shortcuts.go +2 -0
@@ -170,6 +170,7 @@ const (
170170 ActionReload Action = "reload"
171171 ActionHardReload Action = "hard_reload"
172172 ActionStop Action = "stop"
173+ ActionPrintPage Action = "print_page"
173174
174175 // Zoom
175176 ActionZoomIn Action = "zoom_in"
@@ -369,6 +370,7 @@ func (s *ShortcutSet) registerStandardShortcuts() {
369370 s.Global[KeyBinding{uint(gdk.KEY_q), ModCtrl}] = ActionQuit
370371 s.Global[KeyBinding{uint(gdk.KEY_F11), ModNone}] = ActionToggleFullscreen
371372 s.Global[KeyBinding{uint('c'), ModCtrl | ModShift}] = ActionCopyURL
373+ s.Global[KeyBinding{uint('p'), ModCtrl | ModShift}] = ActionPrintPage
372374 // Session management - direct shortcut to open session manager
373375 s.Global[KeyBinding{uint(gdk.KEY_s), ModCtrl | ModShift}] = ActionOpenSessionManager
374376 }
@@ -170,6 +170,7 @@ const (
170 ActionReload Action = "reload"170 ActionReload Action = "reload"
171 ActionHardReload Action = "hard_reload"171 ActionHardReload Action = "hard_reload"
172 ActionStop Action = "stop"172 ActionStop Action = "stop"
173+ ActionPrintPage Action = "print_page"
173 174
174 // Zoom175 // Zoom
175 ActionZoomIn Action = "zoom_in"176 ActionZoomIn Action = "zoom_in"
@@ -369,6 +370,7 @@ func (s *ShortcutSet) registerStandardShortcuts() {
369 s.Global[KeyBinding{uint(gdk.KEY_q), ModCtrl}] = ActionQuit370 s.Global[KeyBinding{uint(gdk.KEY_q), ModCtrl}] = ActionQuit
370 s.Global[KeyBinding{uint(gdk.KEY_F11), ModNone}] = ActionToggleFullscreen371 s.Global[KeyBinding{uint(gdk.KEY_F11), ModNone}] = ActionToggleFullscreen
371 s.Global[KeyBinding{uint('c'), ModCtrl | ModShift}] = ActionCopyURL372 s.Global[KeyBinding{uint('c'), ModCtrl | ModShift}] = ActionCopyURL
373+ s.Global[KeyBinding{uint('p'), ModCtrl | ModShift}] = ActionPrintPage
372 // Session management - direct shortcut to open session manager374 // Session management - direct shortcut to open session manager
373 s.Global[KeyBinding{uint(gdk.KEY_s), ModCtrl | ModShift}] = ActionOpenSessionManager375 s.Global[KeyBinding{uint(gdk.KEY_s), ModCtrl | ModShift}] = ActionOpenSessionManager
374 }376 }

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