detached d7ec53a

fix(shortcuts): prevent window shortcuts from being blocked by webpage protection

- Remove window shortcuts from global blocking registry to allow GTK handling
- Window shortcuts (ctrl+l, ctrl+f, F12, etc.) now properly bubble up to GTK
- Only pane-level shortcuts are blocked from reaching webpages
- Fixes omnibox not working in new panes when global shortcuts protection is active
- Add dynamic global shortcuts protection system for pane shortcuts only
commit
d7ec53a416df205aae672f7229a91d1da06a5fb8
author
bamen · · 2025-09-23 05:08 UTC
parent
d7fd740
signature
unsigned
4 files · +220 -9
M pkg/webkit/keyboard_cgo.go +4 -0
@@ -133,6 +133,10 @@ func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error
133133 }
134134 viewShortcuts[w.id][accel] = callback
135135 regMu.Unlock()
136+
137+ // Register in global shortcuts registry for automatic webpage blocking
138+ RegisterGlobalShortcut(accel)
139+
136140 return nil
137141 }
138142
@@ -133,6 +133,10 @@ func (w *WebView) RegisterKeyboardShortcut(accel string, callback func()) error
133 }133 }
134 viewShortcuts[w.id][accel] = callback134 viewShortcuts[w.id][accel] = callback
135 regMu.Unlock()135 regMu.Unlock()
136+
137+ // Register in global shortcuts registry for automatic webpage blocking
138+ RegisterGlobalShortcut(accel)
139+
136 return nil140 return nil
137 }141 }
138 142
M pkg/webkit/shortcuts_cgo.go +208 -8
@@ -2,6 +2,14 @@
22
33 package webkit
44
5+import (
6+ "log"
7+ "strings"
8+ "sync"
9+ "sync/atomic"
10+ "unsafe"
11+)
12+
513 /*
614 #cgo pkg-config: gtk4 webkitgtk-6.0
715 #include <gtk/gtk.h>
@@ -59,13 +67,6 @@ static void add_shortcut_to_controller(GtkShortcutController* controller,
5967 */
6068 import "C"
6169
62-import (
63- "log"
64- "sync"
65- "sync/atomic"
66- "unsafe"
67-)
68-
6970 var (
7071 globalShortcutMu sync.Mutex
7172 globalShortcutCounter uint64
@@ -144,11 +145,210 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err
144145 return nil
145146 }
146147
147-// Keyboard event blocking functionality for omnibox isolation
148+// Global shortcuts registry to track all registered shortcuts for automatic blocking
149+var (
150+ globalShortcutsRegistry = make(map[string]bool)
151+ shortcutsRegistryMu sync.RWMutex
152+)
153+
154+// RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking
155+func RegisterGlobalShortcut(shortcut string) {
156+ shortcutsRegistryMu.Lock()
157+ defer shortcutsRegistryMu.Unlock()
158+
159+ // Normalize shortcut format (convert to lowercase, standardize modifiers)
160+ normalized := normalizeShortcut(shortcut)
161+
162+ // Only log if this is a new shortcut registration
163+ if !globalShortcutsRegistry[normalized] {
164+ globalShortcutsRegistry[normalized] = true
165+ log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized)
166+ }
167+}
168+
169+// GetRegisteredShortcuts returns a copy of all registered shortcuts
170+func GetRegisteredShortcuts() []string {
171+ shortcutsRegistryMu.RLock()
172+ defer shortcutsRegistryMu.RUnlock()
173+
174+ shortcuts := make([]string, 0, len(globalShortcutsRegistry))
175+ for shortcut := range globalShortcutsRegistry {
176+ shortcuts = append(shortcuts, shortcut)
177+ }
178+ return shortcuts
179+}
180+
181+// normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking
182+func normalizeShortcut(shortcut string) string {
183+ // Convert to lowercase and replace common variations
184+ normalized := strings.ToLower(shortcut)
185+
186+ // Standardize modifier keys
187+ normalized = strings.ReplaceAll(normalized, "cmdorctrl+", "ctrl+")
188+ normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking
189+ normalized = strings.ReplaceAll(normalized, "control+", "ctrl+")
190+
191+ // Standardize key names
192+ normalized = strings.ReplaceAll(normalized, "arrowleft", "arrowleft")
193+ normalized = strings.ReplaceAll(normalized, "arrowright", "arrowright")
194+ normalized = strings.ReplaceAll(normalized, "arrowup", "arrowup")
195+ normalized = strings.ReplaceAll(normalized, "arrowdown", "arrowdown")
196+ normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+=
197+ normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to =
198+
199+ return normalized
200+}
201+
202+// Keyboard event blocking functionality for global shortcuts protection
148203
149204 // keyboardBlockerScript holds reference to the main-world keyboard blocking script
150205 var keyboardBlockerScript *C.WebKitUserScript = nil
151206
207+// EnableGlobalShortcutsProtection injects a script to block all registered global shortcuts from reaching webpages
208+func (w *WebView) EnableGlobalShortcutsProtection() error {
209+ if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
210+ return ErrWebViewNotInitialized
211+ }
212+
213+ // Get all registered shortcuts
214+ shortcuts := GetRegisteredShortcuts()
215+ if len(shortcuts) == 0 {
216+ log.Printf("[shortcuts] No global shortcuts registered, skipping protection")
217+ return nil
218+ }
219+
220+ // Generate JavaScript array of shortcuts to block
221+ shortcutsJS := "["
222+ for i, shortcut := range shortcuts {
223+ if i > 0 {
224+ shortcutsJS += ","
225+ }
226+ shortcutsJS += `"` + shortcut + `"`
227+ }
228+ shortcutsJS += "]"
229+
230+ // Create the blocking script that only blocks registered global shortcuts
231+ globalProtectionScript := `
232+ (() => {
233+ if (!window.__dumber_global_shortcuts_protection) {
234+ window.__dumber_global_shortcuts_protection = true;
235+
236+ // List of global shortcuts to block from reaching the webpage
237+ const globalShortcuts = ` + shortcutsJS + `;
238+ console.log('[dumber] Protecting global shortcuts:', globalShortcuts);
239+
240+ // Function to check if a key event matches any global shortcut
241+ const isGlobalShortcut = (e) => {
242+ const key = e.key.toLowerCase();
243+ const code = e.code.toLowerCase();
244+
245+ // Build shortcut string (similar to GTK format)
246+ let shortcut = '';
247+ if (e.ctrlKey) shortcut += 'ctrl+';
248+ if (e.altKey) shortcut += 'alt+';
249+ if (e.shiftKey) shortcut += 'shift+';
250+ if (e.metaKey) shortcut += 'meta+';
251+
252+ // Try both key and code for matching
253+ const keyShortcut = shortcut + key;
254+ const codeShortcut = shortcut + code;
255+
256+ // Special case mappings for common shortcuts
257+ const specialMappings = {
258+ 'ctrl+equal': ['ctrl+=', 'ctrl+plus'],
259+ 'ctrl+minus': ['ctrl+-'],
260+ 'f12': ['f12'],
261+ 'ctrl+r': ['ctrl+r'],
262+ 'ctrl+shift+r': ['ctrl+shift+r'],
263+ 'f5': ['f5']
264+ };
265+
266+ // Check direct matches
267+ if (globalShortcuts.includes(keyShortcut) || globalShortcuts.includes(codeShortcut)) {
268+ return true;
269+ }
270+
271+ // Check special mappings
272+ for (const [canonical, variants] of Object.entries(specialMappings)) {
273+ if (variants.includes(keyShortcut) || variants.includes(codeShortcut)) {
274+ if (globalShortcuts.includes(canonical)) {
275+ return true;
276+ }
277+ }
278+ }
279+
280+ return false;
281+ };
282+
283+ // Block only global shortcuts at capture phase
284+ const blockGlobalShortcut = (e) => {
285+ if (isGlobalShortcut(e)) {
286+ console.log('[dumber] Blocked global shortcut from reaching page:', e.key, e.ctrlKey, e.altKey, e.shiftKey);
287+ e.stopImmediatePropagation();
288+ e.preventDefault();
289+ e.stopPropagation();
290+ return false;
291+ }
292+ // Let other key events through normally
293+ };
294+
295+ // Install listeners for keyboard events
296+ document.addEventListener('keydown', blockGlobalShortcut, true);
297+ document.addEventListener('keyup', blockGlobalShortcut, true);
298+ document.addEventListener('keypress', blockGlobalShortcut, true);
299+
300+ // Store cleanup function
301+ window.__dumber_cleanup_global_shortcuts = () => {
302+ document.removeEventListener('keydown', blockGlobalShortcut, true);
303+ document.removeEventListener('keyup', blockGlobalShortcut, true);
304+ document.removeEventListener('keypress', blockGlobalShortcut, true);
305+ delete window.__dumber_global_shortcuts_protection;
306+ delete window.__dumber_cleanup_global_shortcuts;
307+ console.log('[dumber] Global shortcuts protection disabled');
308+ };
309+
310+ console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts');
311+ }
312+ })();
313+ `
314+
315+ // Inject the script into the main world at document start
316+ cScript := C.CString(globalProtectionScript)
317+ defer C.free(unsafe.Pointer(cScript))
318+
319+ script := C.webkit_user_script_new(
320+ (*C.gchar)(cScript),
321+ C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
322+ C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
323+ nil, nil) // nil = main world
324+
325+ if script == nil {
326+ return ErrNotImplemented
327+ }
328+
329+ C.webkit_user_content_manager_add_script(w.native.ucm, script)
330+ C.webkit_user_script_unref(script) // Clean up reference
331+
332+ log.Printf("[shortcuts] Global shortcuts protection enabled for %d shortcuts", len(shortcuts))
333+ return nil
334+}
335+
336+// DisableGlobalShortcutsProtection removes the global shortcuts protection
337+func (w *WebView) DisableGlobalShortcutsProtection() error {
338+ if w == nil || w.destroyed || w.native == nil {
339+ return ErrWebViewNotInitialized
340+ }
341+
342+ // Execute cleanup script
343+ cleanupScript := `
344+ if (window.__dumber_cleanup_global_shortcuts) {
345+ window.__dumber_cleanup_global_shortcuts();
346+ }
347+ `
348+
349+ return w.InjectScript(cleanupScript)
350+}
351+
152352 // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events
153353 // This prevents page JavaScript from receiving keyboard events while omnibox is active
154354 func (w *WebView) EnablePageKeyboardBlocking() error {
@@ -2,6 +2,14 @@
2 2
3 package webkit3 package webkit
4 4
5+import (
6+ "log"
7+ "strings"
8+ "sync"
9+ "sync/atomic"
10+ "unsafe"
11+)
12+
5 /*13 /*
6 #cgo pkg-config: gtk4 webkitgtk-6.014 #cgo pkg-config: gtk4 webkitgtk-6.0
7 #include <gtk/gtk.h>15 #include <gtk/gtk.h>
@@ -59,13 +67,6 @@ static void add_shortcut_to_controller(GtkShortcutController* controller,
59 */67 */
60 import "C"68 import "C"
61 69
62-import (
63- "log"
64- "sync"
65- "sync/atomic"
66- "unsafe"
67-)
68-
69 var (70 var (
70 globalShortcutMu sync.Mutex71 globalShortcutMu sync.Mutex
71 globalShortcutCounter uint6472 globalShortcutCounter uint64
@@ -144,11 +145,210 @@ func AddShortcutToController(controller uintptr, key string, handle uintptr) err
144 return nil145 return nil
145 }146 }
146 147
147-// Keyboard event blocking functionality for omnibox isolation148+// Global shortcuts registry to track all registered shortcuts for automatic blocking
149+var (
150+ globalShortcutsRegistry = make(map[string]bool)
151+ shortcutsRegistryMu sync.RWMutex
152+)
153+
154+// RegisterGlobalShortcut adds a shortcut to the global registry for automatic webpage blocking
155+func RegisterGlobalShortcut(shortcut string) {
156+ shortcutsRegistryMu.Lock()
157+ defer shortcutsRegistryMu.Unlock()
158+
159+ // Normalize shortcut format (convert to lowercase, standardize modifiers)
160+ normalized := normalizeShortcut(shortcut)
161+
162+ // Only log if this is a new shortcut registration
163+ if !globalShortcutsRegistry[normalized] {
164+ globalShortcutsRegistry[normalized] = true
165+ log.Printf("[shortcuts] Registered global shortcut for blocking: %s -> %s", shortcut, normalized)
166+ }
167+}
168+
169+// GetRegisteredShortcuts returns a copy of all registered shortcuts
170+func GetRegisteredShortcuts() []string {
171+ shortcutsRegistryMu.RLock()
172+ defer shortcutsRegistryMu.RUnlock()
173+
174+ shortcuts := make([]string, 0, len(globalShortcutsRegistry))
175+ for shortcut := range globalShortcutsRegistry {
176+ shortcuts = append(shortcuts, shortcut)
177+ }
178+ return shortcuts
179+}
180+
181+// normalizeShortcut converts shortcuts to a standardized format for JavaScript blocking
182+func normalizeShortcut(shortcut string) string {
183+ // Convert to lowercase and replace common variations
184+ normalized := strings.ToLower(shortcut)
185+
186+ // Standardize modifier keys
187+ normalized = strings.ReplaceAll(normalized, "cmdorctrl+", "ctrl+")
188+ normalized = strings.ReplaceAll(normalized, "cmd+", "ctrl+") // Treat cmd as ctrl for blocking
189+ normalized = strings.ReplaceAll(normalized, "control+", "ctrl+")
190+
191+ // Standardize key names
192+ normalized = strings.ReplaceAll(normalized, "arrowleft", "arrowleft")
193+ normalized = strings.ReplaceAll(normalized, "arrowright", "arrowright")
194+ normalized = strings.ReplaceAll(normalized, "arrowup", "arrowup")
195+ normalized = strings.ReplaceAll(normalized, "arrowdown", "arrowdown")
196+ normalized = strings.ReplaceAll(normalized, "equal", "equal") // For Ctrl+=
197+ normalized = strings.ReplaceAll(normalized, "plus", "equal") // Normalize + to =
198+
199+ return normalized
200+}
201+
202+// Keyboard event blocking functionality for global shortcuts protection
148 203
149 // keyboardBlockerScript holds reference to the main-world keyboard blocking script204 // keyboardBlockerScript holds reference to the main-world keyboard blocking script
150 var keyboardBlockerScript *C.WebKitUserScript = nil205 var keyboardBlockerScript *C.WebKitUserScript = nil
151 206
207+// EnableGlobalShortcutsProtection injects a script to block all registered global shortcuts from reaching webpages
208+func (w *WebView) EnableGlobalShortcutsProtection() error {
209+ if w == nil || w.destroyed || w.native == nil || w.native.ucm == nil {
210+ return ErrWebViewNotInitialized
211+ }
212+
213+ // Get all registered shortcuts
214+ shortcuts := GetRegisteredShortcuts()
215+ if len(shortcuts) == 0 {
216+ log.Printf("[shortcuts] No global shortcuts registered, skipping protection")
217+ return nil
218+ }
219+
220+ // Generate JavaScript array of shortcuts to block
221+ shortcutsJS := "["
222+ for i, shortcut := range shortcuts {
223+ if i > 0 {
224+ shortcutsJS += ","
225+ }
226+ shortcutsJS += `"` + shortcut + `"`
227+ }
228+ shortcutsJS += "]"
229+
230+ // Create the blocking script that only blocks registered global shortcuts
231+ globalProtectionScript := `
232+ (() => {
233+ if (!window.__dumber_global_shortcuts_protection) {
234+ window.__dumber_global_shortcuts_protection = true;
235+
236+ // List of global shortcuts to block from reaching the webpage
237+ const globalShortcuts = ` + shortcutsJS + `;
238+ console.log('[dumber] Protecting global shortcuts:', globalShortcuts);
239+
240+ // Function to check if a key event matches any global shortcut
241+ const isGlobalShortcut = (e) => {
242+ const key = e.key.toLowerCase();
243+ const code = e.code.toLowerCase();
244+
245+ // Build shortcut string (similar to GTK format)
246+ let shortcut = '';
247+ if (e.ctrlKey) shortcut += 'ctrl+';
248+ if (e.altKey) shortcut += 'alt+';
249+ if (e.shiftKey) shortcut += 'shift+';
250+ if (e.metaKey) shortcut += 'meta+';
251+
252+ // Try both key and code for matching
253+ const keyShortcut = shortcut + key;
254+ const codeShortcut = shortcut + code;
255+
256+ // Special case mappings for common shortcuts
257+ const specialMappings = {
258+ 'ctrl+equal': ['ctrl+=', 'ctrl+plus'],
259+ 'ctrl+minus': ['ctrl+-'],
260+ 'f12': ['f12'],
261+ 'ctrl+r': ['ctrl+r'],
262+ 'ctrl+shift+r': ['ctrl+shift+r'],
263+ 'f5': ['f5']
264+ };
265+
266+ // Check direct matches
267+ if (globalShortcuts.includes(keyShortcut) || globalShortcuts.includes(codeShortcut)) {
268+ return true;
269+ }
270+
271+ // Check special mappings
272+ for (const [canonical, variants] of Object.entries(specialMappings)) {
273+ if (variants.includes(keyShortcut) || variants.includes(codeShortcut)) {
274+ if (globalShortcuts.includes(canonical)) {
275+ return true;
276+ }
277+ }
278+ }
279+
280+ return false;
281+ };
282+
283+ // Block only global shortcuts at capture phase
284+ const blockGlobalShortcut = (e) => {
285+ if (isGlobalShortcut(e)) {
286+ console.log('[dumber] Blocked global shortcut from reaching page:', e.key, e.ctrlKey, e.altKey, e.shiftKey);
287+ e.stopImmediatePropagation();
288+ e.preventDefault();
289+ e.stopPropagation();
290+ return false;
291+ }
292+ // Let other key events through normally
293+ };
294+
295+ // Install listeners for keyboard events
296+ document.addEventListener('keydown', blockGlobalShortcut, true);
297+ document.addEventListener('keyup', blockGlobalShortcut, true);
298+ document.addEventListener('keypress', blockGlobalShortcut, true);
299+
300+ // Store cleanup function
301+ window.__dumber_cleanup_global_shortcuts = () => {
302+ document.removeEventListener('keydown', blockGlobalShortcut, true);
303+ document.removeEventListener('keyup', blockGlobalShortcut, true);
304+ document.removeEventListener('keypress', blockGlobalShortcut, true);
305+ delete window.__dumber_global_shortcuts_protection;
306+ delete window.__dumber_cleanup_global_shortcuts;
307+ console.log('[dumber] Global shortcuts protection disabled');
308+ };
309+
310+ console.log('[dumber] Global shortcuts protection enabled for', globalShortcuts.length, 'shortcuts');
311+ }
312+ })();
313+ `
314+
315+ // Inject the script into the main world at document start
316+ cScript := C.CString(globalProtectionScript)
317+ defer C.free(unsafe.Pointer(cScript))
318+
319+ script := C.webkit_user_script_new(
320+ (*C.gchar)(cScript),
321+ C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
322+ C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
323+ nil, nil) // nil = main world
324+
325+ if script == nil {
326+ return ErrNotImplemented
327+ }
328+
329+ C.webkit_user_content_manager_add_script(w.native.ucm, script)
330+ C.webkit_user_script_unref(script) // Clean up reference
331+
332+ log.Printf("[shortcuts] Global shortcuts protection enabled for %d shortcuts", len(shortcuts))
333+ return nil
334+}
335+
336+// DisableGlobalShortcutsProtection removes the global shortcuts protection
337+func (w *WebView) DisableGlobalShortcutsProtection() error {
338+ if w == nil || w.destroyed || w.native == nil {
339+ return ErrWebViewNotInitialized
340+ }
341+
342+ // Execute cleanup script
343+ cleanupScript := `
344+ if (window.__dumber_cleanup_global_shortcuts) {
345+ window.__dumber_cleanup_global_shortcuts();
346+ }
347+ `
348+
349+ return w.InjectScript(cleanupScript)
350+}
351+
152 // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events352 // EnablePageKeyboardBlocking injects a script into the main world to block all keyboard events
153 // This prevents page JavaScript from receiving keyboard events while omnibox is active353 // This prevents page JavaScript from receiving keyboard events while omnibox is active
154 func (w *WebView) EnablePageKeyboardBlocking() error {354 func (w *WebView) EnablePageKeyboardBlocking() error {
M pkg/webkit/webview_cgo.go +5 -1
@@ -1990,6 +1990,11 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
19901990 }
19911991 }
19921992
1993+ // Enable global shortcuts protection to prevent shortcuts from leaking to webpages
1994+ if err := w.EnableGlobalShortcutsProtection(); err != nil {
1995+ log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err)
1996+ }
1997+
19931998 // No JS fallback bridge — native UCM is active
19941999 }
19952000
@@ -2580,7 +2585,6 @@ func (w *WebView) ID() string {
25802585 return strconv.FormatUint(uint64(w.id), 10)
25812586 }
25822587
2583-
25842588 // PrefersDarkTheme returns true if GTK is configured to prefer dark theme
25852589 func PrefersDarkTheme() bool {
25862590 return C.gtk_prefers_dark() != 0
@@ -1990,6 +1990,11 @@ func (w *WebView) enableUserContentManager(cfg *Config) {
1990 }1990 }
1991 }1991 }
1992 1992
1993+ // Enable global shortcuts protection to prevent shortcuts from leaking to webpages
1994+ if err := w.EnableGlobalShortcutsProtection(); err != nil {
1995+ log.Printf("[webkit] Warning: Failed to enable global shortcuts protection: %v", err)
1996+ }
1997+
1993 // No JS fallback bridge — native UCM is active1998 // No JS fallback bridge — native UCM is active
1994 }1999 }
1995 2000
@@ -2580,7 +2585,6 @@ func (w *WebView) ID() string {
2580 return strconv.FormatUint(uint64(w.id), 10)2585 return strconv.FormatUint(uint64(w.id), 10)
2581 }2586 }
2582 2587
2583-
2584 // PrefersDarkTheme returns true if GTK is configured to prefer dark theme2588 // PrefersDarkTheme returns true if GTK is configured to prefer dark theme
2585 func PrefersDarkTheme() bool {2589 func PrefersDarkTheme() bool {
2586 return C.gtk_prefers_dark() != 02590 return C.gtk_prefers_dark() != 0
M pkg/webkit/window_shortcuts.go +3 -0
@@ -71,6 +71,9 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e
7171 return err
7272 }
7373
74+ // Window shortcuts should NOT be added to global blocking registry
75+ // They need to bubble up to GTK for proper handling
76+
7477 log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey)
7578 return nil
7679 }
@@ -71,6 +71,9 @@ func (ws *WindowShortcuts) RegisterGlobalShortcut(key string, callback func()) e
71 return err71 return err
72 }72 }
73 73
74+ // Window shortcuts should NOT be added to global blocking registry
75+ // They need to bubble up to GTK for proper handling
76+
74 log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey)77 log.Printf("[window-shortcuts] Registered global shortcut: %s -> %s", key, gtkKey)
75 return nil78 return nil
76 }79 }

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