1package browser
2
3import (
4 "fmt"
5 "log"
6 "sync"
7 "time"
8
9 "github.com/bnema/dumber/internal/app/messaging"
10 "github.com/bnema/dumber/pkg/webkit"
11 "github.com/diamondburned/gotk4/pkg/gdk/v4"
12 "github.com/diamondburned/gotk4/pkg/gtk/v4"
13)
14
15// WindowShortcutHandler manages global shortcuts at the window level
16type WindowShortcutHandler struct {
17 window *webkit.Window
18 app *BrowserApp
19 shortcuts *webkit.WindowShortcuts
20 mu sync.Mutex
21
22 // Debounce tracking
23 lastOmniboxToggle time.Time
24 lastFindToggle time.Time
25 lastCopyURL time.Time
26 lastDevTools time.Time
27 lastPrint time.Time
28}
29
30// NewWindowShortcutHandler creates a new window-level shortcut handler
31func NewWindowShortcutHandler(window *webkit.Window, app *BrowserApp) *WindowShortcutHandler {
32 h := &WindowShortcutHandler{
33 window: window,
34 app: app,
35 }
36
37 if err := h.initialize(); err != nil {
38 log.Printf("[window-shortcuts] Failed to initialize: %v", err)
39 return nil
40 }
41
42 return h
43}
44
45func (h *WindowShortcutHandler) initialize() error {
46 // Create WindowShortcuts manager
47 h.shortcuts = webkit.NewWindowShortcuts(h.window)
48 if h.shortcuts == nil {
49 return ErrFailedToInitialize
50 }
51
52 // Register hardware keycode-based shortcuts (for AZERTY/international keyboards)
53 if err := h.registerKeycodeShortcuts(); err != nil {
54 log.Printf("[window-shortcuts] Failed to register keycode shortcuts: %v", err)
55 }
56
57 return h.registerGlobalShortcuts()
58}
59
60func (h *WindowShortcutHandler) registerGlobalShortcuts() error {
61 shortcuts := []struct {
62 key string
63 handler func()
64 desc string
65 }{
66 {"ctrl+l", h.handleOmniboxToggle, "Omnibox toggle"},
67 {"ctrl+f", h.handleFindToggle, "Find in page"},
68 {"ctrl+shift+c", h.handleCopyURL, "Copy URL"},
69 {"ctrl+shift+p", h.handlePrint, "Print page"},
70 {"F12", h.handleDevTools, "Developer tools"},
71 // Pane management: Ctrl+P handled by WebView keyboard bridge (not window shortcuts)
72 // Tab management
73 {"ctrl+t", h.handleTabMode, "Enter tab mode"},
74 // Global tab navigation (Ctrl+Tab) is handled via EventControllerKey in registerKeycodeShortcuts
75 // This prevents GTK's default focus cycling behavior
76 // NOTE: Tab mode action keys (n, c, x, l, h, r, Tab, Escape, Enter) are NOT registered
77 // as global shortcuts. They are handled by the WebView keyboard bridge which checks
78 // if tab mode is active before processing them. This prevents them from consuming
79 // keys when tab mode is not active (allowing normal typing in web pages).
80 // Direct tab switching is now handled via hardware keycodes (see registerKeycodeShortcuts)
81 // This supports all keyboard layouts (QWERTY, AZERTY, etc.)
82 // Page reload shortcuts
83 {"ctrl+r", h.handleReload, "Reload page"},
84 {"ctrl+shift+r", h.handleHardReload, "Hard reload (bypass cache)"},
85 {"F5", h.handleReload, "Reload page"},
86 {"ctrl+F5", h.handleHardReload, "Hard reload (bypass cache)"},
87 // Zoom shortcuts - global level for proper active pane targeting
88 {"ctrl+plus", h.handleZoomIn, "Zoom in"},
89 {"ctrl+equal", h.handleZoomIn, "Zoom in (=)"},
90 {"ctrl+minus", h.handleZoomOut, "Zoom out"},
91 {"ctrl+0", h.handleZoomReset, "Zoom reset"},
92 // Workspace navigation shortcuts - global level for proper active pane targeting
93 {"alt+ArrowLeft", func() { h.handleWorkspaceNavigation(DirectionLeft) }, "Navigate left pane"},
94 {"alt+ArrowRight", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane"},
95 {"alt+ArrowUp", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane"},
96 {"alt+ArrowDown", func() { h.handleWorkspaceNavigation(DirectionDown) }, "Navigate down pane"},
97 // Vim-style workspace navigation
98 {"alt+h", func() { h.handleWorkspaceNavigation(DirectionLeft) }, "Navigate left pane (vim)"},
99 {"alt+l", func() { h.handleWorkspaceNavigation(DirectionRight) }, "Navigate right pane (vim)"},
100 {"alt+k", func() { h.handleWorkspaceNavigation(DirectionUp) }, "Navigate up pane (vim)"},
101 {"alt+j", func() { h.handleWorkspaceNavigation(DirectionDown) }, "Navigate down pane (vim)"},
102 }
103
104 for _, shortcut := range shortcuts {
105 h.shortcuts.RegisterShortcut(shortcut.key, shortcut.handler)
106 log.Printf("[window-shortcuts] Registered global shortcut: %s (%s)",
107 shortcut.key, shortcut.desc)
108 }
109
110 // NOTE: Ctrl+P is NOT registered here - it's handled by WebView's keyboard bridge
111 // (RegisterPaneModeHandler in workspace_manager.go) which calls app.workspace.EnterPaneMode()
112
113 return nil
114}
115
116// registerKeycodeShortcuts registers keyboard shortcuts based on hardware keycodes
117// This allows Alt+number shortcuts to work on AZERTY and other keyboard layouts
118func (h *WindowShortcutHandler) registerKeycodeShortcuts() error {
119 if h.window == nil {
120 return fmt.Errorf("window is nil")
121 }
122
123 gtkWindow := h.window.AsWindow()
124 if gtkWindow == nil {
125 return fmt.Errorf("gtk window is nil")
126 }
127
128 // Create event controller for keyboard events
129 keyController := gtk.NewEventControllerKey()
130 keyController.SetPropagationPhase(gtk.PhaseCapture)
131
132 // GDK key constants
133 const (
134 gdkKeyTab = 0xff09
135 gdkKeyEscape = 0xff1b
136 gdkKeyReturn = 0xff0d
137 gdkKeyN = 0x006e
138 gdkKeyC = 0x0063
139 gdkKeyX = 0x0078
140 gdkKeyL = 0x006c
141 gdkKeyH = 0x0068
142 gdkKeyR = 0x0072
143 )
144
145 // Map hardware keycodes to tab indices
146 // These keycodes are the same across all keyboard layouts
147 keycodeToTab := map[uint]int{
148 10: 0, // 1/& key
149 11: 1, // 2/é key
150 12: 2, // 3/" key
151 13: 3, // 4/' key
152 14: 4, // 5/( key
153 15: 5, // 6/- key
154 16: 6, // 7/è key
155 17: 7, // 8/_ key
156 18: 8, // 9/ç key
157 19: 9, // 0/à key
158 }
159
160 keyController.ConnectKeyPressed(func(keyval, keycode uint, state gdk.ModifierType) bool {
161 // IMPORTANT: Let WebView's keyboard bridge handle pane mode action keys first
162 // During pane mode, single-letter keys (h,j,k,l,x,s,v, etc.) must reach the WebView
163 // So we only intercept specific shortcuts here and let everything else through
164
165 // Handle Ctrl+Tab for next tab
166 if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) {
167 log.Printf("[window-shortcuts] Ctrl+Tab -> next tab")
168 h.handleNextTab()
169 return true // Consume event to prevent focus cycling
170 }
171
172 // Handle Ctrl+Shift+Tab for previous tab
173 if keyval == gdkKeyTab && state.Has(gdk.ControlMask) && state.Has(gdk.ShiftMask) {
174 log.Printf("[window-shortcuts] Ctrl+Shift+Tab -> previous tab")
175 h.handlePrevTab()
176 return true // Consume event to prevent focus cycling
177 }
178
179 // Check for Alt+number (but not with Ctrl or Shift)
180 if state.Has(gdk.AltMask) && !state.Has(gdk.ControlMask) && !state.Has(gdk.ShiftMask) {
181 // Check if this is a number key
182 if tabIndex, ok := keycodeToTab[keycode]; ok {
183 log.Printf("[window-shortcuts] Hardware keycode shortcut: Alt+keycode(%d) -> tab %d", keycode, tabIndex)
184 h.handleDirectTabSwitch(tabIndex)
185 return true // Handled
186 }
187 }
188
189 // Handle tab mode action keys (only when tab mode is active)
190 // IMPORTANT: If rename is in progress, let ALL keys through to the Entry widget
191 // These keys need to be checked here so they can propagate when tab mode is not active
192 if h.isTabModeActive() {
193 // Check if rename is in progress - if so, don't intercept ANY keys
194 // This allows the Entry widget to receive all keyboard input
195 if h.isRenameInProgress() {
196 return false // Let all keys propagate to Entry widget
197 }
198
199 handled := false
200 action := ""
201
202 // Check for no modifiers (plain key press)
203 if !state.Has(gdk.ControlMask) && !state.Has(gdk.AltMask) {
204 switch keyval {
205 case gdkKeyN, gdkKeyC:
206 action = "new-tab"
207 handled = true
208 case gdkKeyX:
209 action = "close-tab"
210 handled = true
211 case gdkKeyL:
212 action = "next-tab"
213 handled = true
214 case gdkKeyH:
215 action = "previous-tab"
216 handled = true
217 case gdkKeyR:
218 action = "rename-tab"
219 handled = true
220 case gdkKeyTab:
221 action = "next-tab"
222 handled = true
223 case gdkKeyReturn:
224 action = "confirm"
225 handled = true
226 case gdkKeyEscape:
227 action = "cancel"
228 handled = true
229 }
230 }
231
232 // Handle Shift+Tab for previous tab in tab mode
233 if keyval == gdkKeyTab && state.Has(gdk.ShiftMask) && !state.Has(gdk.ControlMask) {
234 action = "previous-tab"
235 handled = true
236 }
237
238 if handled {
239 log.Printf("[window-shortcuts] Tab mode action: %s", action)
240 h.handleTabModeAction(action)
241 return true // Consume event
242 }
243 }
244
245 // Let all other keys propagate to WebView's keyboard bridge
246 // This includes Ctrl+P, Ctrl+T, and all pane mode action keys
247 return false
248 })
249
250 gtkWindow.AddController(keyController)
251 log.Printf("[window-shortcuts] Registered capture phase shortcuts: Ctrl+Tab, Alt+number")
252
253 return nil
254}
255
256func (h *WindowShortcutHandler) handleOmniboxToggle() {
257 h.handleUIToggle(&h.lastOmniboxToggle, "omnibox", "omnibox-nav-toggle")
258}
259
260func (h *WindowShortcutHandler) handleFindToggle() {
261 h.handleUIToggle(&h.lastFindToggle, "find", "omnibox-find-toggle")
262}
263
264func (h *WindowShortcutHandler) handleCopyURL() {
265 h.mu.Lock()
266 defer h.mu.Unlock()
267
268 if time.Since(h.lastCopyURL) < 100*time.Millisecond {
269 return
270 }
271 h.lastCopyURL = time.Now()
272
273 if h.app.activePane == nil || h.app.activePane.webView == nil {
274 log.Printf("[window-shortcuts] No active pane for copy URL")
275 return
276 }
277
278 log.Printf("[window-shortcuts] Copy URL -> pane %p", h.app.activePane.webView)
279
280 // Execute copy URL script directly on active pane
281 script := `
282 (async function() {
283 const toast = (message, type) => {
284 document.dispatchEvent(new CustomEvent('dumber:toast', {
285 detail: { message, duration: 2000, type }
286 }));
287 };
288
289 try {
290 const url = window.location.href;
291 await navigator.clipboard.writeText(url);
292 console.log('🔗 Window shortcut copied URL:', url);
293 toast('URL copied to clipboard!', 'success');
294 } catch (error) {
295 console.error('❌ Failed to copy URL:', error);
296 toast('Failed to copy URL', 'error');
297 }
298 })();
299 `
300
301 if err := h.app.activePane.webView.InjectScript(script); err != nil {
302 log.Printf("[window-shortcuts] Failed to inject copy URL script: %v", err)
303 }
304}
305
306func (h *WindowShortcutHandler) handleDevTools() {
307 h.mu.Lock()
308 defer h.mu.Unlock()
309
310 if time.Since(h.lastDevTools) < 200*time.Millisecond {
311 return
312 }
313 h.lastDevTools = time.Now()
314
315 if h.app.activePane == nil || h.app.activePane.webView == nil {
316 log.Printf("[window-shortcuts] No active pane for devtools")
317 return
318 }
319
320 log.Printf("[window-shortcuts] DevTools -> pane %p", h.app.activePane.webView)
321
322 if err := h.app.activePane.webView.ShowDevTools(); err != nil {
323 log.Printf("[window-shortcuts] Failed to show devtools: %v", err)
324 }
325}
326
327func (h *WindowShortcutHandler) handlePrint() {
328 h.mu.Lock()
329 defer h.mu.Unlock()
330
331 if time.Since(h.lastPrint) < 200*time.Millisecond {
332 return
333 }
334 h.lastPrint = time.Now()
335
336 if h.app.activePane == nil || h.app.activePane.webView == nil {
337 log.Printf("[window-shortcuts] No active pane for print")
338 return
339 }
340
341 log.Printf("[window-shortcuts] Print -> pane %p", h.app.activePane.webView)
342
343 if err := h.app.activePane.webView.ShowPrintDialog(); err != nil {
344 log.Printf("[window-shortcuts] Failed to show print dialog: %v", err)
345 }
346}
347
348func (h *WindowShortcutHandler) handleReload() {
349 h.mu.Lock()
350 defer h.mu.Unlock()
351
352 if h.app.activePane == nil || h.app.activePane.webView == nil {
353 log.Printf("[window-shortcuts] No active pane for reload")
354 return
355 }
356
357 log.Printf("[window-shortcuts] Reload page -> pane %p", h.app.activePane.webView)
358
359 if err := h.app.activePane.webView.Reload(); err != nil {
360 log.Printf("[window-shortcuts] Failed to reload page: %v", err)
361 }
362}
363
364func (h *WindowShortcutHandler) handleHardReload() {
365 h.mu.Lock()
366 defer h.mu.Unlock()
367
368 if h.app.activePane == nil || h.app.activePane.webView == nil {
369 log.Printf("[window-shortcuts] No active pane for hard reload")
370 return
371 }
372
373 log.Printf("[window-shortcuts] Hard reload (bypass cache) -> pane %p", h.app.activePane.webView)
374
375 if err := h.app.activePane.webView.ReloadBypassCache(); err != nil {
376 log.Printf("[window-shortcuts] Failed to hard reload page: %v", err)
377 }
378}
379
380// handleZoomIn increases zoom level by 10%
381func (h *WindowShortcutHandler) handleZoomIn() {
382 h.handleZoom("in", 1.1)
383}
384
385// handleZoomOut decreases zoom level by 10%
386func (h *WindowShortcutHandler) handleZoomOut() {
387 h.handleZoom("out", 1.0/1.1)
388}
389
390// handleZoomReset resets zoom to 100%
391func (h *WindowShortcutHandler) handleZoomReset() {
392 h.handleZoom("reset", 1.0)
393}
394
395// handleZoom applies zoom changes to the active pane
396func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) {
397 h.mu.Lock()
398 defer h.mu.Unlock()
399
400 if h.app.activePane == nil || h.app.activePane.webView == nil {
401 log.Printf("[window-shortcuts] No active pane for zoom %s", action)
402 return
403 }
404
405 activeWebView := h.app.activePane.webView
406 log.Printf("[window-shortcuts] Zoom %s -> pane %p", action, activeWebView)
407
408 // Get current zoom level
409 currentZoom := activeWebView.GetZoom()
410
411 // Calculate new zoom level
412 var newZoom float64
413 if action == "reset" {
414 newZoom = 1.0
415 } else {
416 newZoom = currentZoom * multiplier
417 // Apply zoom limits
418 if newZoom < 0.25 {
419 newZoom = 0.25
420 }
421 if newZoom > 5.0 {
422 newZoom = 5.0
423 }
424 }
425
426 log.Printf("[window-shortcuts] Zoom %s: %.2f -> %.2f", action, currentZoom, newZoom)
427
428 // Apply zoom to active pane
429 if err := activeWebView.SetZoom(newZoom); err != nil {
430 log.Printf("[window-shortcuts] Failed to set zoom: %v", err)
431 return
432 }
433
434 // Ensure GUI/toast component is loaded before showing toast
435 h.ensureGUIInActivePane("toast")
436
437 // Show zoom toast notification
438 toastScript := fmt.Sprintf(`
439 (function() {
440 try {
441 if (typeof window.__dumber_showZoomToast === 'function') {
442 window.__dumber_showZoomToast(%f);
443 }
444 } catch (e) {
445 console.error('[window-shortcuts] Failed to show zoom toast:', e);
446 }
447 })();
448 `, newZoom)
449
450 if err := activeWebView.InjectScript(toastScript); err != nil {
451 log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err)
452 }
453}
454
455func (h *WindowShortcutHandler) ensureGUIInActivePane(component string) {
456 if h.app.activePane == nil {
457 return
458 }
459
460 pane := h.app.activePane
461 if !pane.HasGUI() {
462 log.Printf("[window-shortcuts] Injecting GUI into pane %s for %s", pane.ID(), component)
463 if h.app.workspace != nil {
464 h.app.workspace.ensureGUIInPane(pane)
465 }
466 }
467
468 // Ensure specific component is available
469 if !pane.HasGUIComponent(component) {
470 log.Printf("[window-shortcuts] Ensuring %s component in pane %s", component, pane.ID())
471 pane.SetGUIComponent(component, true)
472 }
473}
474
475func (h *WindowShortcutHandler) getWebViewId(pane *BrowserPane) string {
476 if pane == nil || pane.webView == nil {
477 return "unknown"
478 }
479 return pane.webView.IDString()
480}
481
482func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureName, action string) {
483 h.mu.Lock()
484 defer h.mu.Unlock()
485
486 if time.Since(*lastToggle) < 50*time.Millisecond {
487 log.Printf("[window-shortcuts] %s toggle debounced", featureName)
488 return
489 }
490 *lastToggle = time.Now()
491
492 if h.app.activePane == nil || h.app.activePane.webView == nil {
493 log.Printf("[window-shortcuts] No active pane for %s", featureName)
494 return
495 }
496
497 log.Printf("[window-shortcuts] %s toggle -> pane %p", featureName, h.app.activePane.webView)
498
499 h.ensureGUIInActivePane("omnibox")
500
501 if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{
502 "action": action,
503 "webviewId": h.getWebViewId(h.app.activePane),
504 "timestamp": time.Now().UnixMilli(),
505 "source": "window-global",
506 }); err != nil {
507 log.Printf("[window-shortcuts] Failed to dispatch %s toggle: %v", featureName, err)
508 }
509}
510
511func (h *WindowShortcutHandler) handleWorkspaceNavigation(direction string) {
512 if h.app == nil || h.app.workspace == nil {
513 log.Printf("[window-shortcuts] No workspace for navigation")
514 return
515 }
516
517 log.Printf("[window-shortcuts] Workspace navigation: %s", direction)
518
519 if h.app.workspace.FocusNeighbor(direction) {
520 log.Printf("[window-shortcuts] Workspace navigation %s successful", direction)
521 } else {
522 log.Printf("[window-shortcuts] Workspace navigation %s failed", direction)
523 }
524}
525
526func (h *WindowShortcutHandler) handleClosePane() {
527 if h.app == nil || h.app.workspace == nil {
528 log.Printf("[window-shortcuts] No workspace for close pane")
529 return
530 }
531
532 // Find the currently active WebView
533 var activeWebView *webkit.WebView
534 log.Printf("[window-shortcuts] Searching for active WebView among %d WebViews", len(h.app.workspace.viewToNode))
535 for webView := range h.app.workspace.viewToNode {
536 if webView != nil {
537 isActive := webView.IsActive()
538 log.Printf("[window-shortcuts] WebView %d: IsActive=%t", webView.ID(), isActive)
539 if isActive {
540 activeWebView = webView
541 log.Printf("[window-shortcuts] Found active WebView: %d", webView.ID())
542 break
543 }
544 }
545 }
546
547 if activeWebView == nil {
548 log.Printf("[window-shortcuts] No active WebView found, using workspace closeCurrentPane")
549 h.app.workspace.closeCurrentPane()
550 return
551 }
552
553 // Check if the active WebView is a popup
554 node := h.app.workspace.viewToNode[activeWebView]
555 if node != nil && node.isPopup {
556 log.Printf("[window-shortcuts] Closing popup via OnWorkspaceMessage")
557 // Use the proper close-popup message for popups
558 msg := messaging.Message{
559 Event: "close-popup",
560 WebViewID: activeWebView.IDString(),
561 Reason: "user-ctrl-w",
562 }
563 h.app.workspace.OnWorkspaceMessage(activeWebView, msg)
564 } else {
565 log.Printf("[window-shortcuts] Closing regular pane via workspace closeCurrentPane")
566 // Use regular close for non-popup panes
567 h.app.workspace.closeCurrentPane()
568 }
569}
570
571// handlePaneMode enters pane mode for modal pane management.
572func (h *WindowShortcutHandler) handlePaneMode() {
573 if h.app == nil || h.app.workspace == nil {
574 log.Printf("[window-shortcuts] Cannot enter pane mode: workspace not available")
575 return
576 }
577
578 log.Printf("[window-shortcuts] Entering pane mode")
579 h.app.workspace.EnterPaneMode()
580}
581
582// handleTabMode enters tab mode for modal tab management.
583func (h *WindowShortcutHandler) handleTabMode() {
584 if h.app == nil || h.app.tabManager == nil {
585 log.Printf("[window-shortcuts] Cannot enter tab mode: tab manager not available")
586 return
587 }
588
589 log.Printf("[window-shortcuts] Entering tab mode")
590 h.app.tabManager.EnterTabMode()
591}
592
593// handleNextTab switches to the next tab.
594func (h *WindowShortcutHandler) handleNextTab() {
595 if h.app == nil || h.app.tabManager == nil {
596 log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
597 return
598 }
599
600 log.Printf("[window-shortcuts] Switching to next tab")
601 if err := h.app.tabManager.NextTab(); err != nil {
602 log.Printf("[window-shortcuts] Failed to switch to next tab: %v", err)
603 }
604}
605
606// handlePrevTab switches to the previous tab.
607func (h *WindowShortcutHandler) handlePrevTab() {
608 if h.app == nil || h.app.tabManager == nil {
609 log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
610 return
611 }
612
613 log.Printf("[window-shortcuts] Switching to previous tab")
614 if err := h.app.tabManager.PreviousTab(); err != nil {
615 log.Printf("[window-shortcuts] Failed to switch to previous tab: %v", err)
616 }
617}
618
619// handleDirectTabSwitch switches to a specific tab by index (0-based).
620func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) {
621 if h.app == nil || h.app.tabManager == nil {
622 log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
623 return
624 }
625
626 log.Printf("[window-shortcuts] Direct tab switch to index %d", index)
627 if err := h.app.tabManager.SwitchToTab(index); err != nil {
628 log.Printf("[window-shortcuts] Failed to switch to tab %d: %v", index, err)
629 }
630}
631
632// isTabModeActive checks if tab mode is currently active
633func (h *WindowShortcutHandler) isTabModeActive() bool {
634 if h.app == nil || h.app.tabManager == nil {
635 return false
636 }
637 return h.app.tabManager.IsTabModeActive()
638}
639
640// isRenameInProgress checks if a tab rename is currently in progress
641func (h *WindowShortcutHandler) isRenameInProgress() bool {
642 if h.app == nil || h.app.tabManager == nil {
643 return false
644 }
645 return h.app.tabManager.IsRenameInProgress()
646}
647
648// handleTabModeAction handles tab mode action keys (n, x, l, h, etc.)
649// Only processes actions when tab mode is active
650func (h *WindowShortcutHandler) handleTabModeAction(action string) {
651 if h.app == nil || h.app.tabManager == nil {
652 return
653 }
654
655 // Tab mode is active, handle the action
656 log.Printf("[window-shortcuts] Tab mode action: %s", action)
657 h.app.tabManager.HandleTabAction(action)
658}
659
660// Cleanup releases resources
661func (h *WindowShortcutHandler) Cleanup() {
662 // WindowShortcuts doesn't require explicit cleanup
663 h.shortcuts = nil
664}
665
666// Error definitions
667var (
668 ErrFailedToInitialize = fmt.Errorf("failed to initialize window shortcuts")
669)