detached a39a329

feat(omnibox): add Ctrl+Number quick navigation

Implements keyboard shortcuts for instant navigation to the first 10 results:
- Ctrl+1-9: Navigate to results 1-9
- Ctrl+0: Navigate to 10th result
- Works in both History and Favorites views
- Uses event.code for keyboard layout independence (QWERTY, AZERTY, QWERTZ)
- Visual number hints appear on the right side of each item
- Hints highlighted with accent color on selection/hover
commit
a39a329e34d5c3fac458c1080e979f56c645813f
author
bnema · · 2025-11-16 18:10 UTC
committer
bnema · · 2025-11-16 20:27 UTC
parent
c99d69f
signature
unsigned
2 files · +71 -0
M gui/src/components/omnibox/OmniboxInput.svelte +39 -0
@@ -185,6 +185,27 @@
185185 break;
186186
187187 default:
188+ // Handle Ctrl+1 through Ctrl+0 for quick navigation to first 10 results
189+ // Use event.code for keyboard layout independence (QWERTY, AZERTY, QWERTZ, etc.)
190+ if (event.ctrlKey && mode === 'omnibox') {
191+ const code = event.code;
192+ let targetIndex = -1;
193+
194+ // Map physical keys: Digit1->0, Digit2->1, ..., Digit9->8, Digit0->9
195+ if (code >= 'Digit1' && code <= 'Digit9') {
196+ targetIndex = parseInt(code.substring(5), 10) - 1;
197+ } else if (code === 'Digit0') {
198+ targetIndex = 9;
199+ }
200+
201+ if (targetIndex >= 0) {
202+ event.preventDefault();
203+ event.stopPropagation();
204+ handleNumberKey(targetIndex);
205+ return;
206+ }
207+ }
208+
188209 // For normal typing keys, only stop propagation to prevent page handlers
189210 // but don't prevent default so typing still works in the input
190211 event.stopPropagation();
@@ -245,6 +266,24 @@
245266 }, 100);
246267 }
247268
269+ // Handle Ctrl+Number key for quick navigation
270+ function handleNumberKey(targetIndex: number) {
271+ if (mode !== 'omnibox') return;
272+
273+ // Get the current list based on view mode
274+ const currentList = viewMode === 'history' ? suggestions : favorites;
275+
276+ // Check if the target index is valid
277+ if (targetIndex >= currentList.length) return;
278+
279+ const item = currentList[targetIndex];
280+ if (!item || !item.url) return;
281+
282+ // Navigate to the item and close omnibox
283+ omniboxBridge.navigate(item.url);
284+ omniboxStore.close();
285+ }
286+
248287 // Handle arrow key navigation
249288 function handleArrowKeys(key: string) {
250289 let totalItems: number;
@@ -185,6 +185,27 @@
185 break;185 break;
186 186
187 default:187 default:
188+ // Handle Ctrl+1 through Ctrl+0 for quick navigation to first 10 results
189+ // Use event.code for keyboard layout independence (QWERTY, AZERTY, QWERTZ, etc.)
190+ if (event.ctrlKey && mode === 'omnibox') {
191+ const code = event.code;
192+ let targetIndex = -1;
193+
194+ // Map physical keys: Digit1->0, Digit2->1, ..., Digit9->8, Digit0->9
195+ if (code >= 'Digit1' && code <= 'Digit9') {
196+ targetIndex = parseInt(code.substring(5), 10) - 1;
197+ } else if (code === 'Digit0') {
198+ targetIndex = 9;
199+ }
200+
201+ if (targetIndex >= 0) {
202+ event.preventDefault();
203+ event.stopPropagation();
204+ handleNumberKey(targetIndex);
205+ return;
206+ }
207+ }
208+
188 // For normal typing keys, only stop propagation to prevent page handlers209 // For normal typing keys, only stop propagation to prevent page handlers
189 // but don't prevent default so typing still works in the input210 // but don't prevent default so typing still works in the input
190 event.stopPropagation();211 event.stopPropagation();
@@ -245,6 +266,24 @@
245 }, 100);266 }, 100);
246 }267 }
247 268
269+ // Handle Ctrl+Number key for quick navigation
270+ function handleNumberKey(targetIndex: number) {
271+ if (mode !== 'omnibox') return;
272+
273+ // Get the current list based on view mode
274+ const currentList = viewMode === 'history' ? suggestions : favorites;
275+
276+ // Check if the target index is valid
277+ if (targetIndex >= currentList.length) return;
278+
279+ const item = currentList[targetIndex];
280+ if (!item || !item.url) return;
281+
282+ // Navigate to the item and close omnibox
283+ omniboxBridge.navigate(item.url);
284+ omniboxStore.close();
285+ }
286+
248 // Handle arrow key navigation287 // Handle arrow key navigation
249 function handleArrowKeys(key: string) {288 function handleArrowKeys(key: string) {
250 let totalItems: number;289 let totalItems: number;
M gui/src/components/omnibox/OmniboxSuggestions.svelte +32 -0
@@ -152,6 +152,7 @@
152152 {@const isSelected = index === selectedIndex}
153153 {@const favicon = item.favicon || item.favicon_url || ''}
154154 {@const isFav = viewMode === 'history' && isFavorited(item.url)}
155+ {@const shortcutNumber = index < 9 ? index + 1 : index === 9 ? 0 : null}
155156
156157 <div
157158 id="omnibox-item-{index}"
@@ -195,6 +196,13 @@
195196 {path || '/'}
196197 </span>
197198 </div>
199+
200+ <!-- Keyboard shortcut hint (Ctrl+1-9, Ctrl+0 for 10th) -->
201+ {#if shortcutNumber !== null}
202+ <div class="suggestion-shortcut-hint" aria-hidden="true">
203+ {shortcutNumber}
204+ </div>
205+ {/if}
198206 </div>
199207 {/each}
200208 {/if}
@@ -361,4 +369,28 @@
361369 mask-image: linear-gradient(90deg, black 85%, transparent 100%);
362370 -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%);
363371 }
372+
373+ .suggestion-shortcut-hint {
374+ position: absolute;
375+ right: 0.85rem;
376+ top: 50%;
377+ transform: translateY(-50%);
378+ font-size: 0.75rem;
379+ font-weight: 600;
380+ color: var(--dynamic-muted);
381+ opacity: 0.6;
382+ background: var(--dynamic-bg);
383+ padding: 0.25rem 0.5rem;
384+ border-radius: 3px;
385+ border: 1px solid var(--dynamic-border);
386+ z-index: 10;
387+ pointer-events: none;
388+ transition: opacity 100ms ease, color 100ms ease;
389+ }
390+
391+ .suggestion-item.selected .suggestion-shortcut-hint,
392+ .suggestion-item:hover .suggestion-shortcut-hint {
393+ opacity: 0.9;
394+ color: var(--dynamic-accent);
395+ }
364396 </style>
@@ -152,6 +152,7 @@
152 {@const isSelected = index === selectedIndex}152 {@const isSelected = index === selectedIndex}
153 {@const favicon = item.favicon || item.favicon_url || ''}153 {@const favicon = item.favicon || item.favicon_url || ''}
154 {@const isFav = viewMode === 'history' && isFavorited(item.url)}154 {@const isFav = viewMode === 'history' && isFavorited(item.url)}
155+ {@const shortcutNumber = index < 9 ? index + 1 : index === 9 ? 0 : null}
155 156
156 <div157 <div
157 id="omnibox-item-{index}"158 id="omnibox-item-{index}"
@@ -195,6 +196,13 @@
195 {path || '/'}196 {path || '/'}
196 </span>197 </span>
197 </div>198 </div>
199+
200+ <!-- Keyboard shortcut hint (Ctrl+1-9, Ctrl+0 for 10th) -->
201+ {#if shortcutNumber !== null}
202+ <div class="suggestion-shortcut-hint" aria-hidden="true">
203+ {shortcutNumber}
204+ </div>
205+ {/if}
198 </div>206 </div>
199 {/each}207 {/each}
200 {/if}208 {/if}
@@ -361,4 +369,28 @@
361 mask-image: linear-gradient(90deg, black 85%, transparent 100%);369 mask-image: linear-gradient(90deg, black 85%, transparent 100%);
362 -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%);370 -webkit-mask-image: linear-gradient(90deg, black 85%, transparent 100%);
363 }371 }
372+
373+ .suggestion-shortcut-hint {
374+ position: absolute;
375+ right: 0.85rem;
376+ top: 50%;
377+ transform: translateY(-50%);
378+ font-size: 0.75rem;
379+ font-weight: 600;
380+ color: var(--dynamic-muted);
381+ opacity: 0.6;
382+ background: var(--dynamic-bg);
383+ padding: 0.25rem 0.5rem;
384+ border-radius: 3px;
385+ border: 1px solid var(--dynamic-border);
386+ z-index: 10;
387+ pointer-events: none;
388+ transition: opacity 100ms ease, color 100ms ease;
389+ }
390+
391+ .suggestion-item.selected .suggestion-shortcut-hint,
392+ .suggestion-item:hover .suggestion-shortcut-hint {
393+ opacity: 0.9;
394+ color: var(--dynamic-accent);
395+ }
364 </style>396 </style>

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