Put every shortcut on one shared keymap and make the app keyboard-reachable

The keymap was scattered over ShortcutsService if-chains, the file panel's
key switch, macOS-only menu accelerators and hand-typed hints in 16
tooltips x 7 locales, and large parts of the app had no keyboard path at
all. src/shared/keymap.ts is now the one table: scopes, per-platform
defaults, exact matching, formatting with localized key names, conflicts,
reserved combos and native-menu accelerators, proven by keymap.spec.ts.

- ShortcutsService resolves against the effective keymap: claim() takes
  the key (preventDefault, held keys repeat only movement, never mid-IME),
  and from a text field only chords and F-keys fire. The file panel, App,
  editor, manager and Settings windows all dispatch through it.
- FileActionsService.run is the one file-command dispatcher for the list
  keys, the row menu, the macOS menu bar and the toolbar.
- New keys: the row menu (Shift+F10 / Menu / Ctrl+Return), the source
  picker (Mod+K), the path bar (Mod+L), transfers (Mod+J), sorting
  (Alt+1-7), Shift+movement selection, close other tabs, move and
  disconnect tabs, refresh and edit on Windows/Linux, permissions, copy
  path, reveal, trash, open in default app, editor find next/previous.
- Right-click menus: soft shortcut hints, icons on the everyday verbs
  (the toolbar's glyphs), section headers, scrolling, item keys while
  open, a highlighted first item when opened from the keyboard. The panel
  source picker now renders through the same menu.
- Dialogs: role=dialog, a Tab trap via cycleFocus (WKWebView skips
  buttons on Tab without Keyboard navigation), initial focus, Esc that
  stops propagating; the chmod dialog takes focus and applies on Enter.
- Fixed: Ctrl+Tab also flipped the active panel; Esc in a manager dialog
  closed the manager; Ctrl-F/T fired Cmd shortcuts on macOS; shortcuts
  fired under dialogs; keys died after any button click on macOS; the
  editor's save prompt and find box never got focus; WebView2 reload keys;
  a window could save default settings before its first load.
- Settings closes on its own close-window key (new settingsClose request).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-10-05 14:48:11 +02:00
co-authored by Claude
parent 0eb5608873
commit d361297690
44 changed files with 2531 additions and 700 deletions
+7
View File
@@ -467,6 +467,8 @@ export interface SettingsRpcHooks {
/** Every edit here is a settings save — rebroadcast it to the other windows. */
onSettingsSaved: (next: AppSettings) => void;
showAbout: () => void;
/** The window's own close-window key. */
close: () => void;
}
/** RPC for the single Settings window (per-window instance). */
@@ -483,6 +485,11 @@ export function createSettingsRPC(deps: { settings: SettingsStore }, hooks: Sett
hooks.showAbout();
return {};
}),
settingsClose: () =>
wrap(async () => {
hooks.close();
return {};
}),
},
},
});
+4 -3
View File
@@ -23,9 +23,9 @@ export interface SettingsWindowDeps {
/**
* The single Settings window. Unlike the connection manager it is NOT modal:
* every edit is an ordinary settings save, so the other windows follow live
* while it stays open. On macOS ⌘W reaches bun as the main window's close-tab
* menu action, so index.ts asks isFocused() first and closes this window
* instead of a main-window tab.
* while it stays open. Its page claims its own close-window key (settingsClose).
* A ⌘W it doesn't claim (remapped away) falls through to the app menu's Close
* Tab, which index.ts routes here while this window is focused.
*/
export class SettingsWindow {
private win: BrowserWindow | null = null;
@@ -49,6 +49,7 @@ export class SettingsWindow {
{
onSettingsSaved: (next) => this.deps.onSettingsSaved(next),
showAbout: () => this.deps.showAbout(),
close: () => this.close(),
},
);
+4
View File
@@ -1,5 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { sanitizeKeybindings } from "../../shared/keymap";
import { clampUiScale, DEFAULT_SETTINGS, type AppSettings } from "../../shared/types";
import { appDataDir } from "./paths";
@@ -24,6 +25,9 @@ export class SettingsStore {
// into every window's root font-size, so a hand-edited settings.json
// (uiScale: 100, "2", NaN) must not be able to render the UI unusable
settings.uiScale = clampUiScale(Number(settings.uiScale));
// same reasoning: every window resolves keys through this map
settings.keybindings = sanitizeKeybindings(data.keybindings);
settings.openFilesOnEnter = data.openFilesOnEnter === true;
return settings;
} catch {
return { ...DEFAULT_SETTINGS };