Remap any shortcut in Settings → Keyboard; the macOS menu bar follows
A Keyboard section in the Settings window edits the shared keymap: rows by group with a search field, a chip per key (x removes), + to record, reset per row and Reset all. Recording takes the keydown before every other key in the window (Cmd+W records instead of closing), refuses reserved combos without preventDefault (Cmd+Q still quits), requires a chord or F-key for editor commands, and offers Replace when another command in an overlapping scope owns the key. Rows flag conflicts and plain character keys that would stop starting a type-to-filter query. Fixed keys are listed read-only, in the platform's spelling. Remaps are ordinary settings (only rows that differ from the defaults), so every window, the menu hints and the tooltips follow live — and the macOS menu bar: its accelerators are built from the keymap (only the combos Electrobun's parser expresses faithfully) and rebuilt on a remap. A main-window menu action fired from the editor or Settings window brings the main window forward first. Also: the "Enter / double-click on a file" setting under File list (do nothing / open local files in their default app), arrow keys in the Settings sidebar with focus on open and a hand-walked Tab, translations for all seven languages (the spec now checks every language labels every command), and a README shortcut table generated from the keymap. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -35,13 +35,15 @@ Dense reference material lives in [docs/](docs/): [architecture](docs/architectu
|
||||
a standard titlebar.
|
||||
- **Menu-bar (tray) icon**: template icon that adapts to light/dark; click it
|
||||
to bring the app to the front.
|
||||
- **Edit files** (F4 / toolbar / File menu): a built-in editor works on local
|
||||
- **Edit files** (F4 / ⌘E / toolbar / File menu): a built-in editor works on local
|
||||
and remote files directly; or pick an external app (Settings → Editor →
|
||||
Custom → Choose…). External edits of remote files download to a temp folder,
|
||||
are watched, and every save auto-uploads (visible in the transfer list) until
|
||||
the session disconnects. Leftover temp files from a crash are offered for
|
||||
cleanup at the next start.
|
||||
- Browse, sort (name/size/modified, folders first), keyboard navigation.
|
||||
- Browse, sort (name/size/modified, folders first), full keyboard control
|
||||
with remappable shortcuts (see below). Enter / double-click on a local file
|
||||
can open it in its default app (Settings → File list; off by default).
|
||||
- Create folder / file, rename, delete (recursive), change permissions (chmod).
|
||||
- Upload, download, and remote→remote send — files **and** whole folders —
|
||||
with a transfer queue, per-job progress, and cancel.
|
||||
@@ -178,14 +180,102 @@ Saved connections live in a plain JSON file in the OS app-data directory
|
||||
|
||||
## Keyboard shortcuts
|
||||
|
||||
`Tab` switch panel · `↑`/`↓`/`PgUp`/`PgDn`/`Home`/`End` move cursor ·
|
||||
`Space` select · `Enter` open folder · `Backspace` parent ·
|
||||
`Ctrl/Cmd+A` select all · `F5` send to other panel · `F2` rename ·
|
||||
`F7` new folder · `F8`/`Delete` delete · `Cmd/Ctrl+T` new tab ·
|
||||
`Cmd/Ctrl+W` close tab · `Ctrl+Tab` next tab · `Cmd/Ctrl+,` settings ·
|
||||
`Cmd/Ctrl+=`/`-`/`0` interface bigger / smaller / 100%.
|
||||
The whole app works from the keyboard, and **every command can be remapped** in
|
||||
**Settings → Keyboard**: press **+** on a row and type the keys, see conflicts,
|
||||
reset one row or all. Remaps apply live in every window, the right-click menus'
|
||||
hints and the toolbar tooltips follow, and so does the macOS menu bar. The
|
||||
defaults:
|
||||
|
||||
(On Macs, function keys may need `fn` — every action also has a toolbar button.)
|
||||
| Command | macOS | Windows / Linux |
|
||||
| --- | --- | --- |
|
||||
| **File list** | | |
|
||||
| Move up | `↑` | `↑` |
|
||||
| Move down | `↓` | `↓` |
|
||||
| Page up | `PgUp` | `PgUp` |
|
||||
| Page down | `PgDn` | `PgDn` |
|
||||
| First item | `Home` | `Home` |
|
||||
| Last item | `End` | `End` |
|
||||
| Select / deselect | `Space` | `Space` `Ins` |
|
||||
| Select all | `⌘A` | `Ctrl+A` |
|
||||
| Open | `↩` `⌘↓` | `Enter` |
|
||||
| Parent folder | `⌫` `⌘↑` | `Backspace` `Alt+↑` |
|
||||
| Switch panel | `⇥` `⇧⇥` | `Tab` `Shift+Tab` |
|
||||
| Show the item's menu | `⇧F10` `⌃↩` | `Shift+F10` `Menu` |
|
||||
| **Files** | | |
|
||||
| Send to the other panel | `F5` | `F5` |
|
||||
| Edit | `⌘E` `F4` | `F4` `Ctrl+E` |
|
||||
| Rename | `F2` | `F2` |
|
||||
| New folder | `⇧⌘N` `F7` | `F7` `Ctrl+Shift+N` |
|
||||
| New file | `⌘N` `⇧F4` | `Shift+F4` `Ctrl+N` |
|
||||
| Permissions | `⌘I` | `Ctrl+I` |
|
||||
| Delete | `F8` `⌦` | `F8` `Del` |
|
||||
| Move to Trash | `⌘⌫` | `Ctrl+Del` |
|
||||
| Copy path | `⌥⌘C` | `Ctrl+Shift+C` |
|
||||
| Reveal in file manager | `⌥⌘R` | `Alt+Shift+R` |
|
||||
| Open in default app | `⌘O` | `Ctrl+O` |
|
||||
| Refresh | `⌘R` | `Ctrl+R` |
|
||||
| Go to path | `⌘L` | `Ctrl+L` |
|
||||
| Choose what the panel shows | `⌘K` | `Ctrl+K` |
|
||||
| **Sorting** | | |
|
||||
| Sort by name | `⌥1` | `Alt+1` |
|
||||
| Sort by size | `⌥2` | `Alt+2` |
|
||||
| Sort by modified | `⌥3` | `Alt+3` |
|
||||
| Sort by kind | `⌥4` | `Alt+4` |
|
||||
| Sort by rights | `⌥5` | `Alt+5` |
|
||||
| Sort by owner | `⌥6` | `Alt+6` |
|
||||
| Sort by group | `⌥7` | `Alt+7` |
|
||||
| **Tabs** | | |
|
||||
| New tab | `⌘T` | `Ctrl+T` |
|
||||
| Close tab | `⌘W` | `Ctrl+W` |
|
||||
| Close other tabs | `⌥⌘W` | `Ctrl+Shift+W` |
|
||||
| Close tabs to the right | — | — |
|
||||
| Next tab | `⌃⇥` `⇧⌘]` | `Ctrl+Tab` `Ctrl+PgDn` |
|
||||
| Previous tab | `⌃⇧⇥` `⇧⌘[` | `Ctrl+Shift+Tab` `Ctrl+PgUp` |
|
||||
| Go to tab 1 | `⌘1` | `Ctrl+1` |
|
||||
| Go to tab 2 | `⌘2` | `Ctrl+2` |
|
||||
| Go to tab 3 | `⌘3` | `Ctrl+3` |
|
||||
| Go to tab 4 | `⌘4` | `Ctrl+4` |
|
||||
| Go to tab 5 | `⌘5` | `Ctrl+5` |
|
||||
| Go to tab 6 | `⌘6` | `Ctrl+6` |
|
||||
| Go to tab 7 | `⌘7` | `Ctrl+7` |
|
||||
| Go to tab 8 | `⌘8` | `Ctrl+8` |
|
||||
| Go to last tab | `⌘9` | `Ctrl+9` |
|
||||
| Move tab left | `⌃⇧PgUp` | `Ctrl+Shift+PgUp` |
|
||||
| Move tab right | `⌃⇧PgDn` | `Ctrl+Shift+PgDn` |
|
||||
| Disconnect tab | `⌘D` | `Ctrl+D` |
|
||||
| **Window** | | |
|
||||
| Settings | `⌘,` | `Ctrl+,` |
|
||||
| Connection manager | `⇧⌘K` | `Ctrl+Shift+K` |
|
||||
| Show / hide transfers | `⌘J` | `Ctrl+J` |
|
||||
| Install update | — | — |
|
||||
| Bigger interface | `⌘=` `⇧⌘=` `⌘Num +` | `Ctrl+=` `Ctrl+Shift+=` `Ctrl+Num +` |
|
||||
| Smaller interface | `⌘-` | `Ctrl+-` |
|
||||
| Interface at 100% | `⌘0` | `Ctrl+0` |
|
||||
| Close window | `⌘W` | `Ctrl+W` |
|
||||
| **Editor** | | |
|
||||
| Save | `⌘S` | `Ctrl+S` |
|
||||
| Find | `⌘F` | `Ctrl+F` |
|
||||
| Find next | `⌘G` | `F3` |
|
||||
| Find previous | `⇧⌘G` | `Shift+F3` |
|
||||
| Wrap lines | `⌥Z` | `Alt+Z` |
|
||||
| **Connection manager** | | |
|
||||
| New connection | `⌘N` | `Ctrl+N` |
|
||||
| New stack | `⇧⌘N` | `Ctrl+Shift+N` |
|
||||
| Save connection | `⌘S` | `Ctrl+S` |
|
||||
| Connect / open stack | `⌘↩` | `Ctrl+Enter` |
|
||||
| Delete connection or stack | `⌘⌫` | `Del` |
|
||||
| Rename stack | `F2` | `F2` |
|
||||
| Import from WinSCP | `⌘O` | `Ctrl+O` |
|
||||
|
||||
**Fixed keys** (not remappable): Esc closes menus, dialogs and the find bar —
|
||||
in the file list it clears the filter, then cancels a connect, then clears the
|
||||
selection. Enter / Esc confirm / cancel dialogs and Tab walks their controls
|
||||
(on macOS too, without the system's Keyboard navigation setting). Typing in the
|
||||
file list filters it (Backspace trims); Shift + any movement key selects the
|
||||
rows the cursor passes. ↑ ↓ Home End Enter work in every menu.
|
||||
|
||||
(On Macs, function keys may need `fn` — every F-key command also has a ⌘ key
|
||||
or a toolbar button.)
|
||||
|
||||
## Same-file protection
|
||||
|
||||
|
||||
+35
-7
@@ -131,10 +131,27 @@ 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.
|
||||
window's focus FIRST and closes it (see gotchas). Sidebar sections (↑ / ↓ move
|
||||
between them, focus starts on the current one, Tab is walked by `cycleFocus`):
|
||||
Appearance, Transfers, Editor, File list (visible columns — the column-header
|
||||
right-click menu toggles the same setting — and "Enter / double-click on a
|
||||
file": nothing | open local files in their default app), Keyboard, About
|
||||
(opens the native About box). The native title is `options.title`, retitled on
|
||||
a locale switch.
|
||||
|
||||
**Keyboard** is the remap editor over the shared keymap: rows by group with a
|
||||
search field (label or key text), a chip per combo (× removes), **+** records
|
||||
(the next non-modifier keydown, taken before every other key in the window so
|
||||
⌘W records instead of closing; Esc cancels; a click elsewhere or a blur ends
|
||||
it), ↺ resets a row, Reset all (inline confirm). Reserved combos are refused
|
||||
without `preventDefault` (⌘Q still quits); editor-scope combos must be chords
|
||||
or F-keys (the editor's text area always has focus); a combo owned by an
|
||||
overlapping-scope command offers **Replace** (moves it). Rows flag conflicts
|
||||
and plain character keys on file-list rows (they no longer start a filter).
|
||||
Fixed keys are listed read-only below. Every edit is `ShortcutsService.rebind`
|
||||
→ `settings.keybindings` (only rows that differ from the defaults) → the
|
||||
ordinary save + broadcast, so menus, tooltips and the macOS menu bar follow
|
||||
live.
|
||||
|
||||
## Bun modules
|
||||
|
||||
@@ -185,9 +202,16 @@ native title is `options.title`, retitled on a locale switch.
|
||||
atomic tmp+rename, 0600 + win32 `icacls` ACL. `store/settings.ts` — defaults
|
||||
spread-merge on load, so NEW SETTINGS KEYS need no backend change.
|
||||
- `menu.ts` — macOS-only app menu, rebuilt on every connections change (the
|
||||
Connect submenu mirrors the store; "Settings…" carries ⌘,). ⌘W is intercepted
|
||||
by the menu, so bun routes `close-tab` to a focused Settings or editor window
|
||||
first. Labels go through
|
||||
Connect submenu mirrors the store), language switch and keyboard remap.
|
||||
**Accelerators come from the shared keymap** (`effectiveKeymap` +
|
||||
`toAccelerator`, the first combo the native parser can express faithfully:
|
||||
⌘/⌃ chords only — a bare key means ⌘+key to it — no ⌫ / ⌦ / Shift+punctuation);
|
||||
⌘Q stays fixed. The keybindings are read through a getter AFTER the
|
||||
connections load, so a remap saved meanwhile isn't lost. The page claims the
|
||||
keys it handles first, so the menu only runs what falls through; a
|
||||
`close-tab` that still arrives while a Settings or editor window is focused
|
||||
closes THAT window, and other main-targeted actions bring the main window
|
||||
forward first. Labels go through
|
||||
backend `i18n.t` and the menu is rebuilt on a locale change too.
|
||||
- `i18n.ts` — backend translator (native menus + thrown error messages, which
|
||||
no Angular service can reach). `setLocale(setting)` caches the resolved
|
||||
@@ -402,6 +426,10 @@ 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.
|
||||
|
||||
**Remaps** live in `AppSettings.keybindings` (sanitized on load), edited in
|
||||
Settings → Keyboard (see the Settings window). `ShortcutsService.rebind`
|
||||
stores only rows that differ from their defaults.
|
||||
|
||||
**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
|
||||
|
||||
@@ -311,6 +311,14 @@ touching the adjacent code.
|
||||
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`).
|
||||
- **Electrobun's native menu accelerator turns a combo with no modifier token
|
||||
into ⌘+key** ("f7" shows and fires ⌘F7), its `backspace` / `delete` tokens
|
||||
are characters AppKit matches unreliably, and AppKit matches Shift+punctuation
|
||||
by the shifted character ("}" for ⇧]). `toAccelerator` returns null for all of
|
||||
those; the menu item then shows no key and the page still handles it.
|
||||
- **Recording a key must take the keydown before every window handler** — and
|
||||
must NOT take reserved ones: preventDefault on ⌘Q while recording would make
|
||||
the app unquittable from the keyboard.
|
||||
- **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.
|
||||
|
||||
@@ -23,7 +23,7 @@ Every platform seam carries a `PORTABILITY:` comment at the branch site.
|
||||
| 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) |
|
||||
| App menu | `src/bun/menu.ts` | full menu incl. About; accelerators follow the keymap (remaps included), ⌘Q fixed | 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 |
|
||||
| App data dir | `store/paths.ts` | ~/Library/Application Support | %APPDATA% | $XDG_CONFIG_HOME/.config |
|
||||
| ssh-agent | `fs/sftp.ts` | $SSH_AUTH_SOCK | `\\.\pipe\openssh-ssh-agent` fallback | $SSH_AUTH_SOCK |
|
||||
|
||||
+15
-7
@@ -108,7 +108,7 @@ const manager = new ManagerWindow({
|
||||
connectionsChanged: () => rpc.send.connectionsChanged({}),
|
||||
menuAction: (info) => rpc.send.menuAction(info),
|
||||
},
|
||||
rebuildMenu: () => void applyApplicationMenu(store),
|
||||
rebuildMenu: () => void applyApplicationMenu(store, () => currentSettings.keybindings),
|
||||
onSettingsSaved: (s) => settingsSaved(s, "manager"),
|
||||
});
|
||||
const settingsWin = new SettingsWindow({
|
||||
@@ -134,12 +134,14 @@ function settingsSaved(
|
||||
): void {
|
||||
engine.setConcurrency(effectiveConcurrency(s));
|
||||
const localeChanged = s.locale !== currentSettings.locale;
|
||||
const keysChanged = JSON.stringify(s.keybindings) !== JSON.stringify(currentSettings.keybindings);
|
||||
currentSettings = s; // future windows' preloads bake the fresh theme
|
||||
if (localeChanged) {
|
||||
setBackendLocale(s.locale);
|
||||
void applyApplicationMenu(store); // relabel the native menu in the new language
|
||||
settingsWin.retitle();
|
||||
}
|
||||
// relabel the native menu in the new language / give it the remapped keys
|
||||
if (localeChanged || keysChanged) void applyApplicationMenu(store, () => currentSettings.keybindings);
|
||||
if (sender !== "main") rpc.send.settingsChanged({ settings: s });
|
||||
if (sender !== "manager") manager.broadcastSettings(s);
|
||||
if (sender !== "settings") settingsWin.broadcastSettings(s);
|
||||
@@ -258,16 +260,19 @@ events.sessionClosed = (info) => {
|
||||
rpc.send.sessionClosed(info);
|
||||
editors.notifySessionClosed(info);
|
||||
};
|
||||
events.connectionsChanged = () => void applyApplicationMenu(store);
|
||||
events.connectionsChanged = () => void applyApplicationMenu(store, () => currentSettings.keybindings);
|
||||
engine.setConcurrency(effectiveConcurrency(currentSettings));
|
||||
|
||||
void applyApplicationMenu(store);
|
||||
void applyApplicationMenu(store, () => currentSettings.keybindings);
|
||||
ApplicationMenu.on("application-menu-clicked", (event) => {
|
||||
const action = (event as { data?: { action?: string } }).data?.action;
|
||||
if (!action) return;
|
||||
// ⌘W while the Settings window or an editor window is focused closes THAT
|
||||
// window (an editor with its dirty prompt), not a main-window tab. Other
|
||||
// menu actions deliberately still target the main window.
|
||||
// Their pages claim close-window (⌘W) themselves — macOS hands key
|
||||
// equivalents to the page first — so a close-tab that still arrives while
|
||||
// the Settings window or an editor is focused is a ⌘W the page let through
|
||||
// (or the menu item clicked): close THAT window (an editor with its dirty
|
||||
// prompt), never a main-window tab behind it. Other menu actions
|
||||
// deliberately still target the main window.
|
||||
if (action === "close-tab") {
|
||||
if (settingsWin.isFocused()) {
|
||||
settingsWin.close();
|
||||
@@ -299,6 +304,9 @@ ApplicationMenu.on("application-menu-clicked", (event) => {
|
||||
}
|
||||
// the manager is modal to the main window — don't drive main behind it
|
||||
if (manager.isOpen()) return;
|
||||
// everything below acts on the main window: from a focused editor or the
|
||||
// Settings window (⌘T, ⌘N…), bring it forward so the effect is visible
|
||||
if (settingsWin.isFocused() || editors.focusedKey()) win.activate();
|
||||
rpc.send.menuAction({ action });
|
||||
});
|
||||
|
||||
|
||||
+33
-11
@@ -1,5 +1,6 @@
|
||||
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/main";
|
||||
import { BRANDING } from "../shared/branding";
|
||||
import { effectiveKeymap, type ShortcutId, toAccelerator } from "../shared/keymap";
|
||||
import type { SavedConnection } from "../shared/types";
|
||||
import { t } from "./i18n";
|
||||
import type { ConnectionStore } from "./store/connections";
|
||||
@@ -7,13 +8,30 @@ import type { ConnectionStore } from "./store/connections";
|
||||
/**
|
||||
* macOS application menu (electrobun menus are macOS-only). Standard roles plus
|
||||
* app actions; custom actions are forwarded to the webview as menuAction
|
||||
* messages. Rebuilt whenever saved connections change so the Connect submenu
|
||||
* stays current.
|
||||
* messages. Rebuilt whenever saved connections, the language or the keyboard
|
||||
* remaps change.
|
||||
*
|
||||
* Accelerators come from the shared keymap, so the menu bar shows — and
|
||||
* fires — the user's keys. The page sees ⌘-keys first and claims the ones it
|
||||
* handles; the menu only runs what falls through (see shortcuts.service.ts).
|
||||
* ⌘Q stays fixed: quitting must always work.
|
||||
*/
|
||||
export async function applyApplicationMenu(store: ConnectionStore): Promise<void> {
|
||||
export async function applyApplicationMenu(
|
||||
store: ConnectionStore,
|
||||
keybindings: () => Record<string, string[]>,
|
||||
): Promise<void> {
|
||||
if (process.platform !== "darwin") return;
|
||||
|
||||
const { stacks, connections } = await store.load();
|
||||
// read after the await: a remap saved meanwhile must not be lost
|
||||
const keymap = effectiveKeymap(keybindings(), true);
|
||||
const accelerator = (id: ShortcutId): string | undefined => {
|
||||
for (const combo of keymap.find((b) => b.id === id)?.combos ?? []) {
|
||||
const native = toAccelerator(combo);
|
||||
if (native) return native;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const item = (c: SavedConnection): ApplicationMenuItemConfig => ({
|
||||
label: c.name,
|
||||
action: `connect:${c.id}`,
|
||||
@@ -28,7 +46,11 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
|
||||
...looseItems,
|
||||
...stackMenus,
|
||||
...(looseItems.length || stackMenus.length ? [{ type: "separator" } as const] : []),
|
||||
{ label: t("menu.connectionManager"), action: "manage-connections" },
|
||||
{
|
||||
label: t("menu.connectionManager"),
|
||||
action: "manage-connections",
|
||||
accelerator: accelerator("connection-manager"),
|
||||
},
|
||||
];
|
||||
|
||||
ApplicationMenu.setApplicationMenu([
|
||||
@@ -36,7 +58,7 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
|
||||
submenu: [
|
||||
{ label: t("menu.about", { app: BRANDING.appName }), action: "about" },
|
||||
{ type: "separator" },
|
||||
{ label: t("menu.settings"), action: "settings", accelerator: "," },
|
||||
{ label: t("menu.settings"), action: "settings", accelerator: accelerator("open-settings") },
|
||||
{ type: "separator" },
|
||||
{ label: t("menu.checkForUpdates"), action: "check-for-updates" },
|
||||
{ type: "separator" },
|
||||
@@ -53,13 +75,13 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
|
||||
{
|
||||
label: t("menu.file"),
|
||||
submenu: [
|
||||
{ label: t("menu.newTab"), action: "new-tab", accelerator: "t" },
|
||||
{ label: t("menu.closeTab"), action: "close-tab", accelerator: "w" },
|
||||
{ label: t("menu.newTab"), action: "new-tab", accelerator: accelerator("new-tab") },
|
||||
{ label: t("menu.closeTab"), action: "close-tab", accelerator: accelerator("close-tab") },
|
||||
{ type: "separator" },
|
||||
{ label: t("menu.newFolder"), action: "new-folder" },
|
||||
{ label: t("menu.newFile"), action: "new-file" },
|
||||
{ label: t("menu.newFolder"), action: "new-folder", accelerator: accelerator("new-folder") },
|
||||
{ label: t("menu.newFile"), action: "new-file", accelerator: accelerator("new-file") },
|
||||
{ type: "separator" },
|
||||
{ label: t("menu.editFile"), action: "edit-file", accelerator: "e" },
|
||||
{ label: t("menu.editFile"), action: "edit-file", accelerator: accelerator("edit") },
|
||||
{ type: "separator" },
|
||||
{ label: t("menu.connect"), submenu: connectSubmenu },
|
||||
],
|
||||
@@ -81,7 +103,7 @@ export async function applyApplicationMenu(store: ConnectionStore): Promise<void
|
||||
{
|
||||
label: t("menu.view"),
|
||||
submenu: [
|
||||
{ label: t("menu.refresh"), action: "refresh-active", accelerator: "r" },
|
||||
{ label: t("menu.refresh"), action: "refresh-active", accelerator: accelerator("refresh") },
|
||||
{ type: "separator" },
|
||||
{ role: "toggleFullScreen" },
|
||||
],
|
||||
|
||||
@@ -36,6 +36,7 @@ export const de = {
|
||||
transfers: "Übertragungen",
|
||||
editor: "Editor",
|
||||
fileList: "Dateiliste",
|
||||
keyboard: "Tastatur",
|
||||
about: "Über",
|
||||
},
|
||||
columns: "Sichtbare Spalten",
|
||||
@@ -51,6 +52,7 @@ export const de = {
|
||||
systemDefault: "Systemstandard",
|
||||
resetDefault: "Auf Standard zurücksetzen",
|
||||
finishedTransfers: { label: "Abgeschlossene Übertragungen", autoClear: "Auto-löschen", keep: "Behalten" },
|
||||
openFiles: { label: "Eingabe / Doppelklick auf eine Datei", nothing: "Nichts tun", open: "Öffnen", hint: "Lokale Dateien öffnen sich in ihrer Standard-App; Ordner öffnen sich immer." },
|
||||
},
|
||||
kinds: {
|
||||
folder: "Ordner",
|
||||
@@ -293,6 +295,108 @@ export const de = {
|
||||
trashTooltip: "Finders Wiederherstellen ist für von hier gelöschte Elemente nicht verfügbar",
|
||||
},
|
||||
keys: {
|
||||
"cursor-up": "Nach oben",
|
||||
"cursor-down": "Nach unten",
|
||||
"page-up": "Seite nach oben",
|
||||
"page-down": "Seite nach unten",
|
||||
"cursor-first": "Erster Eintrag",
|
||||
"cursor-last": "Letzter Eintrag",
|
||||
"toggle-select": "Auswählen / abwählen",
|
||||
"select-all": "Alles auswählen",
|
||||
open: "Öffnen",
|
||||
parent: "Übergeordneter Ordner",
|
||||
"switch-panel": "Panel wechseln",
|
||||
"row-menu": "Menü des Eintrags zeigen",
|
||||
send: "An anderes Panel senden",
|
||||
edit: "Bearbeiten",
|
||||
rename: "Umbenennen",
|
||||
"new-folder": "Neuer Ordner",
|
||||
"new-file": "Neue Datei",
|
||||
chmod: "Berechtigungen",
|
||||
delete: "Löschen",
|
||||
trash: "In den Papierkorb",
|
||||
"copy-path": "Pfad kopieren",
|
||||
reveal: "Im Dateimanager zeigen",
|
||||
"open-default": "Mit Standard-App öffnen",
|
||||
refresh: "Aktualisieren",
|
||||
"focus-path": "Zum Pfad",
|
||||
"source-picker": "Panelinhalt wählen",
|
||||
"sort-name": "Nach Name sortieren",
|
||||
"sort-size": "Nach Größe sortieren",
|
||||
"sort-mtime": "Nach Änderungsdatum sortieren",
|
||||
"sort-kind": "Nach Art sortieren",
|
||||
"sort-rights": "Nach Rechten sortieren",
|
||||
"sort-owner": "Nach Eigentümer sortieren",
|
||||
"sort-group": "Nach Gruppe sortieren",
|
||||
"new-tab": "Neuer Tab",
|
||||
"close-tab": "Tab schließen",
|
||||
"close-other-tabs": "Andere Tabs schließen",
|
||||
"close-tabs-right": "Tabs rechts schließen",
|
||||
"next-tab": "Nächster Tab",
|
||||
"prev-tab": "Vorheriger Tab",
|
||||
"go-to-tab": "Zu Tab {n}",
|
||||
"go-to-last-tab": "Zum letzten Tab",
|
||||
"move-tab-left": "Tab nach links",
|
||||
"move-tab-right": "Tab nach rechts",
|
||||
"disconnect-tab": "Tab trennen",
|
||||
"open-settings": "Einstellungen",
|
||||
"connection-manager": "Verbindungsverwaltung",
|
||||
"toggle-transfers": "Übertragungen ein-/ausblenden",
|
||||
"install-update": "Update installieren",
|
||||
"scale-up": "Oberfläche größer",
|
||||
"scale-down": "Oberfläche kleiner",
|
||||
"scale-reset": "Oberfläche auf 100 %",
|
||||
"close-window": "Fenster schließen",
|
||||
save: "Speichern",
|
||||
find: "Suchen",
|
||||
"find-next": "Weitersuchen",
|
||||
"find-prev": "Rückwärts suchen",
|
||||
"toggle-wrap": "Zeilen umbrechen",
|
||||
"conn-new": "Neue Verbindung",
|
||||
"conn-new-stack": "Neue Gruppe",
|
||||
"conn-save": "Verbindung speichern",
|
||||
"conn-connect": "Verbinden / Gruppe öffnen",
|
||||
"conn-delete": "Verbindung oder Gruppe löschen",
|
||||
"conn-rename-stack": "Gruppe umbenennen",
|
||||
"conn-import": "Aus WinSCP importieren",
|
||||
groups: {
|
||||
panels: "Dateiliste",
|
||||
files: "Dateien",
|
||||
sort: "Sortierung",
|
||||
tabs: "Tabs",
|
||||
window: "Fenster",
|
||||
editor: "Editor",
|
||||
connections: "Verbindungsverwaltung",
|
||||
},
|
||||
ui: {
|
||||
filter: "Tastenkürzel suchen",
|
||||
intro: "Auf + klicken und die Tasten drücken, um ein Kürzel hinzuzufügen. Das erste zeigen Menüs und Tooltips an.",
|
||||
add: "Kürzel hinzufügen",
|
||||
remove: "{key} entfernen",
|
||||
reset: "Auf Standard zurücksetzen",
|
||||
resetAll: "Alle zurücksetzen",
|
||||
resetAllConfirm: "Alle Kürzel zurücksetzen",
|
||||
press: "Tasten drücken… ({esc} bricht ab)",
|
||||
unbound: "Kein Kürzel",
|
||||
reserved: "{key} ist vom System reserviert",
|
||||
needsModifier: "Im Editor würde {key} nur tippen – eine Sondertaste hinzufügen oder eine F-Taste nehmen",
|
||||
usedBy: "{key} wird schon von „{action}“ benutzt",
|
||||
replace: "Ersetzen",
|
||||
conflict: "Gleiche Taste wie {action}",
|
||||
plainLetter: "{key} in der Dateiliste tippen filtert sie nicht mehr",
|
||||
noResults: "Kein Kürzel passt",
|
||||
fixed: "Feste Tasten",
|
||||
},
|
||||
fixed: {
|
||||
escape: "{esc} schließt Menüs, Dialoge und die Suchleiste",
|
||||
escList: "{esc} in der Dateiliste leert den Filter, bricht dann eine Verbindung ab und hebt dann die Auswahl auf",
|
||||
dialogs: "{enter} und {esc} bestätigen oder verwerfen einen Dialog; {tab} und {shiftTab} wechseln zwischen seinen Elementen",
|
||||
menus: "↑ ↓ {home} {end} und {enter} funktionieren in jedem Menü",
|
||||
filter: "Tippen in der Dateiliste filtert sie; {backspace} löscht das letzte Zeichen des Filters",
|
||||
shift: "Umschalt mit einer Bewegungstaste wählt die überfahrenen Zeilen aus",
|
||||
find: "{enter} und {shiftEnter} springen durch die Treffer der Suche",
|
||||
sidebar: "↑ ↓ wechseln hier zwischen den Bereichen",
|
||||
},
|
||||
names: {
|
||||
ctrl: "Strg",
|
||||
alt: "Alt",
|
||||
|
||||
@@ -40,6 +40,7 @@ export const en = {
|
||||
transfers: "Transfers",
|
||||
editor: "Editor",
|
||||
fileList: "File list",
|
||||
keyboard: "Keyboard",
|
||||
about: "About",
|
||||
},
|
||||
columns: "Visible columns",
|
||||
@@ -55,6 +56,7 @@ export const en = {
|
||||
systemDefault: "System default",
|
||||
resetDefault: "Reset to system default",
|
||||
finishedTransfers: { label: "Finished transfers", autoClear: "Auto-clear", keep: "Keep" },
|
||||
openFiles: { label: "Enter / double-click on a file", nothing: "Do nothing", open: "Open", hint: "Local files open in their default app; folders always open." },
|
||||
},
|
||||
kinds: {
|
||||
folder: "Folder",
|
||||
@@ -372,6 +374,37 @@ export const en = {
|
||||
editor: "Editor",
|
||||
connections: "Connection manager",
|
||||
},
|
||||
// Settings → Keyboard
|
||||
ui: {
|
||||
filter: "Search shortcuts",
|
||||
intro: "Press + and type the keys to add a shortcut. The first one is the hint menus and tooltips show.",
|
||||
add: "Add a shortcut",
|
||||
remove: "Remove {key}",
|
||||
reset: "Reset to default",
|
||||
resetAll: "Reset all",
|
||||
resetAllConfirm: "Reset every shortcut",
|
||||
press: "Press keys… ({esc} cancels)",
|
||||
unbound: "No shortcut",
|
||||
reserved: "{key} is reserved by the system",
|
||||
needsModifier: "In the editor {key} would only type — add a modifier or use an F-key",
|
||||
usedBy: "{key} is already used by “{action}”",
|
||||
replace: "Replace",
|
||||
conflict: "Same key as {action}",
|
||||
plainLetter: "Typing {key} in the file list no longer filters it",
|
||||
noResults: "No shortcut matches",
|
||||
fixed: "Fixed keys",
|
||||
},
|
||||
// fixed keys, listed read-only there (key params come from the platform)
|
||||
fixed: {
|
||||
escape: "{esc} closes menus, dialogs and the find bar",
|
||||
escList: "{esc} in the file list clears the filter, then cancels a connect, then clears the selection",
|
||||
dialogs: "{enter} and {esc} confirm or cancel a dialog; {tab} and {shiftTab} move between its controls",
|
||||
menus: "↑ ↓ {home} {end} and {enter} work in every menu",
|
||||
filter: "Typing in the file list filters it; {backspace} trims the filter",
|
||||
shift: "Shift with a movement key selects the rows the cursor passes",
|
||||
find: "{enter} and {shiftEnter} step through the matches in the find box",
|
||||
sidebar: "↑ ↓ move between these sections",
|
||||
},
|
||||
// key names for "Ctrl+Shift+Del"-style hints (not used on macOS, which shows ⌘⇧⌫)
|
||||
names: {
|
||||
ctrl: "Ctrl",
|
||||
|
||||
@@ -36,6 +36,7 @@ export const es = {
|
||||
transfers: "Transferencias",
|
||||
editor: "Editor",
|
||||
fileList: "Lista de archivos",
|
||||
keyboard: "Teclado",
|
||||
about: "Acerca de",
|
||||
},
|
||||
columns: "Columnas visibles",
|
||||
@@ -51,6 +52,7 @@ export const es = {
|
||||
systemDefault: "Predeterminado del sistema",
|
||||
resetDefault: "Restablecer al predeterminado",
|
||||
finishedTransfers: { label: "Transferencias completadas", autoClear: "Auto-limpiar", keep: "Mantener" },
|
||||
openFiles: { label: "Intro / doble clic en un archivo", nothing: "No hacer nada", open: "Abrir", hint: "Los archivos locales se abren con su app predeterminada; las carpetas siempre se abren." },
|
||||
},
|
||||
kinds: {
|
||||
folder: "Carpeta",
|
||||
@@ -293,6 +295,108 @@ export const es = {
|
||||
trashTooltip: "La opción Restaurar de Finder no está disponible para elementos enviados a la papelera desde aquí",
|
||||
},
|
||||
keys: {
|
||||
"cursor-up": "Subir",
|
||||
"cursor-down": "Bajar",
|
||||
"page-up": "Página arriba",
|
||||
"page-down": "Página abajo",
|
||||
"cursor-first": "Primer elemento",
|
||||
"cursor-last": "Último elemento",
|
||||
"toggle-select": "Seleccionar / deseleccionar",
|
||||
"select-all": "Seleccionar todo",
|
||||
open: "Abrir",
|
||||
parent: "Carpeta superior",
|
||||
"switch-panel": "Cambiar de panel",
|
||||
"row-menu": "Mostrar el menú del elemento",
|
||||
send: "Enviar al otro panel",
|
||||
edit: "Editar",
|
||||
rename: "Renombrar",
|
||||
"new-folder": "Nueva carpeta",
|
||||
"new-file": "Nuevo archivo",
|
||||
chmod: "Permisos",
|
||||
delete: "Eliminar",
|
||||
trash: "Mover a la papelera",
|
||||
"copy-path": "Copiar ruta",
|
||||
reveal: "Mostrar en el gestor de archivos",
|
||||
"open-default": "Abrir con la app predeterminada",
|
||||
refresh: "Actualizar",
|
||||
"focus-path": "Ir a la ruta",
|
||||
"source-picker": "Elegir qué muestra el panel",
|
||||
"sort-name": "Ordenar por nombre",
|
||||
"sort-size": "Ordenar por tamaño",
|
||||
"sort-mtime": "Ordenar por fecha de modificación",
|
||||
"sort-kind": "Ordenar por tipo",
|
||||
"sort-rights": "Ordenar por permisos",
|
||||
"sort-owner": "Ordenar por propietario",
|
||||
"sort-group": "Ordenar por grupo",
|
||||
"new-tab": "Nueva pestaña",
|
||||
"close-tab": "Cerrar pestaña",
|
||||
"close-other-tabs": "Cerrar las demás pestañas",
|
||||
"close-tabs-right": "Cerrar las pestañas de la derecha",
|
||||
"next-tab": "Pestaña siguiente",
|
||||
"prev-tab": "Pestaña anterior",
|
||||
"go-to-tab": "Ir a la pestaña {n}",
|
||||
"go-to-last-tab": "Ir a la última pestaña",
|
||||
"move-tab-left": "Mover la pestaña a la izquierda",
|
||||
"move-tab-right": "Mover la pestaña a la derecha",
|
||||
"disconnect-tab": "Desconectar la pestaña",
|
||||
"open-settings": "Ajustes",
|
||||
"connection-manager": "Gestor de conexiones",
|
||||
"toggle-transfers": "Mostrar / ocultar transferencias",
|
||||
"install-update": "Instalar la actualización",
|
||||
"scale-up": "Interfaz más grande",
|
||||
"scale-down": "Interfaz más pequeña",
|
||||
"scale-reset": "Interfaz al 100 %",
|
||||
"close-window": "Cerrar la ventana",
|
||||
save: "Guardar",
|
||||
find: "Buscar",
|
||||
"find-next": "Buscar siguiente",
|
||||
"find-prev": "Buscar anterior",
|
||||
"toggle-wrap": "Ajustar líneas",
|
||||
"conn-new": "Nueva conexión",
|
||||
"conn-new-stack": "Nuevo grupo",
|
||||
"conn-save": "Guardar la conexión",
|
||||
"conn-connect": "Conectar / abrir grupo",
|
||||
"conn-delete": "Eliminar conexión o grupo",
|
||||
"conn-rename-stack": "Renombrar grupo",
|
||||
"conn-import": "Importar de WinSCP",
|
||||
groups: {
|
||||
panels: "Lista de archivos",
|
||||
files: "Archivos",
|
||||
sort: "Orden",
|
||||
tabs: "Pestañas",
|
||||
window: "Ventana",
|
||||
editor: "Editor",
|
||||
connections: "Gestor de conexiones",
|
||||
},
|
||||
ui: {
|
||||
filter: "Buscar atajos",
|
||||
intro: "Pulsa + y teclea las teclas para añadir un atajo. El primero es el que muestran los menús y las pistas.",
|
||||
add: "Añadir un atajo",
|
||||
remove: "Quitar {key}",
|
||||
reset: "Restablecer el predeterminado",
|
||||
resetAll: "Restablecer todo",
|
||||
resetAllConfirm: "Restablecer todos los atajos",
|
||||
press: "Pulsa las teclas… ({esc} cancela)",
|
||||
unbound: "Sin atajo",
|
||||
reserved: "{key} está reservado por el sistema",
|
||||
needsModifier: "En el editor {key} solo escribiría: añade un modificador o usa una tecla F",
|
||||
usedBy: "{key} ya lo usa «{action}»",
|
||||
replace: "Reemplazar",
|
||||
conflict: "Misma tecla que {action}",
|
||||
plainLetter: "Teclear {key} en la lista de archivos ya no la filtra",
|
||||
noResults: "Ningún atajo coincide",
|
||||
fixed: "Teclas fijas",
|
||||
},
|
||||
fixed: {
|
||||
escape: "{esc} cierra menús, diálogos y la barra de búsqueda",
|
||||
escList: "{esc} en la lista de archivos borra el filtro, luego cancela una conexión y luego quita la selección",
|
||||
dialogs: "{enter} y {esc} confirman o cancelan un diálogo; {tab} y {shiftTab} recorren sus controles",
|
||||
menus: "↑ ↓ {home} {end} y {enter} funcionan en todos los menús",
|
||||
filter: "Teclear en la lista de archivos la filtra; {backspace} borra el último carácter del filtro",
|
||||
shift: "Mayús con una tecla de movimiento selecciona las filas por las que pasa el cursor",
|
||||
find: "{enter} y {shiftEnter} recorren las coincidencias de la búsqueda",
|
||||
sidebar: "↑ ↓ cambian de sección aquí",
|
||||
},
|
||||
names: {
|
||||
ctrl: "Ctrl",
|
||||
alt: "Alt",
|
||||
|
||||
@@ -38,6 +38,7 @@ export const eu = {
|
||||
transfers: "Transferentziak",
|
||||
editor: "Editorea",
|
||||
fileList: "Fitxategi-zerrenda",
|
||||
keyboard: "Teklatua",
|
||||
about: "Honi buruz",
|
||||
},
|
||||
columns: "Zutabe ikusgaiak",
|
||||
@@ -53,6 +54,7 @@ export const eu = {
|
||||
systemDefault: "Sistemaren lehenetsia",
|
||||
resetDefault: "Berrezarri lehenetsira",
|
||||
finishedTransfers: { label: "Amaitutako transferentziak", autoClear: "Garbitu", keep: "Mantendu" },
|
||||
openFiles: { label: "Sartu / klik bikoitza fitxategi batean", nothing: "Ezer ez", open: "Ireki", hint: "Fitxategi lokalak beren aplikazio lehenetsiarekin irekitzen dira; karpetak beti irekitzen dira." },
|
||||
},
|
||||
kinds: {
|
||||
folder: "Karpeta",
|
||||
@@ -295,6 +297,108 @@ export const eu = {
|
||||
trashTooltip: "Finder-en «Jarri berriro» ez dago erabilgarri hemendik zakarrontzira botatako elementuentzat",
|
||||
},
|
||||
keys: {
|
||||
"cursor-up": "Gora",
|
||||
"cursor-down": "Behera",
|
||||
"page-up": "Orrialde bat gora",
|
||||
"page-down": "Orrialde bat behera",
|
||||
"cursor-first": "Lehen elementua",
|
||||
"cursor-last": "Azken elementua",
|
||||
"toggle-select": "Hautatu / desautatu",
|
||||
"select-all": "Hautatu dena",
|
||||
open: "Ireki",
|
||||
parent: "Karpeta gurasoa",
|
||||
"switch-panel": "Aldatu panela",
|
||||
"row-menu": "Erakutsi elementuaren menua",
|
||||
send: "Bidali beste panelera",
|
||||
edit: "Editatu",
|
||||
rename: "Berrizendatu",
|
||||
"new-folder": "Karpeta berria",
|
||||
"new-file": "Fitxategi berria",
|
||||
chmod: "Baimenak",
|
||||
delete: "Ezabatu",
|
||||
trash: "Eraman zakarrontzira",
|
||||
"copy-path": "Kopiatu bidea",
|
||||
reveal: "Erakutsi fitxategi-kudeatzailean",
|
||||
"open-default": "Ireki aplikazio lehenetsiarekin",
|
||||
refresh: "Freskatu",
|
||||
"focus-path": "Joan bidera",
|
||||
"source-picker": "Aukeratu panelak zer erakusten duen",
|
||||
"sort-name": "Ordenatu izenaren arabera",
|
||||
"sort-size": "Ordenatu tamainaren arabera",
|
||||
"sort-mtime": "Ordenatu aldaketa-dataren arabera",
|
||||
"sort-kind": "Ordenatu motaren arabera",
|
||||
"sort-rights": "Ordenatu baimenen arabera",
|
||||
"sort-owner": "Ordenatu jabearen arabera",
|
||||
"sort-group": "Ordenatu taldearen arabera",
|
||||
"new-tab": "Fitxa berria",
|
||||
"close-tab": "Itxi fitxa",
|
||||
"close-other-tabs": "Itxi beste fitxak",
|
||||
"close-tabs-right": "Itxi eskuineko fitxak",
|
||||
"next-tab": "Hurrengo fitxa",
|
||||
"prev-tab": "Aurreko fitxa",
|
||||
"go-to-tab": "Joan {n}. fitxara",
|
||||
"go-to-last-tab": "Joan azken fitxara",
|
||||
"move-tab-left": "Eraman fitxa ezkerrera",
|
||||
"move-tab-right": "Eraman fitxa eskuinera",
|
||||
"disconnect-tab": "Deskonektatu fitxa",
|
||||
"open-settings": "Ezarpenak",
|
||||
"connection-manager": "Konexio-kudeatzailea",
|
||||
"toggle-transfers": "Erakutsi / ezkutatu transferentziak",
|
||||
"install-update": "Instalatu eguneraketa",
|
||||
"scale-up": "Interfaze handiagoa",
|
||||
"scale-down": "Interfaze txikiagoa",
|
||||
"scale-reset": "Interfazea % 100ean",
|
||||
"close-window": "Itxi leihoa",
|
||||
save: "Gorde",
|
||||
find: "Bilatu",
|
||||
"find-next": "Bilatu hurrengoa",
|
||||
"find-prev": "Bilatu aurrekoa",
|
||||
"toggle-wrap": "Doitu lerroak",
|
||||
"conn-new": "Konexio berria",
|
||||
"conn-new-stack": "Talde berria",
|
||||
"conn-save": "Gorde konexioa",
|
||||
"conn-connect": "Konektatu / ireki taldea",
|
||||
"conn-delete": "Ezabatu konexioa edo taldea",
|
||||
"conn-rename-stack": "Berrizendatu taldea",
|
||||
"conn-import": "Inportatu WinSCPtik",
|
||||
groups: {
|
||||
panels: "Fitxategi-zerrenda",
|
||||
files: "Fitxategiak",
|
||||
sort: "Ordena",
|
||||
tabs: "Fitxak",
|
||||
window: "Leihoa",
|
||||
editor: "Editorea",
|
||||
connections: "Konexio-kudeatzailea",
|
||||
},
|
||||
ui: {
|
||||
filter: "Bilatu lasterbideak",
|
||||
intro: "Sakatu + eta idatzi teklak lasterbide bat gehitzeko. Lehenengoa da menuek eta argibideek erakusten dutena.",
|
||||
add: "Gehitu lasterbide bat",
|
||||
remove: "Kendu {key}",
|
||||
reset: "Berrezarri lehenetsia",
|
||||
resetAll: "Berrezarri dena",
|
||||
resetAllConfirm: "Berrezarri lasterbide guztiak",
|
||||
press: "Sakatu teklak… ({esc} utzi)",
|
||||
unbound: "Lasterbiderik ez",
|
||||
reserved: "{key} sistemak erreserbatuta dauka",
|
||||
needsModifier: "Editorean {key}(e)k idatzi besterik ez luke egingo: gehitu aldatzaile bat edo erabili F tekla bat",
|
||||
usedBy: "{key} «{action}»(e)k erabiltzen du dagoeneko",
|
||||
replace: "Ordeztu",
|
||||
conflict: "{action}(r)en tekla bera",
|
||||
plainLetter: "Fitxategi-zerrendan {key} idazteak ez du jada iragazten",
|
||||
noResults: "Ez dago bat datorren lasterbiderik",
|
||||
fixed: "Tekla finkoak",
|
||||
},
|
||||
fixed: {
|
||||
escape: "{esc}(e)k menuak, elkarrizketa-koadroak eta bilaketa-barra ixten ditu",
|
||||
escList: "Fitxategi-zerrendan {esc}(e)k iragazkia garbitzen du, gero konexioa bertan behera uzten du eta gero hautapena kentzen du",
|
||||
dialogs: "{enter} eta {esc} elkarrizketa-koadroa berresten edo uzten dute; {tab} eta {shiftTab} haren kontroletan mugitzen dira",
|
||||
menus: "↑ ↓ {home} {end} eta {enter} menu guztietan dabiltza",
|
||||
filter: "Fitxategi-zerrendan idazteak iragazi egiten du; {backspace}(e)k iragazkiaren azken karakterea ezabatzen du",
|
||||
shift: "Maius mugimendu-tekla batekin kurtsoreak igarotako errenkadak hautatzen ditu",
|
||||
find: "{enter} eta {shiftEnter} bilaketako bat-etortzeetan mugitzen dira",
|
||||
sidebar: "↑ ↓ atal batetik bestera mugitzen dira hemen",
|
||||
},
|
||||
names: {
|
||||
ctrl: "Ctrl",
|
||||
alt: "Alt",
|
||||
|
||||
@@ -36,6 +36,7 @@ export const fr = {
|
||||
transfers: "Transferts",
|
||||
editor: "Éditeur",
|
||||
fileList: "Liste des fichiers",
|
||||
keyboard: "Clavier",
|
||||
about: "À propos",
|
||||
},
|
||||
columns: "Colonnes visibles",
|
||||
@@ -51,6 +52,7 @@ export const fr = {
|
||||
systemDefault: "Valeur par défaut du système",
|
||||
resetDefault: "Réinitialiser par défaut",
|
||||
finishedTransfers: { label: "Transferts terminés", autoClear: "Effacer auto.", keep: "Garder" },
|
||||
openFiles: { label: "Entrée / double-clic sur un fichier", nothing: "Ne rien faire", open: "Ouvrir", hint: "Les fichiers locaux s'ouvrent avec leur application par défaut ; les dossiers s'ouvrent toujours." },
|
||||
},
|
||||
kinds: {
|
||||
folder: "Dossier",
|
||||
@@ -293,6 +295,108 @@ export const fr = {
|
||||
trashTooltip: "La restauration du Finder n'est pas disponible pour les éléments mis à la corbeille d'ici",
|
||||
},
|
||||
keys: {
|
||||
"cursor-up": "Monter",
|
||||
"cursor-down": "Descendre",
|
||||
"page-up": "Page précédente",
|
||||
"page-down": "Page suivante",
|
||||
"cursor-first": "Premier élément",
|
||||
"cursor-last": "Dernier élément",
|
||||
"toggle-select": "Sélectionner / désélectionner",
|
||||
"select-all": "Tout sélectionner",
|
||||
open: "Ouvrir",
|
||||
parent: "Dossier parent",
|
||||
"switch-panel": "Changer de panneau",
|
||||
"row-menu": "Afficher le menu de l'élément",
|
||||
send: "Envoyer vers l'autre panneau",
|
||||
edit: "Éditer",
|
||||
rename: "Renommer",
|
||||
"new-folder": "Nouveau dossier",
|
||||
"new-file": "Nouveau fichier",
|
||||
chmod: "Permissions",
|
||||
delete: "Supprimer",
|
||||
trash: "Mettre à la corbeille",
|
||||
"copy-path": "Copier le chemin",
|
||||
reveal: "Afficher dans le gestionnaire de fichiers",
|
||||
"open-default": "Ouvrir avec l'application par défaut",
|
||||
refresh: "Actualiser",
|
||||
"focus-path": "Aller au chemin",
|
||||
"source-picker": "Choisir ce qu'affiche le panneau",
|
||||
"sort-name": "Trier par nom",
|
||||
"sort-size": "Trier par taille",
|
||||
"sort-mtime": "Trier par date de modification",
|
||||
"sort-kind": "Trier par type",
|
||||
"sort-rights": "Trier par droits",
|
||||
"sort-owner": "Trier par propriétaire",
|
||||
"sort-group": "Trier par groupe",
|
||||
"new-tab": "Nouvel onglet",
|
||||
"close-tab": "Fermer l'onglet",
|
||||
"close-other-tabs": "Fermer les autres onglets",
|
||||
"close-tabs-right": "Fermer les onglets à droite",
|
||||
"next-tab": "Onglet suivant",
|
||||
"prev-tab": "Onglet précédent",
|
||||
"go-to-tab": "Aller à l'onglet {n}",
|
||||
"go-to-last-tab": "Aller au dernier onglet",
|
||||
"move-tab-left": "Déplacer l'onglet à gauche",
|
||||
"move-tab-right": "Déplacer l'onglet à droite",
|
||||
"disconnect-tab": "Déconnecter l'onglet",
|
||||
"open-settings": "Réglages",
|
||||
"connection-manager": "Gestionnaire de connexions",
|
||||
"toggle-transfers": "Afficher / masquer les transferts",
|
||||
"install-update": "Installer la mise à jour",
|
||||
"scale-up": "Interface plus grande",
|
||||
"scale-down": "Interface plus petite",
|
||||
"scale-reset": "Interface à 100 %",
|
||||
"close-window": "Fermer la fenêtre",
|
||||
save: "Enregistrer",
|
||||
find: "Rechercher",
|
||||
"find-next": "Résultat suivant",
|
||||
"find-prev": "Résultat précédent",
|
||||
"toggle-wrap": "Retour à la ligne",
|
||||
"conn-new": "Nouvelle connexion",
|
||||
"conn-new-stack": "Nouveau groupe",
|
||||
"conn-save": "Enregistrer la connexion",
|
||||
"conn-connect": "Se connecter / ouvrir le groupe",
|
||||
"conn-delete": "Supprimer la connexion ou le groupe",
|
||||
"conn-rename-stack": "Renommer le groupe",
|
||||
"conn-import": "Importer depuis WinSCP",
|
||||
groups: {
|
||||
panels: "Liste des fichiers",
|
||||
files: "Fichiers",
|
||||
sort: "Tri",
|
||||
tabs: "Onglets",
|
||||
window: "Fenêtre",
|
||||
editor: "Éditeur",
|
||||
connections: "Gestionnaire de connexions",
|
||||
},
|
||||
ui: {
|
||||
filter: "Rechercher un raccourci",
|
||||
intro: "Cliquez sur + et tapez les touches pour ajouter un raccourci. Le premier est celui qu'affichent les menus et les infobulles.",
|
||||
add: "Ajouter un raccourci",
|
||||
remove: "Retirer {key}",
|
||||
reset: "Rétablir la valeur par défaut",
|
||||
resetAll: "Tout rétablir",
|
||||
resetAllConfirm: "Rétablir tous les raccourcis",
|
||||
press: "Tapez les touches… ({esc} annule)",
|
||||
unbound: "Aucun raccourci",
|
||||
reserved: "{key} est réservé par le système",
|
||||
needsModifier: "Dans l'éditeur, {key} ne ferait que taper : ajoutez un modificateur ou utilisez une touche F",
|
||||
usedBy: "{key} est déjà utilisé par « {action} »",
|
||||
replace: "Remplacer",
|
||||
conflict: "Même touche que {action}",
|
||||
plainLetter: "Taper {key} dans la liste des fichiers ne la filtre plus",
|
||||
noResults: "Aucun raccourci ne correspond",
|
||||
fixed: "Touches fixes",
|
||||
},
|
||||
fixed: {
|
||||
escape: "{esc} ferme les menus, les dialogues et la barre de recherche",
|
||||
escList: "{esc} dans la liste des fichiers efface le filtre, puis annule une connexion, puis vide la sélection",
|
||||
dialogs: "{enter} et {esc} valident ou annulent un dialogue ; {tab} et {shiftTab} passent d'un contrôle à l'autre",
|
||||
menus: "↑ ↓ {home} {end} et {enter} fonctionnent dans tous les menus",
|
||||
filter: "Taper dans la liste des fichiers la filtre ; {backspace} efface le dernier caractère du filtre",
|
||||
shift: "Maj avec une touche de déplacement sélectionne les lignes parcourues",
|
||||
find: "{enter} et {shiftEnter} parcourent les résultats de la recherche",
|
||||
sidebar: "↑ ↓ changent de section ici",
|
||||
},
|
||||
names: {
|
||||
ctrl: "Ctrl",
|
||||
alt: "Alt",
|
||||
|
||||
@@ -37,6 +37,7 @@ export const ja = {
|
||||
transfers: "転送",
|
||||
editor: "エディタ",
|
||||
fileList: "ファイル一覧",
|
||||
keyboard: "キーボード",
|
||||
about: "情報",
|
||||
},
|
||||
columns: "表示する列",
|
||||
@@ -52,6 +53,7 @@ export const ja = {
|
||||
systemDefault: "システムの既定",
|
||||
resetDefault: "既定に戻す",
|
||||
finishedTransfers: { label: "完了した転送", autoClear: "自動クリア", keep: "保持" },
|
||||
openFiles: { label: "Enter / ファイルをダブルクリック", nothing: "何もしない", open: "開く", hint: "ローカルファイルは既定のアプリで開きます。フォルダは常に開きます。" },
|
||||
},
|
||||
kinds: {
|
||||
folder: "フォルダ",
|
||||
@@ -293,6 +295,108 @@ export const ja = {
|
||||
trashTooltip: "ここからゴミ箱に入れた項目は Finder の「戻す」を利用できません",
|
||||
},
|
||||
keys: {
|
||||
"cursor-up": "上へ",
|
||||
"cursor-down": "下へ",
|
||||
"page-up": "前のページ",
|
||||
"page-down": "次のページ",
|
||||
"cursor-first": "先頭の項目",
|
||||
"cursor-last": "最後の項目",
|
||||
"toggle-select": "選択 / 選択解除",
|
||||
"select-all": "すべて選択",
|
||||
open: "開く",
|
||||
parent: "親フォルダ",
|
||||
"switch-panel": "パネルを切り替え",
|
||||
"row-menu": "項目のメニューを表示",
|
||||
send: "もう一方のパネルへ送信",
|
||||
edit: "編集",
|
||||
rename: "名前を変更",
|
||||
"new-folder": "新規フォルダ",
|
||||
"new-file": "新規ファイル",
|
||||
chmod: "アクセス権",
|
||||
delete: "削除",
|
||||
trash: "ゴミ箱に移動",
|
||||
"copy-path": "パスをコピー",
|
||||
reveal: "ファイルマネージャーで表示",
|
||||
"open-default": "既定のアプリで開く",
|
||||
refresh: "更新",
|
||||
"focus-path": "パスへ移動",
|
||||
"source-picker": "パネルの表示内容を選択",
|
||||
"sort-name": "名前で並べ替え",
|
||||
"sort-size": "サイズで並べ替え",
|
||||
"sort-mtime": "更新日時で並べ替え",
|
||||
"sort-kind": "種類で並べ替え",
|
||||
"sort-rights": "権限で並べ替え",
|
||||
"sort-owner": "所有者で並べ替え",
|
||||
"sort-group": "グループで並べ替え",
|
||||
"new-tab": "新しいタブ",
|
||||
"close-tab": "タブを閉じる",
|
||||
"close-other-tabs": "他のタブを閉じる",
|
||||
"close-tabs-right": "右側のタブを閉じる",
|
||||
"next-tab": "次のタブ",
|
||||
"prev-tab": "前のタブ",
|
||||
"go-to-tab": "タブ {n} へ移動",
|
||||
"go-to-last-tab": "最後のタブへ移動",
|
||||
"move-tab-left": "タブを左へ移動",
|
||||
"move-tab-right": "タブを右へ移動",
|
||||
"disconnect-tab": "タブを切断",
|
||||
"open-settings": "設定",
|
||||
"connection-manager": "接続マネージャー",
|
||||
"toggle-transfers": "転送の表示 / 非表示",
|
||||
"install-update": "アップデートをインストール",
|
||||
"scale-up": "インターフェースを拡大",
|
||||
"scale-down": "インターフェースを縮小",
|
||||
"scale-reset": "インターフェースを 100% に",
|
||||
"close-window": "ウインドウを閉じる",
|
||||
save: "保存",
|
||||
find: "検索",
|
||||
"find-next": "次を検索",
|
||||
"find-prev": "前を検索",
|
||||
"toggle-wrap": "行を折り返す",
|
||||
"conn-new": "新規接続",
|
||||
"conn-new-stack": "新規グループ",
|
||||
"conn-save": "接続を保存",
|
||||
"conn-connect": "接続 / グループを開く",
|
||||
"conn-delete": "接続またはグループを削除",
|
||||
"conn-rename-stack": "グループ名を変更",
|
||||
"conn-import": "WinSCP からインポート",
|
||||
groups: {
|
||||
panels: "ファイル一覧",
|
||||
files: "ファイル",
|
||||
sort: "並べ替え",
|
||||
tabs: "タブ",
|
||||
window: "ウインドウ",
|
||||
editor: "エディタ",
|
||||
connections: "接続マネージャー",
|
||||
},
|
||||
ui: {
|
||||
filter: "ショートカットを検索",
|
||||
intro: "+ を押してキーを入力するとショートカットを追加できます。最初のものがメニューとツールチップに表示されます。",
|
||||
add: "ショートカットを追加",
|
||||
remove: "{key} を削除",
|
||||
reset: "既定に戻す",
|
||||
resetAll: "すべて戻す",
|
||||
resetAllConfirm: "すべてのショートカットを戻す",
|
||||
press: "キーを押してください…({esc} でキャンセル)",
|
||||
unbound: "ショートカットなし",
|
||||
reserved: "{key} はシステムで予約されています",
|
||||
needsModifier: "エディタでは {key} は入力されるだけです。修飾キーを加えるか F キーを使ってください",
|
||||
usedBy: "{key} はすでに「{action}」で使われています",
|
||||
replace: "置き換える",
|
||||
conflict: "{action} と同じキー",
|
||||
plainLetter: "ファイル一覧で {key} を入力しても絞り込まれなくなります",
|
||||
noResults: "一致するショートカットはありません",
|
||||
fixed: "固定キー",
|
||||
},
|
||||
fixed: {
|
||||
escape: "{esc} でメニュー、ダイアログ、検索バーを閉じます",
|
||||
escList: "ファイル一覧の {esc} は、絞り込みの解除、接続のキャンセル、選択の解除の順に働きます",
|
||||
dialogs: "{enter} と {esc} でダイアログを確定 / キャンセルし、{tab} と {shiftTab} でコントロール間を移動します",
|
||||
menus: "↑ ↓ {home} {end} と {enter} はすべてのメニューで使えます",
|
||||
filter: "ファイル一覧で入力すると絞り込まれます。{backspace} で最後の文字を消します",
|
||||
shift: "Shift と移動キーで、カーソルが通過した行を選択します",
|
||||
find: "{enter} と {shiftEnter} で検索結果を移動します",
|
||||
sidebar: "↑ ↓ でここのセクションを切り替えます",
|
||||
},
|
||||
names: {
|
||||
ctrl: "Ctrl",
|
||||
alt: "Alt",
|
||||
|
||||
@@ -37,6 +37,7 @@ export const zh = {
|
||||
transfers: "传输",
|
||||
editor: "编辑器",
|
||||
fileList: "文件列表",
|
||||
keyboard: "键盘",
|
||||
about: "关于",
|
||||
},
|
||||
columns: "显示的列",
|
||||
@@ -52,6 +53,7 @@ export const zh = {
|
||||
systemDefault: "系统默认",
|
||||
resetDefault: "恢复为系统默认",
|
||||
finishedTransfers: { label: "已完成传输", autoClear: "自动清除", keep: "保留" },
|
||||
openFiles: { label: "回车 / 双击文件", nothing: "不执行操作", open: "打开", hint: "本地文件用默认应用打开;文件夹始终打开。" },
|
||||
},
|
||||
kinds: {
|
||||
folder: "文件夹",
|
||||
@@ -293,6 +295,108 @@ export const zh = {
|
||||
trashTooltip: "从此处移入废纸篓的项目无法使用访达的放回原处功能",
|
||||
},
|
||||
keys: {
|
||||
"cursor-up": "上移",
|
||||
"cursor-down": "下移",
|
||||
"page-up": "上一页",
|
||||
"page-down": "下一页",
|
||||
"cursor-first": "第一项",
|
||||
"cursor-last": "最后一项",
|
||||
"toggle-select": "选择 / 取消选择",
|
||||
"select-all": "全选",
|
||||
open: "打开",
|
||||
parent: "上级目录",
|
||||
"switch-panel": "切换面板",
|
||||
"row-menu": "显示该项的菜单",
|
||||
send: "发送到另一个面板",
|
||||
edit: "编辑",
|
||||
rename: "重命名",
|
||||
"new-folder": "新建文件夹",
|
||||
"new-file": "新建文件",
|
||||
chmod: "权限",
|
||||
delete: "删除",
|
||||
trash: "移到废纸篓",
|
||||
"copy-path": "复制路径",
|
||||
reveal: "在文件管理器中显示",
|
||||
"open-default": "用默认应用打开",
|
||||
refresh: "刷新",
|
||||
"focus-path": "转到路径",
|
||||
"source-picker": "选择面板显示的内容",
|
||||
"sort-name": "按名称排序",
|
||||
"sort-size": "按大小排序",
|
||||
"sort-mtime": "按修改时间排序",
|
||||
"sort-kind": "按种类排序",
|
||||
"sort-rights": "按权限排序",
|
||||
"sort-owner": "按所有者排序",
|
||||
"sort-group": "按组排序",
|
||||
"new-tab": "新建标签页",
|
||||
"close-tab": "关闭标签页",
|
||||
"close-other-tabs": "关闭其他标签页",
|
||||
"close-tabs-right": "关闭右侧标签页",
|
||||
"next-tab": "下一个标签页",
|
||||
"prev-tab": "上一个标签页",
|
||||
"go-to-tab": "转到标签页 {n}",
|
||||
"go-to-last-tab": "转到最后一个标签页",
|
||||
"move-tab-left": "标签页左移",
|
||||
"move-tab-right": "标签页右移",
|
||||
"disconnect-tab": "断开标签页",
|
||||
"open-settings": "设置",
|
||||
"connection-manager": "连接管理器",
|
||||
"toggle-transfers": "显示 / 隐藏传输",
|
||||
"install-update": "安装更新",
|
||||
"scale-up": "放大界面",
|
||||
"scale-down": "缩小界面",
|
||||
"scale-reset": "界面 100%",
|
||||
"close-window": "关闭窗口",
|
||||
save: "保存",
|
||||
find: "查找",
|
||||
"find-next": "查找下一个",
|
||||
"find-prev": "查找上一个",
|
||||
"toggle-wrap": "自动换行",
|
||||
"conn-new": "新建连接",
|
||||
"conn-new-stack": "新建分组",
|
||||
"conn-save": "保存连接",
|
||||
"conn-connect": "连接 / 打开分组",
|
||||
"conn-delete": "删除连接或分组",
|
||||
"conn-rename-stack": "重命名分组",
|
||||
"conn-import": "从 WinSCP 导入",
|
||||
groups: {
|
||||
panels: "文件列表",
|
||||
files: "文件",
|
||||
sort: "排序",
|
||||
tabs: "标签页",
|
||||
window: "窗口",
|
||||
editor: "编辑器",
|
||||
connections: "连接管理器",
|
||||
},
|
||||
ui: {
|
||||
filter: "搜索快捷键",
|
||||
intro: "点击 + 并按下按键即可添加快捷键。第一个会显示在菜单和提示中。",
|
||||
add: "添加快捷键",
|
||||
remove: "移除 {key}",
|
||||
reset: "恢复默认",
|
||||
resetAll: "全部恢复",
|
||||
resetAllConfirm: "恢复所有快捷键",
|
||||
press: "请按键…({esc} 取消)",
|
||||
unbound: "无快捷键",
|
||||
reserved: "{key} 已被系统保留",
|
||||
needsModifier: "在编辑器中 {key} 只会输入字符:请加上修饰键或使用 F 键",
|
||||
usedBy: "{key} 已被“{action}”使用",
|
||||
replace: "替换",
|
||||
conflict: "与 {action} 的按键相同",
|
||||
plainLetter: "在文件列表中输入 {key} 将不再进行筛选",
|
||||
noResults: "没有匹配的快捷键",
|
||||
fixed: "固定按键",
|
||||
},
|
||||
fixed: {
|
||||
escape: "{esc} 关闭菜单、对话框和查找栏",
|
||||
escList: "在文件列表中按 {esc} 依次清除筛选、取消连接、清除选择",
|
||||
dialogs: "{enter} 和 {esc} 确认或取消对话框;{tab} 和 {shiftTab} 在其控件间移动",
|
||||
menus: "↑ ↓ {home} {end} 和 {enter} 适用于所有菜单",
|
||||
filter: "在文件列表中输入即可筛选;{backspace} 删除筛选的最后一个字符",
|
||||
shift: "Shift 加移动键会选中光标经过的行",
|
||||
find: "{enter} 和 {shiftEnter} 在查找结果间跳转",
|
||||
sidebar: "↑ ↓ 在这些分区间切换",
|
||||
},
|
||||
names: {
|
||||
ctrl: "Ctrl",
|
||||
alt: "Alt",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { DICTS } from "./i18n";
|
||||
import { en } from "./i18n/en";
|
||||
import {
|
||||
BINDINGS,
|
||||
@@ -111,10 +112,15 @@ for (const isMac of [true, false]) {
|
||||
});
|
||||
}
|
||||
|
||||
test("every command and group has an English label", () => {
|
||||
const keys = en.keys as Record<string, unknown>;
|
||||
for (const b of BINDINGS) expect(typeof keys["n" in b ? "go-to-tab" : b.id]).toBe("string");
|
||||
for (const g of GROUPS) expect(typeof en.keys.groups[g]).toBe("string");
|
||||
test("every command and group is labelled — in every shipped language", () => {
|
||||
for (const [locale, dict] of Object.entries(DICTS)) {
|
||||
const keys = dict.keys as Record<string, unknown> & { groups: Record<string, unknown> };
|
||||
for (const b of BINDINGS) {
|
||||
expect(`${locale}:${b.id}:${typeof keys["n" in b ? "go-to-tab" : b.id]}`).toBe(`${locale}:${b.id}:string`);
|
||||
}
|
||||
for (const g of GROUPS) expect(`${locale}:${g}:${typeof keys.groups[g]}`).toBe(`${locale}:${g}:string`);
|
||||
}
|
||||
expect(typeof en.keys.ui.press).toBe("string");
|
||||
});
|
||||
|
||||
test("matching is exact about modifiers", () => {
|
||||
|
||||
@@ -403,6 +403,12 @@ export function firesInTextField(c: Combo, isMac: boolean): boolean {
|
||||
return c.mod || c.alt;
|
||||
}
|
||||
|
||||
/** Whether a plain press of the combo types a character — bound in the file
|
||||
* list, that key then no longer starts a type-to-filter query. */
|
||||
export function typesCharacter(c: Combo): boolean {
|
||||
return !c.mod && !c.ctrl && !c.alt && (LETTER.test(c.key) || DIGIT.test(c.key) || Object.hasOwn(CHAR_OF, c.key));
|
||||
}
|
||||
|
||||
/** Commands a held key may repeat; any other held shortcut fires once
|
||||
* (holding ⌘W must not close every tab). */
|
||||
const REPEATABLE = new Set<ShortcutId>([
|
||||
|
||||
@@ -117,11 +117,16 @@ export class ShortcutsService {
|
||||
return keys.length ? `${text} (${keys.join(' / ')})` : text;
|
||||
}
|
||||
|
||||
/** The same for a FIXED key — one no remap moves, such as Enter in the find
|
||||
* box — so it reads in the platform's spelling too (⇧↩ / Shift+Enter). */
|
||||
tipFixed(text: string, combo: string): string {
|
||||
/** A FIXED key — one no remap moves, such as Enter in the find box — in
|
||||
* the platform's spelling (⇧↩ / Shift+Enter). */
|
||||
fixedKey(combo: string): string {
|
||||
const parsed = parseCombo(combo, IS_MAC);
|
||||
return parsed ? `${text} (${this.format(parsed)})` : text;
|
||||
return parsed ? this.format(parsed) : combo;
|
||||
}
|
||||
|
||||
/** tip() for a fixed key. */
|
||||
tipFixed(text: string, combo: string): string {
|
||||
return `${text} (${this.fixedKey(combo)})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,10 +15,16 @@
|
||||
|
||||
<div class="flex min-h-0 flex-1">
|
||||
<!-- section list -->
|
||||
<nav class="flex w-44 shrink-0 flex-col gap-0.5 border-r border-border bg-chrome p-2">
|
||||
<nav
|
||||
#nav
|
||||
(keydown)="onNavKeydown($event)"
|
||||
class="flex w-44 shrink-0 flex-col gap-0.5 border-r border-border bg-chrome p-2"
|
||||
>
|
||||
@for (s of sections; track s) {
|
||||
<button
|
||||
(click)="section.set(s)"
|
||||
[attr.data-section]="s"
|
||||
[attr.aria-current]="section() === s ? 'page' : null"
|
||||
class="px-2.5 py-1.5 text-left text-xs hover:bg-accent hover:text-foreground"
|
||||
[class.bg-accent]="section() === s"
|
||||
[class.font-medium]="section() === s"
|
||||
@@ -266,6 +272,171 @@
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Enter / double-click on a file: nothing (folders only) or open it -->
|
||||
<div class="mt-2 flex items-center justify-between gap-2">
|
||||
<span class="text-xs">{{ i18n.t('options.openFiles.label') }}</span>
|
||||
<div class="flex border border-border p-0.5">
|
||||
<button
|
||||
(click)="settings.setOpenFilesOnEnter(false)"
|
||||
class="px-2 py-0.5 text-[0.6875rem]"
|
||||
[class.bg-accent]="!settings.openFilesOnEnter()"
|
||||
[class.text-foreground]="!settings.openFilesOnEnter()"
|
||||
[class.text-muted-foreground]="settings.openFilesOnEnter()"
|
||||
>
|
||||
{{ i18n.t('options.openFiles.nothing') }}
|
||||
</button>
|
||||
<button
|
||||
(click)="settings.setOpenFilesOnEnter(true)"
|
||||
class="px-2 py-0.5 text-[0.6875rem]"
|
||||
[class.bg-accent]="settings.openFilesOnEnter()"
|
||||
[class.text-foreground]="settings.openFilesOnEnter()"
|
||||
[class.text-muted-foreground]="!settings.openFilesOnEnter()"
|
||||
>
|
||||
{{ i18n.t('options.openFiles.open') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="-mt-1 pl-3 text-[0.6875rem] text-muted-foreground">{{ i18n.t('options.openFiles.hint') }}</p>
|
||||
}
|
||||
|
||||
@case ('keyboard') {
|
||||
<!-- search + reset all -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<ng-icon
|
||||
name="tdesignSearch"
|
||||
size="0.8rem"
|
||||
class="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<input
|
||||
[value]="keyFilter()"
|
||||
(input)="keyFilter.set($any($event.target).value)"
|
||||
[placeholder]="i18n.t('keys.ui.filter')"
|
||||
spellcheck="false"
|
||||
class="h-7 w-full border border-input bg-background pr-2 pl-7 text-xs outline-none focus:ring-2 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
@if (confirmResetAll()) {
|
||||
<button
|
||||
(click)="resetAll()"
|
||||
class="shrink-0 border border-destructive px-2 py-0.5 text-[0.6875rem] text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
{{ i18n.t('keys.ui.resetAllConfirm') }}
|
||||
</button>
|
||||
<button
|
||||
(click)="confirmResetAll.set(false)"
|
||||
class="shrink-0 px-2 py-0.5 text-[0.6875rem] text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
(click)="confirmResetAll.set(true)"
|
||||
[disabled]="!anyCustom()"
|
||||
class="shrink-0 border border-border px-2 py-0.5 text-[0.6875rem] hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
>
|
||||
{{ i18n.t('keys.ui.resetAll') }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<p class="text-[0.6875rem] text-muted-foreground">{{ i18n.t('keys.ui.intro') }}</p>
|
||||
|
||||
@for (group of keyGroups(); track group.id) {
|
||||
<h3 class="mt-1 text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{{ i18n.t('keys.groups.' + group.id) }}
|
||||
</h3>
|
||||
<div class="-mt-2 flex flex-col">
|
||||
@for (binding of group.rows; track binding.id) {
|
||||
<div class="flex min-h-7 items-center gap-1.5 border-b border-border/50 py-0.5 text-xs">
|
||||
<span class="min-w-0 flex-1 truncate" [title]="rowLabel(binding)">{{ rowLabel(binding) }}</span>
|
||||
@if (conflictWith(binding); as others) {
|
||||
<span
|
||||
class="grid size-4 shrink-0 place-items-center bg-destructive/15 text-[0.625rem] font-bold text-destructive"
|
||||
[title]="i18n.t('keys.ui.conflict', { action: others })"
|
||||
>!</span
|
||||
>
|
||||
}
|
||||
<div class="flex shrink-0 flex-wrap justify-end gap-1">
|
||||
@for (combo of binding.combos; track $index) {
|
||||
<span class="flex items-center border border-border bg-muted pl-1.5 text-[0.6875rem] tabular-nums">
|
||||
{{ shortcuts.format(combo) }}
|
||||
<button
|
||||
(click)="removeCombo(binding, $index)"
|
||||
[attr.aria-label]="i18n.t('keys.ui.remove', { key: shortcuts.format(combo) })"
|
||||
[title]="i18n.t('keys.ui.remove', { key: shortcuts.format(combo) })"
|
||||
class="grid size-4 place-items-center text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ng-icon name="tdesignClose" size="0.6rem" />
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
@if (capturing() === binding.id) {
|
||||
<span class="border border-primary px-1.5 text-[0.6875rem] text-primary">
|
||||
{{ i18n.t('keys.ui.press', { esc: fixedKeys().esc }) }}
|
||||
</span>
|
||||
} @else if (!binding.combos.length) {
|
||||
<span class="px-1 text-[0.6875rem] text-muted-foreground">{{ i18n.t('keys.ui.unbound') }}</span>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
data-record
|
||||
(click)="startCapture(binding.id)"
|
||||
[attr.aria-label]="i18n.t('keys.ui.add')"
|
||||
[title]="i18n.t('keys.ui.add')"
|
||||
class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
[class.text-primary]="capturing() === binding.id"
|
||||
>
|
||||
<ng-icon name="tdesignAdd" size="0.75rem" />
|
||||
</button>
|
||||
<button
|
||||
(click)="resetRow(binding)"
|
||||
[disabled]="!binding.custom"
|
||||
[attr.aria-label]="i18n.t('keys.ui.reset')"
|
||||
[title]="i18n.t('keys.ui.reset')"
|
||||
class="grid size-5 shrink-0 place-items-center text-muted-foreground hover:bg-accent hover:text-foreground disabled:invisible"
|
||||
>
|
||||
<ng-icon name="tdesignRefresh" size="0.7rem" />
|
||||
</button>
|
||||
</div>
|
||||
@if (note()?.id === binding.id) {
|
||||
<div class="flex items-center gap-2 py-1 pl-3 text-[0.6875rem] text-destructive">
|
||||
<span class="min-w-0 flex-1">{{ noteText(note()!) }}</span>
|
||||
@if (note()!.kind === 'conflict') {
|
||||
<button
|
||||
(click)="replaceConflict()"
|
||||
class="shrink-0 border border-border px-2 py-0.5 text-foreground hover:bg-accent"
|
||||
>
|
||||
{{ i18n.t('keys.ui.replace') }}
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
(click)="note.set(null)"
|
||||
class="shrink-0 px-2 py-0.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
{{ i18n.t('common.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (filterKey(binding); as key) {
|
||||
<p class="py-0.5 pl-3 text-[0.6875rem] text-muted-foreground">
|
||||
{{ i18n.t('keys.ui.plainLetter', { key }) }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<p class="text-xs text-muted-foreground">{{ i18n.t('keys.ui.noResults') }}</p>
|
||||
}
|
||||
|
||||
<h3 class="mt-1 text-[0.625rem] font-medium tracking-wide text-muted-foreground uppercase">
|
||||
{{ i18n.t('keys.ui.fixed') }}
|
||||
</h3>
|
||||
<ul class="-mt-1 flex flex-col gap-1 text-[0.6875rem] text-muted-foreground">
|
||||
@for (line of fixedLines; track line) {
|
||||
<li>{{ i18n.t('keys.fixed.' + line, fixedKeys()) }}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@case ('about') {
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { Component, ElementRef, afterNextRender, computed, inject, signal, viewChild } from '@angular/core';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { BRANDING } from '@shared/branding';
|
||||
import { LOCALE_LABELS, SUPPORTED } from '@shared/i18n';
|
||||
import {
|
||||
type Combo,
|
||||
comboFromEvent,
|
||||
conflicts,
|
||||
firesInTextField,
|
||||
GROUPS,
|
||||
isReserved,
|
||||
ownerOf,
|
||||
type ResolvedBinding,
|
||||
sameCombo,
|
||||
type ShortcutId,
|
||||
typesCharacter,
|
||||
} from '@shared/keymap';
|
||||
import { COLUMN_KEYS } from '@core/context-menu.service';
|
||||
import { cycleFocus } from '@core/focus-cycle';
|
||||
import { I18nService } from '@core/i18n.service';
|
||||
import { blockNativeContextMenu, blockReloadKeys, IS_MAC_CHROME } from '@core/platform';
|
||||
import { blockNativeContextMenu, blockReloadKeys, IS_MAC, IS_MAC_CHROME } from '@core/platform';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { SettingsService } from '@core/settings.service';
|
||||
import { ShortcutsService } from '@core/shortcuts.service';
|
||||
|
||||
const SECTIONS = ['appearance', 'transfers', 'editor', 'fileList', 'about'] as const;
|
||||
const SECTIONS = ['appearance', 'transfers', 'editor', 'fileList', 'keyboard', 'about'] as const;
|
||||
type Section = (typeof SECTIONS)[number];
|
||||
|
||||
const THEMES = ['system', 'light', 'dark'] as const;
|
||||
@@ -21,10 +35,23 @@ const SPEED_UNITS = [
|
||||
const TRANSFER_MODES = ['single', 'multi'] as const;
|
||||
const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
|
||||
/** The keys no remap moves, listed read-only under the editable ones. */
|
||||
const FIXED_LINES = ['escape', 'escList', 'dialogs', 'menus', 'filter', 'shift', 'find', 'sidebar'] as const;
|
||||
|
||||
/** A recording that could not simply be added: explained under its row. */
|
||||
interface CaptureNote {
|
||||
id: ShortcutId;
|
||||
kind: 'reserved' | 'needsModifier' | 'conflict';
|
||||
combo: Combo;
|
||||
/** conflict: the command that already owns the combo. */
|
||||
owner?: ShortcutId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Root of the Settings window (APP_MODE 'settings'). Every control is a plain
|
||||
* SettingsService setter: the save goes to bun, which rebroadcasts it, so the
|
||||
* other windows follow live while this one stays open.
|
||||
* other windows follow live while this one stays open — keyboard remaps
|
||||
* included (menu hints, tooltips and the macOS menu bar follow at once).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -33,13 +60,15 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
host: {
|
||||
'(window:keydown)': 'onWindowKeydown($event)',
|
||||
'(window:contextmenu)': 'blockContextMenu($event)',
|
||||
'(window:mousedown)': 'onWindowMousedown($event)',
|
||||
'(window:blur)': 'capturing.set(null)',
|
||||
},
|
||||
})
|
||||
export class SettingsWindow {
|
||||
readonly settings = inject(SettingsService);
|
||||
readonly i18n = inject(I18nService);
|
||||
readonly shortcuts = inject(ShortcutsService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
private readonly shortcuts = inject(ShortcutsService);
|
||||
|
||||
readonly isMacChrome = IS_MAC_CHROME;
|
||||
readonly sections = SECTIONS;
|
||||
@@ -52,11 +81,50 @@ export class SettingsWindow {
|
||||
readonly transferModes = TRANSFER_MODES;
|
||||
readonly concurrencyChoices = CONCURRENCY_CHOICES;
|
||||
readonly columnKeys = COLUMN_KEYS;
|
||||
readonly fixedLines = FIXED_LINES;
|
||||
readonly appName = BRANDING.appName;
|
||||
readonly buildLabel = BRANDING.buildLabel;
|
||||
/** "100%" for the stepper readout; the value itself lives in settings. */
|
||||
readonly scalePercent = computed(() => `${Math.round(this.settings.uiScale() * 100)}%`);
|
||||
|
||||
private readonly nav = viewChild.required<ElementRef<HTMLElement>>('nav');
|
||||
|
||||
// ---- Settings → Keyboard ----
|
||||
readonly keyFilter = signal('');
|
||||
/** The command being recorded, if any. */
|
||||
readonly capturing = signal<ShortcutId | null>(null);
|
||||
readonly note = signal<CaptureNote | null>(null);
|
||||
readonly confirmResetAll = signal(false);
|
||||
readonly anyCustom = computed(() => this.shortcuts.keymap().some((b) => b.custom));
|
||||
private readonly conflictMap = computed(() => conflicts(this.shortcuts.keymap()));
|
||||
|
||||
/** The editable rows by group, narrowed by the search field (label or keys). */
|
||||
readonly keyGroups = computed(() => {
|
||||
const query = this.keyFilter().trim().toLowerCase();
|
||||
const keymap = this.shortcuts.keymap();
|
||||
return GROUPS.map((group) => ({
|
||||
id: group,
|
||||
rows: keymap.filter((b) => b.group === group && (!query || this.rowText(b).includes(query))),
|
||||
})).filter((group) => group.rows.length);
|
||||
});
|
||||
|
||||
/** The fixed keys, in this platform's spelling, for the read-only lines. */
|
||||
readonly fixedKeys = computed(() => ({
|
||||
esc: this.shortcuts.fixedKey('Escape'),
|
||||
enter: this.shortcuts.fixedKey('Enter'),
|
||||
shiftEnter: this.shortcuts.fixedKey('Shift+Enter'),
|
||||
tab: this.shortcuts.fixedKey('Tab'),
|
||||
shiftTab: this.shortcuts.fixedKey('Shift+Tab'),
|
||||
backspace: this.shortcuts.fixedKey('Backspace'),
|
||||
home: this.shortcuts.fixedKey('Home'),
|
||||
end: this.shortcuts.fixedKey('End'),
|
||||
}));
|
||||
|
||||
constructor() {
|
||||
// the keyboard starts on the current section
|
||||
afterNextRender(() => this.sectionButton(this.section())?.focus());
|
||||
}
|
||||
|
||||
/** "Visual Studio Code.app" -> "Visual Studio Code"; empty -> system default. */
|
||||
editorAppName(): string {
|
||||
const path = this.settings.editorApp();
|
||||
@@ -74,16 +142,153 @@ export class SettingsWindow {
|
||||
void this.rpc.request.showAbout({});
|
||||
}
|
||||
|
||||
/** This window's keys: close-window, and the any-window interface scale. */
|
||||
// ---- sections ----
|
||||
|
||||
/** ↑ / ↓ in the sidebar move between sections (and focus follows). */
|
||||
onNavKeydown(event: KeyboardEvent): void {
|
||||
const step = event.key === 'ArrowDown' ? 1 : event.key === 'ArrowUp' ? -1 : 0;
|
||||
if (!step) return;
|
||||
event.preventDefault();
|
||||
const next = SECTIONS[(SECTIONS.indexOf(this.section()) + step + SECTIONS.length) % SECTIONS.length];
|
||||
this.section.set(next);
|
||||
this.sectionButton(next)?.focus();
|
||||
}
|
||||
|
||||
private sectionButton(section: Section): HTMLElement | null {
|
||||
return this.nav().nativeElement.querySelector<HTMLElement>(`[data-section="${section}"]`);
|
||||
}
|
||||
|
||||
// ---- keyboard remaps ----
|
||||
|
||||
rowLabel(binding: ResolvedBinding): string {
|
||||
return binding.n
|
||||
? this.i18n.t('keys.go-to-tab', { n: binding.n })
|
||||
: this.i18n.t(`keys.${binding.id}`);
|
||||
}
|
||||
|
||||
private labelOf(id: ShortcutId): string {
|
||||
const binding = this.shortcuts.binding(id);
|
||||
return binding ? this.rowLabel(binding) : id;
|
||||
}
|
||||
|
||||
private rowText(binding: ResolvedBinding): string {
|
||||
return [this.rowLabel(binding), ...binding.combos.map((c) => this.shortcuts.format(c))]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/** "Rename, Edit" when this row shares a key with others that can collide. */
|
||||
conflictWith(binding: ResolvedBinding): string {
|
||||
return (this.conflictMap().get(binding.id) ?? []).map((id) => this.labelOf(id)).join(', ');
|
||||
}
|
||||
|
||||
/** A plain character key on a file-list row: typing it no longer filters. */
|
||||
filterKey(binding: ResolvedBinding): string {
|
||||
if (!binding.scopes.some((s) => s === 'panel' || s === 'main')) return '';
|
||||
const plain = binding.combos.find(typesCharacter);
|
||||
return plain ? this.shortcuts.format(plain) : '';
|
||||
}
|
||||
|
||||
noteText(note: CaptureNote): string {
|
||||
const key = this.shortcuts.format(note.combo);
|
||||
if (note.kind === 'reserved') return this.i18n.t('keys.ui.reserved', { key });
|
||||
if (note.kind === 'needsModifier') return this.i18n.t('keys.ui.needsModifier', { key });
|
||||
return this.i18n.t('keys.ui.usedBy', { key, action: note.owner ? this.labelOf(note.owner) : '' });
|
||||
}
|
||||
|
||||
startCapture(id: ShortcutId): void {
|
||||
this.note.set(null);
|
||||
this.capturing.set(this.capturing() === id ? null : id);
|
||||
}
|
||||
|
||||
/**
|
||||
* One keydown while recording, taken before every other key in this window
|
||||
* (⌘W records instead of closing). A bare modifier keeps waiting; Esc on its
|
||||
* own cancels. Reserved combos are refused WITHOUT preventDefault, so ⌘Q
|
||||
* still quits and ⌘C still copies.
|
||||
*/
|
||||
private record(event: KeyboardEvent, id: ShortcutId): void {
|
||||
const combo = comboFromEvent(event, IS_MAC);
|
||||
if (combo && isReserved(combo, IS_MAC)) {
|
||||
this.capturing.set(null);
|
||||
this.note.set({ id, kind: 'reserved', combo });
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.repeat || !combo) return;
|
||||
const bare = !combo.mod && !combo.ctrl && !combo.alt && !combo.shift;
|
||||
this.capturing.set(null);
|
||||
if (bare && combo.key === 'Escape') return;
|
||||
const binding = this.shortcuts.binding(id);
|
||||
if (!binding || binding.combos.some((c) => sameCombo(c, combo))) return;
|
||||
// the editor's text area always has focus: a plain key could never fire there
|
||||
if (binding.scopes.includes('editor') && !firesInTextField(combo, IS_MAC)) {
|
||||
this.note.set({ id, kind: 'needsModifier', combo });
|
||||
return;
|
||||
}
|
||||
const owner = ownerOf(this.shortcuts.keymap(), combo, binding.scopes, id);
|
||||
if (owner) {
|
||||
this.note.set({ id, kind: 'conflict', combo, owner: owner.id });
|
||||
return;
|
||||
}
|
||||
this.shortcuts.rebind({ [id]: [...binding.combos, combo] });
|
||||
}
|
||||
|
||||
/** "Replace": the key moves from the command that had it to this one. */
|
||||
replaceConflict(): void {
|
||||
const note = this.note();
|
||||
const binding = note && this.shortcuts.binding(note.id);
|
||||
const owner = note?.owner && this.shortcuts.binding(note.owner);
|
||||
if (!note || !binding || !owner) return;
|
||||
this.shortcuts.rebind({
|
||||
[note.id]: [...binding.combos, note.combo],
|
||||
[owner.id]: owner.combos.filter((c) => !sameCombo(c, note.combo)),
|
||||
});
|
||||
this.note.set(null);
|
||||
}
|
||||
|
||||
removeCombo(binding: ResolvedBinding, index: number): void {
|
||||
this.shortcuts.rebind({ [binding.id]: binding.combos.filter((_, i) => i !== index) });
|
||||
}
|
||||
|
||||
resetRow(binding: ResolvedBinding): void {
|
||||
this.shortcuts.rebind({ [binding.id]: binding.defaults });
|
||||
if (this.note()?.id === binding.id) this.note.set(null);
|
||||
}
|
||||
|
||||
resetAll(): void {
|
||||
this.shortcuts.resetAll();
|
||||
this.confirmResetAll.set(false);
|
||||
this.note.set(null);
|
||||
}
|
||||
|
||||
// ---- window ----
|
||||
|
||||
/** Recording first; then this window's keys (close-window, the scale) and
|
||||
* Tab, walked by hand — WKWebView skips buttons on Tab otherwise. */
|
||||
onWindowKeydown(event: KeyboardEvent): void {
|
||||
const recording = this.capturing();
|
||||
if (recording) {
|
||||
this.record(event, recording);
|
||||
return;
|
||||
}
|
||||
const id = this.shortcuts.claim(event, 'settings');
|
||||
if (id === 'close-window') void this.rpc.request.settingsClose({});
|
||||
else if (id === 'scale-up') this.settings.setUiScale(this.settings.uiScale() + 0.1);
|
||||
else if (id === 'scale-down') this.settings.setUiScale(this.settings.uiScale() - 0.1);
|
||||
else if (id === 'scale-reset') this.settings.setUiScale(1);
|
||||
else if (!id) cycleFocus(document.body, event);
|
||||
blockReloadKeys(event);
|
||||
}
|
||||
|
||||
/** A click anywhere but a record button ends a recording. */
|
||||
onWindowMousedown(event: MouseEvent): void {
|
||||
if (this.capturing() && !(event.target as Element).closest('[data-record]')) {
|
||||
this.capturing.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
blockContextMenu(event: Event): void {
|
||||
blockNativeContextMenu(event);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user