detached 549d2ce

feat(print): add Ctrl+Shift+P native print dialog with WebKit Ctrl+P blocking

commit
549d2cefcc3f20cc991c1cbd63e01a85f3daef3e
author
bamen · · 2025-09-22 21:25 UTC
committer
bamen · · 2025-09-22 19:53 UTC
parent
7ebcaf5
signature
unsigned
11 files · +142 -20
M CHANGELOG.md +5 -0
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
55 ## [Unreleased]
66
7+### Added
8+- **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts
9+
710 ### Fixed
811 - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes
912 - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality
@@ -12,6 +15,8 @@ All notable changes to this project will be documented in this file.
1215 - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation
1316 - **Workspace theme integration**: Fixed white borders on inactive panes by implementing dynamic border colors based on GTK theme preference (dark mode: #333333 borders, light mode: #dddddd borders)
1417 - **GTK window background**: Added theme-aware window and pane background colors to prevent white bleeding through transparent elements
18+- **Print dialog window handling**: Fixed GTK window destroy errors during WebView cleanup by only destroying windows when CreateWindow was actually enabled
19+- **Modifier key logging noise**: Filtered out Ctrl, Shift, Alt, and Super key presses from accelerator miss logging to reduce console spam
1520
1621 ## [0.9.0] - 2025-09-22
1722
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
4 4
5 ## [Unreleased]5 ## [Unreleased]
6 6
7+### Added
8+- **Print functionality**: Added Ctrl+Shift+P shortcut to open native print dialogs from any pane. Ctrl+P is blocked at WebKit level to prevent conflicts with pane mode shortcuts
9+
7 ### Fixed10 ### Fixed
8 - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes11 - **Global shortcuts protection**: Fixed window shortcuts (Ctrl+L, Ctrl+F, F12) not working in new panes due to overly aggressive webpage protection. Window shortcuts now properly bubble up to GTK while pane shortcuts remain blocked from webpages. Resolves omnibox and developer tools not responding to keyboard shortcuts in newly created panes
9 - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality12 - **Omnibox keyboard event isolation**: Fixed keyboard event leakage from omnibox to underlying webpages that caused unintended page actions (e.g., typing 's' in omnibox triggering GitHub search). Implemented WebKit-level main-world event blocking that activates when omnibox opens, preventing page JavaScript from receiving keyboard events while preserving native GTK shortcuts (Ctrl+L, Ctrl+F, etc.) and omnibox functionality
@@ -12,6 +15,8 @@ All notable changes to this project will be documented in this file.
12 - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation15 - **WebView ID propagation**: Added webview ID request/response mechanism for stub builds to ensure JavaScript side knows its webview ID for proper focus validation
13 - **Workspace theme integration**: Fixed white borders on inactive panes by implementing dynamic border colors based on GTK theme preference (dark mode: #333333 borders, light mode: #dddddd borders)16 - **Workspace theme integration**: Fixed white borders on inactive panes by implementing dynamic border colors based on GTK theme preference (dark mode: #333333 borders, light mode: #dddddd borders)
14 - **GTK window background**: Added theme-aware window and pane background colors to prevent white bleeding through transparent elements17 - **GTK window background**: Added theme-aware window and pane background colors to prevent white bleeding through transparent elements
18+- **Print dialog window handling**: Fixed GTK window destroy errors during WebView cleanup by only destroying windows when CreateWindow was actually enabled
19+- **Modifier key logging noise**: Filtered out Ctrl, Shift, Alt, and Super key presses from accelerator miss logging to reduce console spam
15 20
16 ## [0.9.0] - 2025-09-2221 ## [0.9.0] - 2025-09-22
17 22
M README.md +1 -0
@@ -46,6 +46,7 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl
4646 | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts |
4747 | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom |
4848 | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast |
49+| **Ctrl/Cmd+Shift+P** | Print Page | Open native print dialog |
4950 | **Ctrl/Cmd+R** / **F5** | Reload Page | Refresh current page |
5051 | **Ctrl/Cmd+Shift+R** | Hard Reload | Refresh ignoring cache |
5152 | **Ctrl+←** / **Ctrl+→** | Navigate Back/Forward | Browser history navigation |
@@ -46,6 +46,7 @@ Dumber is a minimalist browser and launcher companion focused on speed and simpl
46 | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts |46 | **Ctrl/Cmd+-** | Zoom Out | Works across keyboard layouts |
47 | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom |47 | **Ctrl/Cmd+0** | Reset Zoom | Return to 100% zoom |
48 | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast |48 | **Ctrl/Cmd+Shift+C** | Copy URL | Copy current URL to clipboard with toast |
49+| **Ctrl/Cmd+Shift+P** | Print Page | Open native print dialog |
49 | **Ctrl/Cmd+R** / **F5** | Reload Page | Refresh current page |50 | **Ctrl/Cmd+R** / **F5** | Reload Page | Refresh current page |
50 | **Ctrl/Cmd+Shift+R** | Hard Reload | Refresh ignoring cache |51 | **Ctrl/Cmd+Shift+R** | Hard Reload | Refresh ignoring cache |
51 | **Ctrl+←** / **Ctrl+→** | Navigate Back/Forward | Browser history navigation |52 | **Ctrl+←** / **Ctrl+→** | Navigate Back/Forward | Browser history navigation |
M internal/app/browser/window_shortcuts.go +23 -0
@@ -21,6 +21,7 @@ type WindowShortcutHandler struct {
2121 lastFindToggle time.Time
2222 lastCopyURL time.Time
2323 lastDevTools time.Time
24+ lastPrint time.Time
2425 }
2526
2627 // NewWindowShortcutHandler creates a new window-level shortcut handler
@@ -57,6 +58,7 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
5758 {"ctrl+l", h.handleOmniboxToggle, "Omnibox toggle"},
5859 {"ctrl+f", h.handleFindToggle, "Find in page"},
5960 {"ctrl+shift+c", h.handleCopyURL, "Copy URL"},
61+ {"ctrl+shift+p", h.handlePrint, "Print page"},
6062 {"F12", h.handleDevTools, "Developer tools"},
6163 // Zoom shortcuts - global level for proper active pane targeting
6264 {"ctrl+plus", h.handleZoomIn, "Zoom in"},
@@ -149,6 +151,27 @@ func (h *WindowShortcutHandler) handleDevTools() {
149151 }
150152 }
151153
154+func (h *WindowShortcutHandler) handlePrint() {
155+ h.mu.Lock()
156+ defer h.mu.Unlock()
157+
158+ if time.Since(h.lastPrint) < 200*time.Millisecond {
159+ return
160+ }
161+ h.lastPrint = time.Now()
162+
163+ if h.app.activePane == nil || h.app.activePane.webView == nil {
164+ log.Printf("[window-shortcuts] No active pane for print")
165+ return
166+ }
167+
168+ log.Printf("[window-shortcuts] Print -> pane %p", h.app.activePane.webView)
169+
170+ if err := h.app.activePane.webView.ShowPrintDialog(); err != nil {
171+ log.Printf("[window-shortcuts] Failed to show print dialog: %v", err)
172+ }
173+}
174+
152175 // handleZoomIn increases zoom level by 10%
153176 func (h *WindowShortcutHandler) handleZoomIn() {
154177 h.handleZoom("in", 1.1)
@@ -21,6 +21,7 @@ type WindowShortcutHandler struct {
21 lastFindToggle time.Time21 lastFindToggle time.Time
22 lastCopyURL time.Time22 lastCopyURL time.Time
23 lastDevTools time.Time23 lastDevTools time.Time
24+ lastPrint time.Time
24 }25 }
25 26
26 // NewWindowShortcutHandler creates a new window-level shortcut handler27 // NewWindowShortcutHandler creates a new window-level shortcut handler
@@ -57,6 +58,7 @@ func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
57 {"ctrl+l", h.handleOmniboxToggle, "Omnibox toggle"},58 {"ctrl+l", h.handleOmniboxToggle, "Omnibox toggle"},
58 {"ctrl+f", h.handleFindToggle, "Find in page"},59 {"ctrl+f", h.handleFindToggle, "Find in page"},
59 {"ctrl+shift+c", h.handleCopyURL, "Copy URL"},60 {"ctrl+shift+c", h.handleCopyURL, "Copy URL"},
61+ {"ctrl+shift+p", h.handlePrint, "Print page"},
60 {"F12", h.handleDevTools, "Developer tools"},62 {"F12", h.handleDevTools, "Developer tools"},
61 // Zoom shortcuts - global level for proper active pane targeting63 // Zoom shortcuts - global level for proper active pane targeting
62 {"ctrl+plus", h.handleZoomIn, "Zoom in"},64 {"ctrl+plus", h.handleZoomIn, "Zoom in"},
@@ -149,6 +151,27 @@ func (h *WindowShortcutHandler) handleDevTools() {
149 }151 }
150 }152 }
151 153
154+func (h *WindowShortcutHandler) handlePrint() {
155+ h.mu.Lock()
156+ defer h.mu.Unlock()
157+
158+ if time.Since(h.lastPrint) < 200*time.Millisecond {
159+ return
160+ }
161+ h.lastPrint = time.Now()
162+
163+ if h.app.activePane == nil || h.app.activePane.webView == nil {
164+ log.Printf("[window-shortcuts] No active pane for print")
165+ return
166+ }
167+
168+ log.Printf("[window-shortcuts] Print -> pane %p", h.app.activePane.webView)
169+
170+ if err := h.app.activePane.webView.ShowPrintDialog(); err != nil {
171+ log.Printf("[window-shortcuts] Failed to show print dialog: %v", err)
172+ }
173+}
174+
152 // handleZoomIn increases zoom level by 10%175 // handleZoomIn increases zoom level by 10%
153 func (h *WindowShortcutHandler) handleZoomIn() {176 func (h *WindowShortcutHandler) handleZoomIn() {
154 h.handleZoom("in", 1.1)177 h.handleZoom("in", 1.1)
M internal/app/browser/workspace_manager.go +2 -3
@@ -394,7 +394,6 @@ func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool)
394394 }
395395 }
396396
397-
398397 func (wm *WorkspaceManager) focusNode(node *paneNode) {
399398 if node == nil || !node.isLeaf || node.pane == nil || node.pane.webView == nil {
400399 return
@@ -508,10 +507,10 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
508507 var inactiveBorderColor, windowBackgroundColor string
509508 isDark := webkit.PrefersDarkTheme()
510509 if isDark {
511- inactiveBorderColor = "#333333" // Dark border for dark theme
510+ inactiveBorderColor = "#333333" // Dark border for dark theme
512511 windowBackgroundColor = "#2b2b2b" // Dark window background
513512 } else {
514- inactiveBorderColor = "#dddddd" // Light border for light theme
513+ inactiveBorderColor = "#dddddd" // Light border for light theme
515514 windowBackgroundColor = "#ffffff" // Light window background
516515 }
517516
@@ -394,7 +394,6 @@ func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool)
394 }394 }
395 }395 }
396 396
397-
398 func (wm *WorkspaceManager) focusNode(node *paneNode) {397 func (wm *WorkspaceManager) focusNode(node *paneNode) {
399 if node == nil || !node.isLeaf || node.pane == nil || node.pane.webView == nil {398 if node == nil || !node.isLeaf || node.pane == nil || node.pane.webView == nil {
400 return399 return
@@ -508,10 +507,10 @@ func (wm *WorkspaceManager) generateActivePaneCSS() string {
508 var inactiveBorderColor, windowBackgroundColor string507 var inactiveBorderColor, windowBackgroundColor string
509 isDark := webkit.PrefersDarkTheme()508 isDark := webkit.PrefersDarkTheme()
510 if isDark {509 if isDark {
511- inactiveBorderColor = "#333333" // Dark border for dark theme510+ inactiveBorderColor = "#333333" // Dark border for dark theme
512 windowBackgroundColor = "#2b2b2b" // Dark window background511 windowBackgroundColor = "#2b2b2b" // Dark window background
513 } else {512 } else {
514- inactiveBorderColor = "#dddddd" // Light border for light theme513+ inactiveBorderColor = "#dddddd" // Light border for light theme
515 windowBackgroundColor = "#ffffff" // Light window background514 windowBackgroundColor = "#ffffff" // Light window background
516 }515 }
517 516
M internal/app/browser/workspace_manager_tree_test.go +7 -7
@@ -347,7 +347,7 @@ func TestTreeTraversal(t *testing.T) {
347347 original := wm.root
348348 node1, _ := wm.splitNode(original, "right")
349349 _, _ = wm.splitNode(node1, "down")
350- _, _ = wm.splitNode(original, "down") // Split the original leaf, not the new root branch
350+ _, _ = wm.splitNode(original, "down") // Split the original leaf, not the new root branch
351351
352352 leaves := wm.collectLeaves()
353353
@@ -532,7 +532,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
532532 // This is where the bug occurs: parent is root, but 2 panes remain (B and C)
533533 // The current code incorrectly thinks this is an error
534534 err = wm.closePane(A)
535-
535+
536536 // THIS IS THE BUG: The current implementation returns an error here
537537 // but it shouldn't - it's a valid operation
538538 if err != nil {
@@ -541,7 +541,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
541541 t.Logf("Bug reproduced: %v", err)
542542 t.Logf("This error should not occur - closing pane with branch sibling is valid")
543543 // Comment out the return to see what the final state should be
544- // return
544+ // return
545545 } else {
546546 t.Fatalf("Unexpected error: %v", err)
547547 }
@@ -550,7 +550,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
550550 // What SHOULD happen after successful close:
551551 // The SubPaned (containing B and C) should become the new root
552552 validateTreeStructure(t, wm.root)
553-
553+
554554 // Verify final state (what we expect after fix)
555555 finalLeaves, finalBranches := countNodes(wm.root)
556556 if finalLeaves != 2 {
@@ -564,7 +564,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
564564 if wm.root.isLeaf {
565565 t.Error("Root should be a branch node after closing A")
566566 }
567-
567+
568568 // B and C should still be in the tree
569569 foundB := false
570570 foundC := false
@@ -594,7 +594,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
594594 if err != nil {
595595 t.Fatalf("Split right failed: %v", err)
596596 }
597-
597+
598598 C, err := wm.splitNode(B, "down")
599599 if err != nil {
600600 t.Fatalf("Split down failed: %v", err)
@@ -608,7 +608,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
608608
609609 // Close A - the Paned(v) containing B and C should become root
610610 err = wm.closePane(A)
611-
611+
612612 // Bug should occur here too
613613 if err != nil {
614614 if err.Error() == "unexpected tree state during pane closure" {
@@ -347,7 +347,7 @@ func TestTreeTraversal(t *testing.T) {
347 original := wm.root347 original := wm.root
348 node1, _ := wm.splitNode(original, "right")348 node1, _ := wm.splitNode(original, "right")
349 _, _ = wm.splitNode(node1, "down")349 _, _ = wm.splitNode(node1, "down")
350- _, _ = wm.splitNode(original, "down") // Split the original leaf, not the new root branch350+ _, _ = wm.splitNode(original, "down") // Split the original leaf, not the new root branch
351 351
352 leaves := wm.collectLeaves()352 leaves := wm.collectLeaves()
353 353
@@ -532,7 +532,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
532 // This is where the bug occurs: parent is root, but 2 panes remain (B and C)532 // This is where the bug occurs: parent is root, but 2 panes remain (B and C)
533 // The current code incorrectly thinks this is an error533 // The current code incorrectly thinks this is an error
534 err = wm.closePane(A)534 err = wm.closePane(A)
535- 535+
536 // THIS IS THE BUG: The current implementation returns an error here536 // THIS IS THE BUG: The current implementation returns an error here
537 // but it shouldn't - it's a valid operation537 // but it shouldn't - it's a valid operation
538 if err != nil {538 if err != nil {
@@ -541,7 +541,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
541 t.Logf("Bug reproduced: %v", err)541 t.Logf("Bug reproduced: %v", err)
542 t.Logf("This error should not occur - closing pane with branch sibling is valid")542 t.Logf("This error should not occur - closing pane with branch sibling is valid")
543 // Comment out the return to see what the final state should be543 // Comment out the return to see what the final state should be
544- // return 544+ // return
545 } else {545 } else {
546 t.Fatalf("Unexpected error: %v", err)546 t.Fatalf("Unexpected error: %v", err)
547 }547 }
@@ -550,7 +550,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
550 // What SHOULD happen after successful close:550 // What SHOULD happen after successful close:
551 // The SubPaned (containing B and C) should become the new root551 // The SubPaned (containing B and C) should become the new root
552 validateTreeStructure(t, wm.root)552 validateTreeStructure(t, wm.root)
553- 553+
554 // Verify final state (what we expect after fix)554 // Verify final state (what we expect after fix)
555 finalLeaves, finalBranches := countNodes(wm.root)555 finalLeaves, finalBranches := countNodes(wm.root)
556 if finalLeaves != 2 {556 if finalLeaves != 2 {
@@ -564,7 +564,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
564 if wm.root.isLeaf {564 if wm.root.isLeaf {
565 t.Error("Root should be a branch node after closing A")565 t.Error("Root should be a branch node after closing A")
566 }566 }
567- 567+
568 // B and C should still be in the tree568 // B and C should still be in the tree
569 foundB := false569 foundB := false
570 foundC := false570 foundC := false
@@ -594,7 +594,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
594 if err != nil {594 if err != nil {
595 t.Fatalf("Split right failed: %v", err)595 t.Fatalf("Split right failed: %v", err)
596 }596 }
597- 597+
598 C, err := wm.splitNode(B, "down")598 C, err := wm.splitNode(B, "down")
599 if err != nil {599 if err != nil {
600 t.Fatalf("Split down failed: %v", err)600 t.Fatalf("Split down failed: %v", err)
@@ -608,7 +608,7 @@ func TestBugClosePaneWithBranchSibling(t *testing.T) {
608 608
609 // Close A - the Paned(v) containing B and C should become root609 // Close A - the Paned(v) containing B and C should become root
610 err = wm.closePane(A)610 err = wm.closePane(A)
611- 611+
612 // Bug should occur here too612 // Bug should occur here too
613 if err != nil {613 if err != nil {
614 if err.Error() == "unexpected tree state during pane closure" {614 if err.Error() == "unexpected tree state during pane closure" {
M internal/app/control/zoom.go +1 -1
@@ -59,7 +59,7 @@ func (z *ZoomController) handleZoomChange(level float64) {
5959 if url == "" {
6060 return
6161 }
62-
62+
6363 go func(url string, level float64) {
6464 ctx := context.Background()
6565 if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
@@ -59,7 +59,7 @@ func (z *ZoomController) handleZoomChange(level float64) {
59 if url == "" {59 if url == "" {
60 return60 return
61 }61 }
62- 62+
63 go func(url string, level float64) {63 go func(url string, level float64) {
64 ctx := context.Background()64 ctx := context.Background()
65 if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {65 if err := z.browserService.SetZoomLevel(ctx, url, level); err != nil {
M internal/app/messaging/deduplicator.go +2 -3
@@ -102,13 +102,12 @@ func (d *PaneRequestDeduplicator) ClearRequestID(requestID string) {
102102 if requestID == "" {
103103 return
104104 }
105-
105+
106106 d.mu.Lock()
107107 defer d.mu.Unlock()
108-
108+
109109 if d.requestIDs[requestID] {
110110 delete(d.requestIDs, requestID)
111111 log.Printf("[deduplicator] Cleared request ID: %s", requestID)
112112 }
113113 }
114-
@@ -102,13 +102,12 @@ func (d *PaneRequestDeduplicator) ClearRequestID(requestID string) {
102 if requestID == "" {102 if requestID == "" {
103 return103 return
104 }104 }
105- 105+
106 d.mu.Lock()106 d.mu.Lock()
107 defer d.mu.Unlock()107 defer d.mu.Unlock()
108- 108+
109 if d.requestIDs[requestID] {109 if d.requestIDs[requestID] {
110 delete(d.requestIDs, requestID)110 delete(d.requestIDs, requestID)
111 log.Printf("[deduplicator] Cleared request ID: %s", requestID)111 log.Printf("[deduplicator] Cleared request ID: %s", requestID)
112 }112 }
113 }113 }
114-
M pkg/webkit/keyboard_cgo.go +14 -1
@@ -199,7 +199,20 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
199199 case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P):
200200 keyName = "p"
201201 default:
202- // If Control held, log unknown keyval for diagnostics
202+ // Filter out modifier keys themselves (like Ctrl, Alt, Shift)
203+ // These keyvals shouldn't be logged as misses
204+ switch keyval {
205+ case 0xffe1, 0xffe2: // GDK_KEY_Shift_L, GDK_KEY_Shift_R
206+ return false
207+ case 0xffe3, 0xffe4: // GDK_KEY_Control_L, GDK_KEY_Control_R
208+ return false
209+ case 0xffe9, 0xffea: // GDK_KEY_Alt_L, GDK_KEY_Alt_R
210+ return false
211+ case 0xffeb, 0xffec: // GDK_KEY_Super_L, GDK_KEY_Super_R
212+ return false
213+ }
214+
215+ // If Control held, log unknown keyval for diagnostics (but not modifier keys)
203216 if ctrl {
204217 log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
205218 }
@@ -199,7 +199,20 @@ func dispatchAccelerator(uid uintptr, keyval uint, state uint) bool {
199 case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P):199 case uint(C.GDK_KEY_p), uint(C.GDK_KEY_P):
200 keyName = "p"200 keyName = "p"
201 default:201 default:
202- // If Control held, log unknown keyval for diagnostics202+ // Filter out modifier keys themselves (like Ctrl, Alt, Shift)
203+ // These keyvals shouldn't be logged as misses
204+ switch keyval {
205+ case 0xffe1, 0xffe2: // GDK_KEY_Shift_L, GDK_KEY_Shift_R
206+ return false
207+ case 0xffe3, 0xffe4: // GDK_KEY_Control_L, GDK_KEY_Control_R
208+ return false
209+ case 0xffe9, 0xffea: // GDK_KEY_Alt_L, GDK_KEY_Alt_R
210+ return false
211+ case 0xffeb, 0xffec: // GDK_KEY_Super_L, GDK_KEY_Super_R
212+ return false
213+ }
214+
215+ // If Control held, log unknown keyval for diagnostics (but not modifier keys)
203 if ctrl {216 if ctrl {
204 log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)217 log.Printf("[accelerator-miss] ctrl keyval=0x%x", keyval)
205 }218 }
A pkg/webkit/print_cgo.go +46 -0
@@ -0,0 +1,46 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+/*
6+#cgo pkg-config: webkitgtk-6.0 gtk4
7+#cgo CFLAGS: -I/usr/include/webkitgtk-6.0
8+#include <webkit/webkit.h>
9+#include <gtk/gtk.h>
10+*/
11+import "C"
12+import "unsafe"
13+
14+// ShowPrintDialog shows the print dialog for the WebView
15+func (w *WebView) ShowPrintDialog() error {
16+ if w == nil || w.native == nil || w.native.wv == nil {
17+ return ErrNotImplemented
18+ }
19+
20+ // Create print operation
21+ printOp := C.webkit_print_operation_new(w.native.wv)
22+ if printOp == nil {
23+ return ErrNotImplemented
24+ }
25+
26+ // Pass nil for parent window - print dialog will work without it
27+ var parentWindow *C.GtkWindow = nil
28+
29+ // Show print dialog
30+ response := C.webkit_print_operation_run_dialog(printOp, parentWindow)
31+
32+ // Clean up print operation
33+ C.g_object_unref(C.gpointer(unsafe.Pointer(printOp)))
34+
35+ // Handle the response
36+ switch response {
37+ case C.WEBKIT_PRINT_OPERATION_RESPONSE_PRINT:
38+ // User clicked print - the operation will proceed
39+ return nil
40+ case C.WEBKIT_PRINT_OPERATION_RESPONSE_CANCEL:
41+ // User cancelled - not an error
42+ return nil
43+ default:
44+ return ErrNotImplemented
45+ }
46+}
@@ -0,0 +1,46 @@
1+//go:build webkit_cgo
2+
3+package webkit
4+
5+/*
6+#cgo pkg-config: webkitgtk-6.0 gtk4
7+#cgo CFLAGS: -I/usr/include/webkitgtk-6.0
8+#include <webkit/webkit.h>
9+#include <gtk/gtk.h>
10+*/
11+import "C"
12+import "unsafe"
13+
14+// ShowPrintDialog shows the print dialog for the WebView
15+func (w *WebView) ShowPrintDialog() error {
16+ if w == nil || w.native == nil || w.native.wv == nil {
17+ return ErrNotImplemented
18+ }
19+
20+ // Create print operation
21+ printOp := C.webkit_print_operation_new(w.native.wv)
22+ if printOp == nil {
23+ return ErrNotImplemented
24+ }
25+
26+ // Pass nil for parent window - print dialog will work without it
27+ var parentWindow *C.GtkWindow = nil
28+
29+ // Show print dialog
30+ response := C.webkit_print_operation_run_dialog(printOp, parentWindow)
31+
32+ // Clean up print operation
33+ C.g_object_unref(C.gpointer(unsafe.Pointer(printOp)))
34+
35+ // Handle the response
36+ switch response {
37+ case C.WEBKIT_PRINT_OPERATION_RESPONSE_PRINT:
38+ // User clicked print - the operation will proceed
39+ return nil
40+ case C.WEBKIT_PRINT_OPERATION_RESPONSE_CANCEL:
41+ // User cancelled - not an error
42+ return nil
43+ default:
44+ return ErrNotImplemented
45+ }
46+}
M pkg/webkit/webview.go +3 -0
@@ -136,6 +136,9 @@ func (w *WebView) ShowDevTools() error { return nil }
136136 // CloseDevTools is a no-op in the non-CGO build.
137137 func (w *WebView) CloseDevTools() error { return nil }
138138
139+// ShowPrintDialog is a no-op in the non-CGO build.
140+func (w *WebView) ShowPrintDialog() error { return nil }
141+
139142 // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
140143 func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
141144
@@ -136,6 +136,9 @@ func (w *WebView) ShowDevTools() error { return nil }
136 // CloseDevTools is a no-op in the non-CGO build.136 // CloseDevTools is a no-op in the non-CGO build.
137 func (w *WebView) CloseDevTools() error { return nil }137 func (w *WebView) CloseDevTools() error { return nil }
138 138
139+// ShowPrintDialog is a no-op in the non-CGO build.
140+func (w *WebView) ShowPrintDialog() error { return nil }
141+
139 // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.142 // RegisterScriptMessageHandler registers a callback invoked when the content script posts a message.
140 func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }143 func (w *WebView) RegisterScriptMessageHandler(cb func(payload string)) { w.msgHandler = cb }
141 144
M pkg/webkit/webview_cgo.go +38 -5
@@ -260,6 +260,26 @@ static void connect_close_signal(WebKitWebView* web_view, unsigned long id) {
260260 printf("[webkit-connect] Connected close signal to WebView with id: %lu\n", id);
261261 }
262262
263+// Callback for print signal - intercepts Ctrl+P and can redirect behavior
264+static gboolean on_print_request(WebKitWebView* web_view, WebKitPrintOperation* print_operation, gpointer user_data) {
265+ (void)web_view;
266+ (void)print_operation;
267+ (void)user_data;
268+
269+ // Return TRUE to prevent default print dialog
270+ // Return FALSE to allow default print behavior
271+ // For now, we'll block the print dialog entirely since Ctrl+P should trigger pane mode
272+ printf("[webkit-print] Print request intercepted - blocking to prevent conflict with pane mode\n");
273+ return TRUE; // Block the print dialog
274+}
275+
276+// Connect print signal to a WebView to intercept Ctrl+P
277+static void connect_print_signal(WebKitWebView* web_view, unsigned long id) {
278+ if (!web_view) return;
279+ g_signal_connect_data(G_OBJECT(web_view), "print", G_CALLBACK(on_print_request), (gpointer)id, NULL, 0);
280+ printf("[webkit-connect] Connected print signal to WebView with id: %lu\n", id);
281+}
282+
263283 static gboolean gtk_prefers_dark() {
264284 // Method 1: Check GNOME desktop interface color-scheme (primary method)
265285 GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");
@@ -1112,6 +1132,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
11121132 // Attach GTK4 input controllers (keyboard, mouse)
11131133 AttachKeyboardControllers(v)
11141134
1135+ // Enable print interception to prevent Ctrl+P conflicts with pane mode
1136+ v.EnablePrintInterception()
1137+
11151138 // Watch for GTK theme changes and propagate to page at runtime
11161139 if settings := C.gtk_settings_get_default(); settings != nil {
11171140 C.connect_theme_changed_with_id(settings, C.ulong(v.id))
@@ -1580,7 +1603,7 @@ func (w *WebView) Destroy() error {
15801603 }
15811604
15821605 // GTK4: destroy windows via gtk_window_destroy if we created one
1583- if w.native.win != nil {
1606+ if w.native.win != nil && w.config != nil && w.config.CreateWindow {
15841607 C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
15851608 w.native.win = nil
15861609 if w.window != nil {
@@ -1667,6 +1690,13 @@ func (w *WebView) RegisterCloseHandler(cb func()) {
16671690 }
16681691 }
16691692
1693+// EnablePrintInterception connects the print signal to intercept Ctrl+P
1694+func (w *WebView) EnablePrintInterception() {
1695+ if w.native != nil && w.native.wv != nil {
1696+ C.connect_print_signal(w.native.wv, C.ulong(w.id))
1697+ }
1698+}
1699+
16701700 func (w *WebView) dispatchScriptMessage(payload string) {
16711701 if w != nil && w.msgHandler != nil {
16721702 w.msgHandler(payload)
@@ -1785,10 +1815,13 @@ func (w *WebView) DestroyWindow() {
17851815 if w == nil || w.native == nil || w.native.win == nil {
17861816 return
17871817 }
1788- C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1789- w.native.win = nil
1790- if w.window != nil {
1791- w.window.win = nil
1818+ // Only destroy if we actually created a window
1819+ if w.config != nil && w.config.CreateWindow {
1820+ C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1821+ w.native.win = nil
1822+ if w.window != nil {
1823+ w.window.win = nil
1824+ }
17921825 }
17931826 }
17941827
@@ -260,6 +260,26 @@ static void connect_close_signal(WebKitWebView* web_view, unsigned long id) {
260 printf("[webkit-connect] Connected close signal to WebView with id: %lu\n", id);260 printf("[webkit-connect] Connected close signal to WebView with id: %lu\n", id);
261 }261 }
262 262
263+// Callback for print signal - intercepts Ctrl+P and can redirect behavior
264+static gboolean on_print_request(WebKitWebView* web_view, WebKitPrintOperation* print_operation, gpointer user_data) {
265+ (void)web_view;
266+ (void)print_operation;
267+ (void)user_data;
268+
269+ // Return TRUE to prevent default print dialog
270+ // Return FALSE to allow default print behavior
271+ // For now, we'll block the print dialog entirely since Ctrl+P should trigger pane mode
272+ printf("[webkit-print] Print request intercepted - blocking to prevent conflict with pane mode\n");
273+ return TRUE; // Block the print dialog
274+}
275+
276+// Connect print signal to a WebView to intercept Ctrl+P
277+static void connect_print_signal(WebKitWebView* web_view, unsigned long id) {
278+ if (!web_view) return;
279+ g_signal_connect_data(G_OBJECT(web_view), "print", G_CALLBACK(on_print_request), (gpointer)id, NULL, 0);
280+ printf("[webkit-connect] Connected print signal to WebView with id: %lu\n", id);
281+}
282+
263 static gboolean gtk_prefers_dark() {283 static gboolean gtk_prefers_dark() {
264 // Method 1: Check GNOME desktop interface color-scheme (primary method)284 // Method 1: Check GNOME desktop interface color-scheme (primary method)
265 GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");285 GSettings* desktop_settings = g_settings_new("org.gnome.desktop.interface");
@@ -1112,6 +1132,9 @@ func NewWebView(cfg *Config) (*WebView, error) {
1112 // Attach GTK4 input controllers (keyboard, mouse)1132 // Attach GTK4 input controllers (keyboard, mouse)
1113 AttachKeyboardControllers(v)1133 AttachKeyboardControllers(v)
1114 1134
1135+ // Enable print interception to prevent Ctrl+P conflicts with pane mode
1136+ v.EnablePrintInterception()
1137+
1115 // Watch for GTK theme changes and propagate to page at runtime1138 // Watch for GTK theme changes and propagate to page at runtime
1116 if settings := C.gtk_settings_get_default(); settings != nil {1139 if settings := C.gtk_settings_get_default(); settings != nil {
1117 C.connect_theme_changed_with_id(settings, C.ulong(v.id))1140 C.connect_theme_changed_with_id(settings, C.ulong(v.id))
@@ -1580,7 +1603,7 @@ func (w *WebView) Destroy() error {
1580 }1603 }
1581 1604
1582 // GTK4: destroy windows via gtk_window_destroy if we created one1605 // GTK4: destroy windows via gtk_window_destroy if we created one
1583- if w.native.win != nil {1606+ if w.native.win != nil && w.config != nil && w.config.CreateWindow {
1584 C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))1607 C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1585 w.native.win = nil1608 w.native.win = nil
1586 if w.window != nil {1609 if w.window != nil {
@@ -1667,6 +1690,13 @@ func (w *WebView) RegisterCloseHandler(cb func()) {
1667 }1690 }
1668 }1691 }
1669 1692
1693+// EnablePrintInterception connects the print signal to intercept Ctrl+P
1694+func (w *WebView) EnablePrintInterception() {
1695+ if w.native != nil && w.native.wv != nil {
1696+ C.connect_print_signal(w.native.wv, C.ulong(w.id))
1697+ }
1698+}
1699+
1670 func (w *WebView) dispatchScriptMessage(payload string) {1700 func (w *WebView) dispatchScriptMessage(payload string) {
1671 if w != nil && w.msgHandler != nil {1701 if w != nil && w.msgHandler != nil {
1672 w.msgHandler(payload)1702 w.msgHandler(payload)
@@ -1785,10 +1815,13 @@ func (w *WebView) DestroyWindow() {
1785 if w == nil || w.native == nil || w.native.win == nil {1815 if w == nil || w.native == nil || w.native.win == nil {
1786 return1816 return
1787 }1817 }
1788- C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))1818+ // Only destroy if we actually created a window
1789- w.native.win = nil1819+ if w.config != nil && w.config.CreateWindow {
1790- if w.window != nil {1820+ C.gtk_window_destroy((*C.GtkWindow)(unsafe.Pointer(w.native.win)))
1791- w.window.win = nil1821+ w.native.win = nil
1822+ if w.window != nil {
1823+ w.window.win = nil
1824+ }
1792 }1825 }
1793 }1826 }
1794 1827

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