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:
+139
-26
@@ -78,9 +78,12 @@ Re-opening focuses the existing window. The webview fetches its own content
|
||||
**Close safety** (designed when electrobun window `close` was NOT cancellable —
|
||||
1.18.1 had no `allow` response like `before-quit`; 2.x adds a cancellable
|
||||
`will-close` for the titlebar close, not used yet — see gotchas):
|
||||
1. ⌘W (routed via the app menu → `editorCloseRequest`) / Close button →
|
||||
styled in-window Save/Don't Save/Cancel; the confirm path calls
|
||||
`editorClose` which sets `suppressPrompt` before `win.close()`.
|
||||
1. ⌘W (the page's own `close-window` key; a ⌘W it doesn't claim falls through
|
||||
to the app menu, which bun routes back as `editorCloseRequest`) / Close
|
||||
button → styled in-window Save/Don't Save/Cancel, which takes focus (Save;
|
||||
⌘S = Save, Esc = Cancel, Tab cycles) so keys can't edit the text behind it;
|
||||
the confirm path calls `editorClose` which sets `suppressPrompt` before
|
||||
`win.close()`.
|
||||
2. Traffic-light close → the window is already gone, but the editor mirrored
|
||||
its draft to bun (`editorState`, 400 ms debounce + flush on blur/dirty-flip)
|
||||
→ native Save/Discard box from the held draft. A failed save (dead session)
|
||||
@@ -96,7 +99,11 @@ to the main window (panels showing that directory refresh). Editor upgrades:
|
||||
scroll-synced line gutter (hidden when wrap is on — wrapped line numbers would
|
||||
need per-edit height measurement), Alt/⌥Z wrap toggle, custom find bar
|
||||
(matches computed in TS, `setSelectionRange` + scrollTop math — deliberately
|
||||
not `findInPage`, which is a shim on WebView2).
|
||||
not `findInPage`, which is a shim on WebView2). Find next / previous (⌘G /
|
||||
⇧⌘G, F3 / ⇧F3) step from the caret with the bar open or closed — the query
|
||||
survives closing it. Focus after a state change goes through
|
||||
`afterNextRender({ injector })`: under zoneless CD a microtask runs BEFORE the
|
||||
render that creates the `@if` element.
|
||||
|
||||
### Connection-manager window (`src/bun/manager-window.ts`)
|
||||
|
||||
@@ -119,11 +126,12 @@ would float over editor windows).
|
||||
Single instance, centered over main, **not modal**: every control is a plain
|
||||
`SettingsService` setter, so each edit is an ordinary save that bun
|
||||
rebroadcasts — the other windows follow live while it stays open. Opened by
|
||||
the tab-bar gear and Ctrl+, (`openSettings` from main) and, on macOS, the app
|
||||
menu's "Settings…" (⌘,, which the menu intercepts first). Its RPC serves only
|
||||
`getSettings`/`saveSettings`/`pickEditorApp`/`showAbout`. ⌘W reaches bun as
|
||||
the main window's `close-tab` menu action, so bun checks the Settings window's
|
||||
focus FIRST and closes it (see gotchas). Sidebar sections: Appearance,
|
||||
the tab-bar gear and the open-settings key (⌘, / Ctrl+,, `openSettings` from
|
||||
main) and, on macOS, the app menu's "Settings…". Its RPC serves only
|
||||
`getSettings`/`saveSettings`/`pickEditorApp`/`showAbout`/`settingsClose`. Its
|
||||
page claims its own close-window key (`settingsClose`); a ⌘W it doesn't claim
|
||||
falls through to the app menu's Close Tab, and bun checks the Settings
|
||||
window's focus FIRST and closes it (see gotchas). Sidebar sections: Appearance,
|
||||
Transfers, Editor, File list (visible columns — the column-header right-click
|
||||
menu toggles the same setting), About (opens the native About box). The
|
||||
native title is `options.title`, retitled on a locale switch.
|
||||
@@ -251,11 +259,22 @@ component; tokens → `ui/src/styles.css`.
|
||||
save too — the scale shortcuts — and the Settings window is nothing but
|
||||
saves); bun's `settingsSaved()` in index.ts runs the
|
||||
side effects once and rebroadcasts to every window **except the sender**, so
|
||||
there is no echo to fight a held Cmd+. `applyAll` never persists. New setting
|
||||
= five touches here + types.ts (backend merges automatically).
|
||||
there is no echo to fight a held Cmd+. `applyAll` never persists. Saves are
|
||||
whole-object, so **nothing persists until the first `getSettings` landed**
|
||||
(`loaded`): a ⌘+ pressed while a window opens would otherwise write the
|
||||
defaults over every remap. New setting = five touches here + types.ts
|
||||
(backend merges automatically; sanitize hand-editable shapes in
|
||||
`SettingsStore.load`).
|
||||
- `focus.service.ts` — dependency-free `tick`/`bump()`; dialogs bump it, the
|
||||
main window's panels listen. Exists so dialogs don't inject PanelsService
|
||||
(whose constructor spawns a workspace — fatal in secondary windows).
|
||||
main window's panels listen — but a panel only takes focus while NO dialog or
|
||||
menu is up (a run of overwrite questions reuses one dialog instance; focusing
|
||||
the list behind it handed Enter/Delete to the panel). Exists so dialogs don't
|
||||
inject PanelsService (whose constructor spawns a workspace — fatal in
|
||||
secondary windows).
|
||||
- `shortcuts.service.ts` — the keyboard layer over `src/shared/keymap.ts`; see
|
||||
"Keyboard" below.
|
||||
- `focus-cycle.ts` — `cycleFocus(root, event)`: Tab / ⇧Tab by hand (see
|
||||
"Keyboard").
|
||||
- `connections.service.ts` — store mirror + ssh-config hosts; reloads on the
|
||||
`connectionsChanged` push.
|
||||
- `i18n.service.ts` — runtime translator. `locale` signal + `t('ns.key', params)`
|
||||
@@ -267,20 +286,42 @@ component; tokens → `ui/src/styles.css`.
|
||||
/`speed()` so the file list reformats on switch. `SettingsService.applyLocale`
|
||||
resolves the `locale` setting (`system` → `navigator.language`) into it, on
|
||||
boot and on the `settingsChanged` broadcast (so editor/manager windows follow).
|
||||
- `file-actions.service.ts` — toolbar/keyboard/context-menu file ops, always
|
||||
against an explicit panel; `menuTarget` remembers the right-clicked row.
|
||||
- `file-actions.service.ts` — **one file-command dispatcher**,
|
||||
`run(id, { side, panel, entry })`, for every entry point: list keys (the
|
||||
cursor row), row-menu picks (the right-clicked row, captured in the menu's
|
||||
`onPick` closure), the macOS menu bar's File/View items and the panel
|
||||
toolbar. Ids are keymap `FileCommand`s (`open`, `open-default`, `send`,
|
||||
`edit`, `rename`, `new-folder`, `new-file`, `chmod`, `delete`, `trash`,
|
||||
`copy-path`, `reveal`, `refresh`, `sort-*`). Selection if any, else the
|
||||
target row; `..` stands for the folder. `open` enters folders and opens a
|
||||
LOCAL file in its default app only when `openFilesOnEnter` is set (Settings
|
||||
→ File list); `open-default` (the menu's Open on a local file) always does.
|
||||
`trash` walks the selection one `moveToTrash` at a time. Main-window only
|
||||
(it injects TransfersService).
|
||||
- `context-menu.service.ts` + `skins/shadlike/context-menu.ts` — the panel and tab right-click
|
||||
menus, in HTML. Electrobun's native `showContextMenu` is a no-op on Linux, so it was
|
||||
retired on ALL platforms rather than kept as a second code path (see
|
||||
platform-notes.md). The service builds the item list (row / empty-space menu,
|
||||
and the header column picker) and holds the open request as a signal; the
|
||||
component renders it at the pointer with edge-flip clamping, arrow/Enter/Escape
|
||||
keys, and close-on-blur/resize/scroll. Picked items are emitted verbatim as
|
||||
`ctx-*` / `col:*` / `tab:*` strings through `RpcService.emitMenuAction`, so they
|
||||
land in the SAME `App.handleMenuAction` dispatcher the macOS app menubar uses —
|
||||
one handler, two sources. The three shell actions the webview cannot perform
|
||||
(reveal, open-with-default, copy path) go back to bun via the `shellItemAction`
|
||||
RPC, which is all that remains of the old native menu.
|
||||
the header column picker, and the panel **source picker** — Local, drives,
|
||||
connections under per-stack `header` items, `~/.ssh/config` hosts — which
|
||||
replaced a bespoke popover so it gets the same keys and look) and holds the
|
||||
open request as a signal; the component renders it at a point
|
||||
(`MenuPoint`: a MouseEvent, or a row rect for a keyboard-opened menu) with
|
||||
edge-flip clamping, a scrolling clamped box, and close-on-blur/resize/scroll.
|
||||
Items carry `shortcut` (a keymap id: its first key is the soft hint at 60%
|
||||
of the row colour, and pressing it while the menu is open picks the item)
|
||||
and `icon` (drawn in the check column; only the everyday verbs get one —
|
||||
Send, Edit, Rename, Delete, New Folder, New/Close Tab, the toolbar's own
|
||||
glyphs). Keys: ↑↓ Home End move the one highlight (mouse hover shares it),
|
||||
Enter/Space pick, Esc closes, items are not Tab stops; every handled key
|
||||
stops propagation so the window handler never runs it twice. A keyboard-opened
|
||||
menu starts with its first item highlighted (`initialIndex`). A request's
|
||||
`onPick` takes the pick (row menu → `FileActionsService.run`, source picker →
|
||||
`FilePanel`); otherwise `col:*` / `tab:*` strings go through
|
||||
`RpcService.emitMenuAction` to the SAME `App.handleMenuAction` dispatcher the
|
||||
macOS app menubar uses. The three shell actions the webview cannot perform
|
||||
(reveal, open-with-default, copy path) go back to bun via the
|
||||
`shellItemAction` RPC, which is all that remains of the old native menu.
|
||||
`tabItems()` builds the workspace-tab menu (`tab:new`, and
|
||||
`tab:close|close-others|close-right|disconnect:<id>`), served by
|
||||
`PanelsService.closeOtherWorkspaces` / `closeWorkspacesToRight` /
|
||||
@@ -298,9 +339,12 @@ component; tokens → `ui/src/styles.css`.
|
||||
the build never checks them: an unregistered name renders nothing (console error
|
||||
only), so every name used anywhere must be listed here.
|
||||
- `platform.ts` — preload-global readers (`PLATFORM`, `APP_MODE`,
|
||||
`EDITOR_CONTEXT`, `IS_MAC`/`IS_MAC_CHROME`, `IS_WINDOWS`, `IS_LINUX`,
|
||||
`MOD_LABEL`/`ALT_LABEL`) + `blockNativeContextMenu` (kills WebKit's
|
||||
Reload/Inspect menu outside text fields — the app draws its own menu instead).
|
||||
`EDITOR_CONTEXT`, `IS_MAC`/`IS_MAC_CHROME`, `IS_WINDOWS`, `IS_LINUX`) +
|
||||
`blockNativeContextMenu` (kills WebKit's Reload/Inspect menu outside text
|
||||
fields — the app draws its own menu instead) + `blockReloadKeys` (swallows
|
||||
unclaimed F5 / Ctrl+R / Ctrl+F5 / Ctrl+Shift+R on Windows, where WebView2
|
||||
would reload the page and drop every session; each window root calls it
|
||||
last).
|
||||
- `updater.service.ts` — auto-updater state for the tab-bar pill. Signals
|
||||
`state` (`idle→available→downloading→ready`|`error`), `version`, `progress`.
|
||||
Subscribes the `updateStatus` push (backend `Updater.checkForUpdate` on
|
||||
@@ -310,6 +354,75 @@ component; tokens → `ui/src/styles.css`.
|
||||
`src/bun/index.ts`; feed is `release.baseUrl` (`https://miniscp.com/dl`, the
|
||||
`/dl` host in `site/`). See build-and-release.md "Auto-updater".
|
||||
|
||||
## Keyboard
|
||||
|
||||
**One keymap** — `src/shared/keymap.ts`, shared so the webviews and bun (the
|
||||
macOS menu bar) read the same table. `BINDINGS` lists every command: `id`,
|
||||
`group` (Settings sections), `scopes`, and default combos per platform
|
||||
(`mac` / `other`, best first: the first is the hint menus and tooltips show).
|
||||
Combos are platform-neutral strings (`Mod+Shift+N`: Mod = ⌘ on macOS, Ctrl
|
||||
elsewhere; `Ctrl` a literal ⌃ only macOS tells apart). Letters match the
|
||||
layout's character (AZERTY's A is A) and fall back to the physical key under
|
||||
Alt (⌥Z types Ω) and on non-Latin layouts; digits match physically (AZERTY
|
||||
types "&" on 1); punctuation by character first (AZERTY's comma is on the M
|
||||
key), physical second (⇧] makes "}"). **Matching is exact**: ⌃F is not ⌘F,
|
||||
Ctrl+Tab is not Tab, and neither the Windows key nor AltGr is ever part of a
|
||||
binding. `effectiveKeymap(overrides)` applies `AppSettings.keybindings`
|
||||
(rows that differ from the defaults; `[]` unbinds) and drops anything
|
||||
malformed. `conflicts()` / `ownerOf()` work over overlapping scopes; the
|
||||
spec (`keymap.spec.ts`) proves the defaults conflict-free, round-trippable and
|
||||
unshadowed on both platforms.
|
||||
|
||||
**Scopes.** `panel` = the focused file list (navigation, file commands,
|
||||
sorting); `main` = anywhere in the main window (tabs, window commands); `editor`
|
||||
/ `manager` / `settings` per window; `any` = every window (interface scale).
|
||||
`panel` sits inside `main`, so they conflict-check together.
|
||||
|
||||
**`ShortcutsService`** resolves keydowns against the effective keymap
|
||||
(reactive: a remap applies live in every window via the settings broadcast) and
|
||||
formats hints — `label(id)` / `labels(id)` / `tip(text, id)` /
|
||||
`tipFixed(text, 'Shift+Enter')`, with **localized key names** off macOS
|
||||
(`keys.names.*`: German says Strg+Entf). `claim()` = resolve + `preventDefault`
|
||||
(so the macOS menu never runs it twice) + auto-repeat only for movement-like
|
||||
commands (holding ⌘W closes one tab). IME composition is never a shortcut.
|
||||
From a text field only `firesInTextField()` combos count: ⌘/Ctrl and Alt chords
|
||||
and F-keys, but not plain keys, not text-editing chords (⌘⌫, ⌥←, ⌘A) and not
|
||||
Ctrl+Alt off macOS (AltGr).
|
||||
|
||||
**Dispatch** (one place per window): an open context menu owns its keys; an
|
||||
open dialog owns Enter / Esc / Tab while the main window swallows window
|
||||
commands and the panel commands the macOS menu bar carries (an unclaimed ⌘T
|
||||
would otherwise reach the menu and open a tab behind the dialog); the focused
|
||||
list (`FilePanel.onKeydown`) runs, in order, the fixed filter-editing keys
|
||||
while a query is typed, the `panel` keymap (Shift + a movement key extends the
|
||||
selection over the rows passed — a fixed rule that follows remaps), lets
|
||||
`main` matches bubble, then type-to-filter, then Esc's fixed chain (clear the
|
||||
filter → cancel a connect → clear the selection); `App` runs `main`. Every root
|
||||
ends with `blockReloadKeys`. Keys that land on `<body>` (a click dropped focus)
|
||||
are forwarded to the active list (`PanelsService.forwardListKey`), and `App`'s
|
||||
`(mousedown)` keeps focus on the list after chrome-button clicks.
|
||||
|
||||
**macOS key equivalents are page-first.** WKWebView hands ⌘-keys to the page
|
||||
before the app menu (WebKit `WebViewImpl::performKeyEquivalent`); only keys the
|
||||
page doesn't `preventDefault` reach the menu bar. So the page owns every app
|
||||
shortcut, and a window that doesn't claim a key lets it fall through to the
|
||||
menu (⌘W in a window whose close-window was remapped away still hits Close Tab,
|
||||
which bun routes to the focused Settings / editor window first).
|
||||
|
||||
**Focus without Keyboard navigation.** WKWebView's Tab skips buttons,
|
||||
checkboxes and radios unless the user turned on System Settings → Keyboard →
|
||||
Keyboard navigation, so `cycleFocus(root, event)` walks Tab / ⇧Tab by hand
|
||||
(DOM order, wrap, a radio group is one stop): every dialog (it is also their
|
||||
focus trap), the transfer list (⌘J opens it focused), the editor's save prompt.
|
||||
Dialogs carry `role="dialog" aria-modal`, take initial focus (the chmod dialog
|
||||
on its octal field, Enter applies) and stop Esc from propagating — a
|
||||
window-level Esc (the connection manager closes on it) must not see it too. A
|
||||
base-layer `:focus-visible` ring in `--ring` marks the focused control.
|
||||
|
||||
**Opening a row's menu from the keyboard**: ⇧F10 / the Menu key / ⌃↩ (macOS)
|
||||
open the cursor row's menu under the row, first item highlighted; the
|
||||
selection is kept, its commands act on it like the F-keys do.
|
||||
|
||||
## Theming
|
||||
|
||||
`ui/src/styles.css`: OKLCH tokens adapted from **Oxide's design system**
|
||||
|
||||
+54
-5
@@ -136,11 +136,18 @@ touching the adjacent code.
|
||||
update-initiated) — a plain `Utils.quit()` there quits without installing.
|
||||
`Utils.quit()` runs the same approval, which is why the guard checks
|
||||
`quitConfirmed` first.
|
||||
- **⌘W in any secondary window arrives as the MAIN window's `close-tab`
|
||||
menu action** (macOS: the app menu intercepts it before the webview sees
|
||||
the key). bun must check the focused secondary window first — the Settings
|
||||
window, then editors — and close THAT; a new window kind that skips this
|
||||
check makes ⌘W close a main-window tab behind it.
|
||||
- **macOS ⌘-keys go to the PAGE first; only unclaimed ones reach the app
|
||||
menu.** WKWebView's `performKeyEquivalent` hands key equivalents to the page
|
||||
while the webview is first responder (WebKit `WebViewImpl.mm`: "lets
|
||||
webpages have a crack at intercepting key-modified keypresses"); a key the
|
||||
page `preventDefault`s never reaches the menu. Old comments here claimed the
|
||||
opposite ("the menu intercepts ⌘W first") — what was really happening is that
|
||||
windows which didn't claim ⌘W let it fall through to the menu's Close Tab.
|
||||
Two consequences: a handled shortcut MUST `preventDefault` (`claim()` does),
|
||||
or the menu runs it a second time; and a ⌘W a secondary window doesn't claim
|
||||
still arrives as the MAIN window's `close-tab` action — bun checks the
|
||||
focused Settings window, then editors, first and closes THAT, so a new
|
||||
window kind must either claim close-window or join that check.
|
||||
- **One rpc instance per window** — BrowserView binding steals the rpc's
|
||||
transport; a shared instance silently sends to the wrong webview.
|
||||
- **RPC before socket open was silently lost on 1.18.1.** 2.x Electroview queues
|
||||
@@ -270,6 +277,44 @@ touching the adjacent code.
|
||||
`libayatana-appindicator is deprecated. Please use libayatana-appindicator-glib`
|
||||
(upstream electrobun's choice, not ours).
|
||||
|
||||
## Keyboard (`src/shared/keymap.ts`, `ui/src/app/core/shortcuts.service.ts`)
|
||||
|
||||
- **WKWebView skips buttons, checkboxes and radios on Tab** unless the user
|
||||
turned on System Settings → Keyboard → Keyboard navigation (or presses ⌥Tab).
|
||||
Dialogs, Settings and the manager were mouse-only on a default Mac. Tab is
|
||||
walked by hand: `cycleFocus(root, event)` (core/focus-cycle.ts) — use it in
|
||||
any new dialog or window.
|
||||
- **WebKit doesn't focus a clicked button — focus drops to `<body>`** (Windows
|
||||
parks it on the button, where Space clicks it again). Every list key died
|
||||
until the list was clicked. `App` `(mousedown)` keeps focus on chrome-button
|
||||
clicks and forwards `<body>` keys to the active list; keep both.
|
||||
- **Match modifiers exactly.** The old panel switch caught every Tab, so
|
||||
Ctrl+Tab flipped the active panel AND switched tabs; `isMod = metaKey ||
|
||||
ctrlKey` made ⌃F (Cocoa's forward-one-character) open the editor's find.
|
||||
`matchesEvent` compares all four modifiers, platform-exact.
|
||||
- **AltGr is Ctrl+Alt on Windows/Linux.** A Ctrl+Alt+letter binding steals
|
||||
"@", "€", "ł"… — never a default there, never fired from a text field, and
|
||||
an event with `getModifierState('AltGraph')` matches nothing.
|
||||
- **Type-to-filter takes every printable key.** A plain-letter binding (vim
|
||||
j/k) must resolve BEFORE it — and then that letter no longer starts a query
|
||||
(Settings warns). While a query is being typed, printable keys (Space too)
|
||||
always extend it.
|
||||
- **A dialog's Esc must `stopPropagation`.** The connection manager closes on
|
||||
a window-level Escape; the dialog cleared its own signal first, so the
|
||||
window handler saw "no dialog open" and closed the whole window.
|
||||
- **An unclaimed key behind a dialog reaches the macOS menu** — ⌘T opened a
|
||||
tab behind the rename prompt. The main window swallows window commands and
|
||||
the menu bar's panel commands while a dialog or menu is up.
|
||||
- **WebView2 reloads the page on unclaimed F5 / Ctrl+R** (default browser
|
||||
accelerators), dropping every session and the editor's unsaved text. Every
|
||||
window root ends with `blockReloadKeys`; a blanket `stopPropagation` (the old
|
||||
path bar) hides keys from it — don't add one.
|
||||
- **Hold-to-repeat** fired ⌘W once per repeat — every tab closed. `claim()`
|
||||
repeats only movement-like commands (`isRepeatable`).
|
||||
- **Focus after a state change needs `afterNextRender({ injector })`.** Under
|
||||
zoneless CD `queueMicrotask` runs before the render that creates an `@if`
|
||||
element (the editor's find box and textarea), so `.focus()` hit nothing.
|
||||
|
||||
## Domain logic
|
||||
|
||||
- **sessionId ≠ filesystem identity.** Two panels on the same server are two
|
||||
@@ -287,6 +332,10 @@ touching the adjacent code.
|
||||
- Settings: the backend spread-merges (`{...DEFAULT_SETTINGS, ...data}`) so
|
||||
new keys auto-default, but the UI SettingsService reads field-by-field —
|
||||
a new setting silently stays at default until wired in all five spots.
|
||||
- **Saves are whole-object: a window must not save before its first
|
||||
`getSettings` landed.** A ⌘+ pressed while an editor opened wrote
|
||||
`DEFAULT_SETTINGS` (no keyboard remaps) over the user's file.
|
||||
`SettingsService.persist()` is a no-op until `applyAll` ran once.
|
||||
- **Every window's RPC must serve `saveSettings`.** The scale shortcuts
|
||||
shipped in editor and manager windows while only the main RPC served it:
|
||||
the request went unanswered, the change applied locally, and main's next
|
||||
|
||||
@@ -17,7 +17,11 @@ Every platform seam carries a `PORTABILITY:` comment at the branch site.
|
||||
| Drive roots | `fs/local.ts listLocalDrives` | empty | `fsutil fsinfo drives` | empty |
|
||||
| chmod/Permissions | context-menu.service.ts, file-panel, file-actions | full | hidden for LOCAL panels (only flips read-only); SFTP unaffected | full |
|
||||
| Shell wording | `ui context-menu.service.ts` | Reveal in Finder / Move to Trash | Show in Explorer / Move to Recycle Bin | Show in File Manager |
|
||||
| Shortcut labels | `ui platform.ts MOD_LABEL/ALT_LABEL` | ⌘ / ⌥ | Ctrl+ / Alt+ | Ctrl+ / Alt+ |
|
||||
| Shortcut labels | `src/shared/keymap.ts formatCombo` (+ `keys.names.*`) | ⌘⇧⌥⌃ glyphs: ⇧⌘N | Ctrl+Shift+N, localized key names (Strg+Entf) | as Windows |
|
||||
| Command modifier | `keymap.ts` `Mod` | ⌘ only (⌃ stays Cocoa's text keys) | Ctrl | Ctrl |
|
||||
| Key equivalents | `shortcuts.service.ts claim` | page first, unclaimed → app menu | page only (no app menu) | page only |
|
||||
| Tab focus | `core/focus-cycle.ts cycleFocus` | WKWebView skips buttons/checkboxes without Keyboard navigation → walked by hand | native order (same walk) | native order (same walk) |
|
||||
| Reload keys | `platform.ts blockReloadKeys` | — | WebView2 reloads on F5 / Ctrl+R → swallowed when unclaimed | — |
|
||||
| Tray | `src/bun/tray.ts` | 22 pt template PNG | 16 px colored PNG | colored (needs AppIndicator, see below) |
|
||||
| App menu | `src/bun/menu.ts` | full menu incl. About | none (early return) — Settings opens from the tab-bar gear (Ctrl+,), About from its About section | none (unsupported upstream) |
|
||||
| connections.json protection | `store/connections.ts` | chmod 0600 | `icacls` current-user ACL (0600 is meaningless) | 0600 |
|
||||
|
||||
Reference in New Issue
Block a user