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 // These keys need to be checked here so they can propagate when tab mode is not active
191 if h.isTabModeActive() {
192 handled := false
193 action := ""
194
195 // Check for no modifiers (plain key press)
196 if !state.Has(gdk.ControlMask) && !state.Has(gdk.AltMask) {
197 switch keyval {
198 case gdkKeyN, gdkKeyC:
199 action = "new-tab"
200 handled = true
201 case gdkKeyX:
202 action = "close-tab"
203 handled = true
204 case gdkKeyL:
205 action = "next-tab"
206 handled = true
207 case gdkKeyH:
208 action = "previous-tab"
209 handled = true
210 case gdkKeyR:
211 action = "rename-tab"
212 handled = true
213 case gdkKeyTab:
214 action = "next-tab"
215 handled = true
216 case gdkKeyReturn:
217 action = "confirm"
218 handled = true
219 case gdkKeyEscape:
220 action = "cancel"
221 handled = true
222 }
223 }
224
225 // Handle Shift+Tab for previous tab in tab mode
226 if keyval == gdkKeyTab && state.Has(gdk.ShiftMask) && !state.Has(gdk.ControlMask) {
227 action = "previous-tab"
228 handled = true
229 }
230
231 if handled {
232 log.Printf("[window-shortcuts] Tab mode action: %s", action)
233 h.handleTabModeAction(action)
234 return true // Consume event
235 }
236 }
237
238 // Let all other keys propagate to WebView's keyboard bridge
239 // This includes Ctrl+P, Ctrl+T, and all pane mode action keys
240 return false
241 })
242
243 gtkWindow.AddController(keyController)
244 log.Printf("[window-shortcuts] Registered capture phase shortcuts: Ctrl+Tab, Alt+number")
245
246 return nil
247}
248
249func (h *WindowShortcutHandler) handleOmniboxToggle() {
250 h.handleUIToggle(&h.lastOmniboxToggle, "omnibox", "omnibox-nav-toggle")
251}
252
253func (h *WindowShortcutHandler) handleFindToggle() {
254 h.handleUIToggle(&h.lastFindToggle, "find", "omnibox-find-toggle")
255}
256
257func (h *WindowShortcutHandler) handleCopyURL() {
258 h.mu.Lock()
259 defer h.mu.Unlock()
260
261 if time.Since(h.lastCopyURL) < 100*time.Millisecond {
262 return
263 }
264 h.lastCopyURL = time.Now()
265
266 if h.app.activePane == nil || h.app.activePane.webView == nil {
267 log.Printf("[window-shortcuts] No active pane for copy URL")
268 return
269 }
270
271 log.Printf("[window-shortcuts] Copy URL -> pane %p", h.app.activePane.webView)
272
273 // Execute copy URL script directly on active pane
274 script := `
275 (async function() {
276 const toast = (message, type) => {
277 document.dispatchEvent(new CustomEvent('dumber:toast', {
278 detail: { message, duration: 2000, type }
279 }));
280 };
281
282 try {
283 const url = window.location.href;
284 await navigator.clipboard.writeText(url);
285 console.log('🔗 Window shortcut copied URL:', url);
286 toast('URL copied to clipboard!', 'success');
287 } catch (error) {
288 console.error('❌ Failed to copy URL:', error);
289 toast('Failed to copy URL', 'error');
290 }
291 })();
292 `
293
294 if err := h.app.activePane.webView.InjectScript(script); err != nil {
295 log.Printf("[window-shortcuts] Failed to inject copy URL script: %v", err)
296 }
297}
298
299func (h *WindowShortcutHandler) handleDevTools() {
300 h.mu.Lock()
301 defer h.mu.Unlock()
302
303 if time.Since(h.lastDevTools) < 200*time.Millisecond {
304 return
305 }
306 h.lastDevTools = time.Now()
307
308 if h.app.activePane == nil || h.app.activePane.webView == nil {
309 log.Printf("[window-shortcuts] No active pane for devtools")
310 return
311 }
312
313 log.Printf("[window-shortcuts] DevTools -> pane %p", h.app.activePane.webView)
314
315 if err := h.app.activePane.webView.ShowDevTools(); err != nil {
316 log.Printf("[window-shortcuts] Failed to show devtools: %v", err)
317 }
318}
319
320func (h *WindowShortcutHandler) handlePrint() {
321 h.mu.Lock()
322 defer h.mu.Unlock()
323
324 if time.Since(h.lastPrint) < 200*time.Millisecond {
325 return
326 }
327 h.lastPrint = time.Now()
328
329 if h.app.activePane == nil || h.app.activePane.webView == nil {
330 log.Printf("[window-shortcuts] No active pane for print")
331 return
332 }
333
334 log.Printf("[window-shortcuts] Print -> pane %p", h.app.activePane.webView)
335
336 if err := h.app.activePane.webView.ShowPrintDialog(); err != nil {
337 log.Printf("[window-shortcuts] Failed to show print dialog: %v", err)
338 }
339}
340
341func (h *WindowShortcutHandler) handleReload() {
342 h.mu.Lock()
343 defer h.mu.Unlock()
344
345 if h.app.activePane == nil || h.app.activePane.webView == nil {
346 log.Printf("[window-shortcuts] No active pane for reload")
347 return
348 }
349
350 log.Printf("[window-shortcuts] Reload page -> pane %p", h.app.activePane.webView)
351
352 if err := h.app.activePane.webView.Reload(); err != nil {
353 log.Printf("[window-shortcuts] Failed to reload page: %v", err)
354 }
355}
356
357func (h *WindowShortcutHandler) handleHardReload() {
358 h.mu.Lock()
359 defer h.mu.Unlock()
360
361 if h.app.activePane == nil || h.app.activePane.webView == nil {
362 log.Printf("[window-shortcuts] No active pane for hard reload")
363 return
364 }
365
366 log.Printf("[window-shortcuts] Hard reload (bypass cache) -> pane %p", h.app.activePane.webView)
367
368 if err := h.app.activePane.webView.ReloadBypassCache(); err != nil {
369 log.Printf("[window-shortcuts] Failed to hard reload page: %v", err)
370 }
371}
372
373// handleZoomIn increases zoom level by 10%
374func (h *WindowShortcutHandler) handleZoomIn() {
375 h.handleZoom("in", 1.1)
376}
377
378// handleZoomOut decreases zoom level by 10%
379func (h *WindowShortcutHandler) handleZoomOut() {
380 h.handleZoom("out", 1.0/1.1)
381}
382
383// handleZoomReset resets zoom to 100%
384func (h *WindowShortcutHandler) handleZoomReset() {
385 h.handleZoom("reset", 1.0)
386}
387
388// handleZoom applies zoom changes to the active pane
389func (h *WindowShortcutHandler) handleZoom(action string, multiplier float64) {
390 h.mu.Lock()
391 defer h.mu.Unlock()
392
393 if h.app.activePane == nil || h.app.activePane.webView == nil {
394 log.Printf("[window-shortcuts] No active pane for zoom %s", action)
395 return
396 }
397
398 activeWebView := h.app.activePane.webView
399 log.Printf("[window-shortcuts] Zoom %s -> pane %p", action, activeWebView)
400
401 // Get current zoom level
402 currentZoom := activeWebView.GetZoom()
403
404 // Calculate new zoom level
405 var newZoom float64
406 if action == "reset" {
407 newZoom = 1.0
408 } else {
409 newZoom = currentZoom * multiplier
410 // Apply zoom limits
411 if newZoom < 0.25 {
412 newZoom = 0.25
413 }
414 if newZoom > 5.0 {
415 newZoom = 5.0
416 }
417 }
418
419 log.Printf("[window-shortcuts] Zoom %s: %.2f -> %.2f", action, currentZoom, newZoom)
420
421 // Apply zoom to active pane
422 if err := activeWebView.SetZoom(newZoom); err != nil {
423 log.Printf("[window-shortcuts] Failed to set zoom: %v", err)
424 return
425 }
426
427 // Ensure GUI/toast component is loaded before showing toast
428 h.ensureGUIInActivePane("toast")
429
430 // Show zoom toast notification
431 toastScript := fmt.Sprintf(`
432 (function() {
433 try {
434 if (typeof window.__dumber_showZoomToast === 'function') {
435 window.__dumber_showZoomToast(%f);
436 }
437 } catch (e) {
438 console.error('[window-shortcuts] Failed to show zoom toast:', e);
439 }
440 })();
441 `, newZoom)
442
443 if err := activeWebView.InjectScript(toastScript); err != nil {
444 log.Printf("[window-shortcuts] Failed to show zoom toast: %v", err)
445 }
446}
447
448func (h *WindowShortcutHandler) ensureGUIInActivePane(component string) {
449 if h.app.activePane == nil {
450 return
451 }
452
453 pane := h.app.activePane
454 if !pane.HasGUI() {
455 log.Printf("[window-shortcuts] Injecting GUI into pane %s for %s", pane.ID(), component)
456 if h.app.workspace != nil {
457 h.app.workspace.ensureGUIInPane(pane)
458 }
459 }
460
461 // Ensure specific component is available
462 if !pane.HasGUIComponent(component) {
463 log.Printf("[window-shortcuts] Ensuring %s component in pane %s", component, pane.ID())
464 pane.SetGUIComponent(component, true)
465 }
466}
467
468func (h *WindowShortcutHandler) getWebViewId(pane *BrowserPane) string {
469 if pane == nil || pane.webView == nil {
470 return "unknown"
471 }
472 return pane.webView.IDString()
473}
474
475func (h *WindowShortcutHandler) handleUIToggle(lastToggle *time.Time, featureName, action string) {
476 h.mu.Lock()
477 defer h.mu.Unlock()
478
479 if time.Since(*lastToggle) < 50*time.Millisecond {
480 log.Printf("[window-shortcuts] %s toggle debounced", featureName)
481 return
482 }
483 *lastToggle = time.Now()
484
485 if h.app.activePane == nil || h.app.activePane.webView == nil {
486 log.Printf("[window-shortcuts] No active pane for %s", featureName)
487 return
488 }
489
490 log.Printf("[window-shortcuts] %s toggle -> pane %p", featureName, h.app.activePane.webView)
491
492 h.ensureGUIInActivePane("omnibox")
493
494 if err := h.app.activePane.webView.DispatchCustomEvent("dumber:ui:shortcut", map[string]any{
495 "action": action,
496 "webviewId": h.getWebViewId(h.app.activePane),
497 "timestamp": time.Now().UnixMilli(),
498 "source": "window-global",
499 }); err != nil {
500 log.Printf("[window-shortcuts] Failed to dispatch %s toggle: %v", featureName, err)
501 }
502}
503
504func (h *WindowShortcutHandler) handleWorkspaceNavigation(direction string) {
505 if h.app == nil || h.app.workspace == nil {
506 log.Printf("[window-shortcuts] No workspace for navigation")
507 return
508 }
509
510 log.Printf("[window-shortcuts] Workspace navigation: %s", direction)
511
512 if h.app.workspace.FocusNeighbor(direction) {
513 log.Printf("[window-shortcuts] Workspace navigation %s successful", direction)
514 } else {
515 log.Printf("[window-shortcuts] Workspace navigation %s failed", direction)
516 }
517}
518
519func (h *WindowShortcutHandler) handleClosePane() {
520 if h.app == nil || h.app.workspace == nil {
521 log.Printf("[window-shortcuts] No workspace for close pane")
522 return
523 }
524
525 // Find the currently active WebView
526 var activeWebView *webkit.WebView
527 log.Printf("[window-shortcuts] Searching for active WebView among %d WebViews", len(h.app.workspace.viewToNode))
528 for webView := range h.app.workspace.viewToNode {
529 if webView != nil {
530 isActive := webView.IsActive()
531 log.Printf("[window-shortcuts] WebView %d: IsActive=%t", webView.ID(), isActive)
532 if isActive {
533 activeWebView = webView
534 log.Printf("[window-shortcuts] Found active WebView: %d", webView.ID())
535 break
536 }
537 }
538 }
539
540 if activeWebView == nil {
541 log.Printf("[window-shortcuts] No active WebView found, using workspace closeCurrentPane")
542 h.app.workspace.closeCurrentPane()
543 return
544 }
545
546 // Check if the active WebView is a popup
547 node := h.app.workspace.viewToNode[activeWebView]
548 if node != nil && node.isPopup {
549 log.Printf("[window-shortcuts] Closing popup via OnWorkspaceMessage")
550 // Use the proper close-popup message for popups
551 msg := messaging.Message{
552 Event: "close-popup",
553 WebViewID: activeWebView.IDString(),
554 Reason: "user-ctrl-w",
555 }
556 h.app.workspace.OnWorkspaceMessage(activeWebView, msg)
557 } else {
558 log.Printf("[window-shortcuts] Closing regular pane via workspace closeCurrentPane")
559 // Use regular close for non-popup panes
560 h.app.workspace.closeCurrentPane()
561 }
562}
563
564// handlePaneMode enters pane mode for modal pane management.
565func (h *WindowShortcutHandler) handlePaneMode() {
566 if h.app == nil || h.app.workspace == nil {
567 log.Printf("[window-shortcuts] Cannot enter pane mode: workspace not available")
568 return
569 }
570
571 log.Printf("[window-shortcuts] Entering pane mode")
572 h.app.workspace.EnterPaneMode()
573}
574
575// handleTabMode enters tab mode for modal tab management.
576func (h *WindowShortcutHandler) handleTabMode() {
577 if h.app == nil || h.app.tabManager == nil {
578 log.Printf("[window-shortcuts] Cannot enter tab mode: tab manager not available")
579 return
580 }
581
582 log.Printf("[window-shortcuts] Entering tab mode")
583 h.app.tabManager.EnterTabMode()
584}
585
586// handleNextTab switches to the next tab.
587func (h *WindowShortcutHandler) handleNextTab() {
588 if h.app == nil || h.app.tabManager == nil {
589 log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
590 return
591 }
592
593 log.Printf("[window-shortcuts] Switching to next tab")
594 if err := h.app.tabManager.NextTab(); err != nil {
595 log.Printf("[window-shortcuts] Failed to switch to next tab: %v", err)
596 }
597}
598
599// handlePrevTab switches to the previous tab.
600func (h *WindowShortcutHandler) handlePrevTab() {
601 if h.app == nil || h.app.tabManager == nil {
602 log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
603 return
604 }
605
606 log.Printf("[window-shortcuts] Switching to previous tab")
607 if err := h.app.tabManager.PreviousTab(); err != nil {
608 log.Printf("[window-shortcuts] Failed to switch to previous tab: %v", err)
609 }
610}
611
612// handleDirectTabSwitch switches to a specific tab by index (0-based).
613func (h *WindowShortcutHandler) handleDirectTabSwitch(index int) {
614 if h.app == nil || h.app.tabManager == nil {
615 log.Printf("[window-shortcuts] Cannot switch tab: tab manager not available")
616 return
617 }
618
619 log.Printf("[window-shortcuts] Direct tab switch to index %d", index)
620 if err := h.app.tabManager.SwitchToTab(index); err != nil {
621 log.Printf("[window-shortcuts] Failed to switch to tab %d: %v", index, err)
622 }
623}
624
625// isTabModeActive checks if tab mode is currently active
626func (h *WindowShortcutHandler) isTabModeActive() bool {
627 if h.app == nil || h.app.tabManager == nil {
628 return false
629 }
630 return h.app.tabManager.IsTabModeActive()
631}
632
633// handleTabModeAction handles tab mode action keys (n, x, l, h, etc.)
634// Only processes actions when tab mode is active
635func (h *WindowShortcutHandler) handleTabModeAction(action string) {
636 if h.app == nil || h.app.tabManager == nil {
637 return
638 }
639
640 // Tab mode is active, handle the action
641 log.Printf("[window-shortcuts] Tab mode action: %s", action)
642 h.app.tabManager.HandleTabAction(action)
643}
644
645// Cleanup releases resources
646func (h *WindowShortcutHandler) Cleanup() {
647 // WindowShortcuts doesn't require explicit cleanup
648 h.shortcuts = nil
649}
650
651// Error definitions
652var (
653 ErrFailedToInitialize = fmt.Errorf("failed to initialize window shortcuts")
654)