feat(shortcuts): Add Ctrl/Cmd+1..4 to switch main views

Add keyboard shortcuts (Ctrl/Cmd + 1–4) to jump to the four main views:
Dashboard, Repositories, Pull Requests and Issues & Boards.

- Handler runs in the capture phase so editors/dialogs can still trap keys.
- Switching is blocked when the app is busy or any modal/overlay is open; repeats
  and combinations with Alt/Shift are ignored.
- When switching to the repository view the shortcut selects an existing repo
  tab (it will not open a native file dialog if no repo is available).

Also update tab button titles to surface the shortcuts and add entries to the
help overlay (English and German).
This commit is contained in:
2026-09-22 14:40:52 +02:00
parent 62e59c6e95
commit b6eb1595af
3 changed files with 68 additions and 5 deletions
+62 -1
View File
@@ -5940,6 +5940,67 @@
// ── Event handlers ───────────────────────────────────────────────────────── // ── Event handlers ─────────────────────────────────────────────────────────
// ── Main view shortcuts ────────────────────────────────────────────────────
// Ctrl/Cmd + 1…4 jump to the four main views in the order of the tab bar.
const mainViewShortcuts: AppView[] = ["management", "repository", "review-center", "issues"];
function mainViewShortcutIndex(event: KeyboardEvent): number {
if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey) return 0;
const code = event.code ?? "";
const digit = code.startsWith("Digit")
? Number(code.slice(5))
: code.startsWith("Numpad")
? Number(code.slice(6))
: Number(event.key);
if (!Number.isInteger(digit) || digit < 1 || digit > mainViewShortcuts.length) return 0;
return digit;
}
// Capture phase: dialogs and editors trap keys on their own nodes, and the
// view shortcut must not depend on the event reaching window afterwards.
function handleViewShortcutKeydown(event: KeyboardEvent) {
const index = mainViewShortcutIndex(event);
if (index === 0) return;
if (import.meta.env.DEV) {
console.debug("[gitty] view shortcut", { key: event.key, code: event.code, index, blocked: mainViewSwitchBlocked(), activeView });
}
event.preventDefault();
if (!event.repeat) switchMainView(index);
}
// Anything modal owns the keyboard, so the view stays where it is.
function mainViewSwitchBlocked(): boolean {
return isBusy
|| commandPaletteOpen || helpOpen || globalSearchOpen || appSettingsOpen
|| cloneDialogOpen || initRepositoryDialogOpen || credDialogOpen
|| compareDialogOpen || compareSelectOpen || fileHistoryDialogOpen
|| resolveDialogOpen || interactiveRebaseOpen || reflogOpen || bisectOpen
|| worktreeDialogOpen || submoduleDialogOpen || gitLfsDialogOpen
|| linePatchOpen || aiReviewOpen
|| pendingSubmoduleInitialization !== null || pendingDiscard !== null
|| newBranchCommit !== null || renameBranchTarget !== null
|| deleteBranchTarget !== null || commitNoteTarget !== null;
}
function switchMainView(index: number) {
if (mainViewSwitchBlocked()) return;
const view = mainViewShortcuts[index - 1];
if (!view || view === activeView) return;
closeRepoTabContextMenu();
if (view === "management") {
openRepoManagement();
} else if (view === "review-center") {
openReviewCenter();
} else if (view === "issues") {
activeView = "issues";
} else {
// Without an open repository the tab bar would ask for a folder; a
// keyboard shortcut should never pop up a native file dialog.
const path = repoTabs.find((tab) => sameRepoPath(tab.path, activeRepoPath))?.path ?? repoTabs[0]?.path;
if (path) void selectRepoTab(path);
}
}
function handleWindowKeydown(event: KeyboardEvent) { function handleWindowKeydown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
event.preventDefault(); event.preventDefault();
@@ -5992,7 +6053,7 @@
<title>Gitty</title> <title>Gitty</title>
</svelte:head> </svelte:head>
<svelte:window on:click={handleWindowClick} on:keydown={handleWindowKeydown} on:contextmenu|capture={handleWindowContextMenu} /> <svelte:window on:click={handleWindowClick} on:keydown|capture={handleViewShortcutKeydown} on:keydown={handleWindowKeydown} on:contextmenu|capture={handleWindowContextMenu} />
{#if sidebarSectionMenu && activeView === "repository"} {#if sidebarSectionMenu && activeView === "repository"}
<SidebarSectionMenu x={sidebarSectionMenu.x} y={sidebarSectionMenu.y} language={appLanguage} <SidebarSectionMenu x={sidebarSectionMenu.x} y={sidebarSectionMenu.y} language={appLanguage}
+4 -4
View File
@@ -89,10 +89,10 @@
<header class="workspace-navigation"> <header class="workspace-navigation">
<nav class="global-navigation" aria-label={language === "de" ? "Hauptnavigation" : "Main navigation"}> <nav class="global-navigation" aria-label={language === "de" ? "Hauptnavigation" : "Main navigation"}>
<button type="button" class:active={activeView === "management"} aria-current={activeView === "management" ? "page" : undefined} disabled={isBusy} onclick={onOpenManagement}><House size={17}/><span>Dashboard</span></button> <button type="button" class:active={activeView === "management"} aria-current={activeView === "management" ? "page" : undefined} disabled={isBusy} onclick={onOpenManagement} title="Dashboard (Ctrl + 1)"><House size={17}/><span>Dashboard</span></button>
<button type="button" class:active={activeView === "repository"} aria-current={activeView === "repository" ? "page" : undefined} disabled={isBusy} onclick={onOpenRepositories}><Database size={17}/><span>Repositories</span></button> <button type="button" class:active={activeView === "repository"} aria-current={activeView === "repository" ? "page" : undefined} disabled={isBusy} onclick={onOpenRepositories} title="Repositories (Ctrl + 2)"><Database size={17}/><span>Repositories</span></button>
<button type="button" class:active={activeView === "review-center"} aria-current={activeView === "review-center" ? "page" : undefined} disabled={isBusy} onclick={onOpenReviewCenter}><GitPullRequest size={17}/><span>Pull Requests</span></button> <button type="button" class:active={activeView === "review-center"} aria-current={activeView === "review-center" ? "page" : undefined} disabled={isBusy} onclick={onOpenReviewCenter} title="Pull Requests (Ctrl + 3)"><GitPullRequest size={17}/><span>Pull Requests</span></button>
<button type="button" class:active={activeView === "issues"} aria-current={activeView === "issues" ? "page" : undefined} disabled={isBusy} onclick={onOpenIssues}><Columns3 size={17}/><span>Issues &amp; Boards</span></button> <button type="button" class:active={activeView === "issues"} aria-current={activeView === "issues" ? "page" : undefined} disabled={isBusy} onclick={onOpenIssues} title="Issues & Boards (Ctrl + 4)"><Columns3 size={17}/><span>Issues &amp; Boards</span></button>
</nav> </nav>
{#if activeView === "repository"} {#if activeView === "repository"}
<div class="repository-row"> <div class="repository-row">
+2
View File
@@ -249,6 +249,7 @@
summary: "Die Hilfe ist überall erreichbar. Dialoge lassen sich konsistent schließen und Suchfelder direkt fokussieren.", summary: "Die Hilfe ist überall erreichbar. Dialoge lassen sich konsistent schließen und Suchfelder direkt fokussieren.",
commands: [ commands: [
{ command: "Ctrl + /", description: "Diese Hilfe öffnen" }, { command: "Ctrl + /", description: "Diese Hilfe öffnen" },
{ command: "Ctrl + 1 … 4", description: "Zwischen Dashboard, Repositories, Pull Requests und Issues & Boards wechseln" },
{ command: "Ctrl + A", description: "Alle Dateien in der aktiven Statusliste („Ungestaged“ oder „Gestaged“) auswählen" }, { command: "Ctrl + A", description: "Alle Dateien in der aktiven Statusliste („Ungestaged“ oder „Gestaged“) auswählen" },
{ command: "Escape", description: "Aktuelles Overlay oder Dialogfenster schließen in der Statusliste die aktuelle Auswahl aufheben" }, { command: "Escape", description: "Aktuelles Overlay oder Dialogfenster schließen in der Statusliste die aktuelle Auswahl aufheben" },
{ command: "Tab / Shift + Tab", description: "Zwischen Bedienelementen wechseln" }, { command: "Tab / Shift + Tab", description: "Zwischen Bedienelementen wechseln" },
@@ -460,6 +461,7 @@
summary: "Help is available everywhere. Dialogs close consistently and search fields receive focus automatically.", summary: "Help is available everywhere. Dialogs close consistently and search fields receive focus automatically.",
commands: [ commands: [
{ command: "Ctrl + /", description: "Open this help center" }, { command: "Ctrl + /", description: "Open this help center" },
{ command: "Ctrl + 1 … 4", description: "Switch between Dashboard, Repositories, Pull Requests and Issues & Boards" },
{ command: "Ctrl + A", description: "Select every file in the focused status list (Unstaged or Staged)" }, { command: "Ctrl + A", description: "Select every file in the focused status list (Unstaged or Staged)" },
{ command: "Escape", description: "Close the current overlay or dialog in the status list, clear the current selection" }, { command: "Escape", description: "Close the current overlay or dialog in the status list, clear the current selection" },
{ command: "Tab / Shift + Tab", description: "Move between controls" }, { command: "Tab / Shift + Tab", description: "Move between controls" },