v0.13.0 976176a

shortcuts.go RawHistoryFolder

1package browser
2
3import (
4	"encoding/json"
5	"fmt"
6	"log"
7
8	"github.com/bnema/dumber/internal/app/control"
9	"github.com/bnema/dumber/internal/config"
10	"github.com/bnema/dumber/pkg/webkit"
11)
12
13// ShortcutHandler manages keyboard shortcuts for the browser
14type ShortcutHandler struct {
15	webView             *webkit.WebView
16	clipboardController *control.ClipboardController
17	config              *config.Config
18	app                 *BrowserApp
19}
20
21// NewShortcutHandler creates a new shortcut handler
22func NewShortcutHandler(webView *webkit.WebView, clipboardController *control.ClipboardController, cfg *config.Config, app *BrowserApp) *ShortcutHandler {
23	return &ShortcutHandler{
24		webView:             webView,
25		clipboardController: clipboardController,
26		config:              cfg,
27		app:                 app,
28	}
29}
30
31func (s *ShortcutHandler) isActivePane() bool {
32	if s == nil || s.webView == nil {
33		return false
34	}
35	if s.app == nil || s.app.workspace == nil {
36		return true // No workspace context; allow
37	}
38
39	// Find node for this WebView
40	node := s.app.workspace.viewToNode[s.webView]
41	if node == nil {
42		return false
43	}
44
45	// For related popups, they're active if parent chain contains the active node
46	if node.isRelated && node.parentPane != nil {
47		activeNode := s.app.workspace.GetActiveNode()
48		cur := node
49		for cur != nil {
50			if cur == activeNode {
51				return true
52			}
53			cur = cur.parentPane
54		}
55		return false
56	}
57
58	// Independent panes: equal to active node
59	return node == s.app.workspace.GetActiveNode()
60}
61
62// RegisterShortcuts registers pane-specific keyboard shortcuts with focus guards
63func (s *ShortcutHandler) RegisterShortcuts() {
64	s.exposeWorkspaceConfig()
65
66	// NOTE: Global shortcuts (Ctrl+L, Ctrl+F, Ctrl+Shift+C, F12) are now handled
67	// by WindowShortcutHandler at the window level to prevent duplicates
68
69	// Page refresh shortcuts
70	_ = s.webView.RegisterKeyboardShortcut("ctrl+r", 0, func() {
71		if !s.isActivePane() {
72			return
73		}
74		log.Printf("Shortcut: Reload page")
75		_ = s.webView.Reload()
76	})
77
78	_ = s.webView.RegisterKeyboardShortcut("ctrl+shift+r", 0, func() {
79		if !s.isActivePane() {
80			return
81		}
82		log.Printf("Shortcut: Hard reload page")
83		_ = s.webView.ReloadBypassCache()
84	})
85
86	_ = s.webView.RegisterKeyboardShortcut("F5", 0, func() {
87		if !s.isActivePane() {
88			return
89		}
90		log.Printf("Shortcut: F5 reload")
91		_ = s.webView.Reload()
92	})
93
94	// Note: Workspace pane navigation shortcuts (Alt + Arrow keys) are handled by WorkspaceManager
95	// and zoom shortcuts (Ctrl +/-/0) are handled by WindowShortcutHandler to ensure they work properly across all panes
96}
97
98func (s *ShortcutHandler) dispatchUIShortcut(action string, extra map[string]any) error {
99	if s == nil || s.webView == nil {
100		return fmt.Errorf("webview unavailable for action %s", action)
101	}
102
103	detail := map[string]any{
104		"action": action,
105	}
106	for k, v := range extra {
107		detail[k] = v
108	}
109
110	return s.webView.DispatchCustomEvent("dumber:ui:shortcut", detail)
111}
112
113func (s *ShortcutHandler) exposeWorkspaceConfig() {
114	if s.webView == nil || s.config == nil {
115		return
116	}
117
118	payload, err := json.Marshal(s.config.Workspace)
119	if err != nil {
120		log.Printf("Shortcut: failed to marshal workspace config: %v", err)
121		return
122	}
123
124	script := fmt.Sprintf(`(function(){
125	const cfg = %s;
126	window.__dumber_workspace_config = cfg;
127	document.dispatchEvent(new CustomEvent('dumber:workspace-config',{detail:cfg}));
128})();`, string(payload))
129
130	if err := s.webView.InjectScript(script); err != nil {
131		log.Printf("Shortcut: failed to expose workspace config: %v", err)
132	}
133}
134
135// Detach releases the WebView reference to prevent post-destruction usage.
136func (s *ShortcutHandler) Detach() {
137	if s == nil {
138		return
139	}
140	s.webView = nil
141}

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