From e28314054f8b2baa369edc8d0935833b4e804007 Mon Sep 17 00:00:00 2001 From: Jokin Date: Mon, 5 Oct 2026 14:59:37 +0200 Subject: [PATCH] Drive the connection manager from the keyboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manager had only Esc and the scale keys: its list took one Tab stop per row with no arrows, a stack's rename / delete / open-in-tabs buttons existed only on hover, and no action had a key. - Commands (the manager keymap scope, remappable like the rest): new connection with focus in Name, new stack, save, connect — or open the focused stack in tabs — delete the focused row, rename the focused stack, import from WinSCP, close the window. - The list: up/down/Home/End move focus between rows (focus only: loading a connection would discard a half-edited form), Enter opens the focused connection, left/right collapse and expand a stack, and a stack's actions show while the keyboard is in its row. - Enter in a form field connects; Esc leaves a field before it closes the window; Tab is walked by hand (WKWebView skips buttons and radios); focus comes back after a dialog and never stays lost on . - Tooltips show the keys; the Settings → Keyboard fixed-key list gains the manager's. Co-Authored-By: Claude --- docs/architecture.md | 14 ++ docs/gotchas.md | 4 + src/shared/i18n/de.ts | 1 + src/shared/i18n/en.ts | 1 + src/shared/i18n/es.ts | 1 + src/shared/i18n/eu.ts | 1 + src/shared/i18n/fr.ts | 1 + src/shared/i18n/ja.ts | 1 + src/shared/i18n/zh.ts | 1 + .../connection-manager.html | 41 +++- .../connection-manager/connection-manager.ts | 222 ++++++++++++++++-- .../settings-window/settings-window.ts | 2 +- 12 files changed, 262 insertions(+), 28 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 8d20a9c..b64849f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -121,6 +121,20 @@ main's `ConnectionsService` reloads; a reload-retry in `App.findConnection` covers the save→connect race. `setAlwaysOnTop` is deliberately NOT used (it would float over editor windows). +Keyboard (the `manager` keymap scope): ⌘N new connection (focus lands in +Name), ⇧⌘N new stack, ⌘S save, ⌘↩ connect — or, on a focused stack row, open +it in tabs — ⌘⌫ / Del delete the focused row (with a confirm), F2 rename the +focused stack, ⌘O import, ⌘W close. The list's rows are real buttons +(`data-row`; `data-stack` / `data-conn` say what they are): ↑ ↓ Home End move +FOCUS only — loading a connection would replace a half-edited form without +asking — Enter opens the focused connection in the form, ← / → collapse and +expand a stack (← on a connection climbs to its stack), and a stack's +hover-only actions also show while the keyboard is in its row. Enter in a form +field = Connect; Esc leaves a field first, then closes the window. Focus +returns where it was after a dialog (`focusin` memory + the FocusService +tick) and falls back to the edited connection's row whenever it drops to +``. + ### Settings window (`src/bun/settings-window.ts` + `ui/src/app/skins/shadlike/settings-window/`) Single instance, centered over main, **not modal**: every control is a plain diff --git a/docs/gotchas.md b/docs/gotchas.md index fdb57a7..a9a6799 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -299,6 +299,10 @@ touching the adjacent code. 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. +- **In a master-detail list, selection must not follow focus** when the detail + is an unsaved form: the connection manager's `edit()` replaces the draft + without asking, so arrowing through the list would silently discard edits. + Arrows move focus; Enter loads. - **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. diff --git a/src/shared/i18n/de.ts b/src/shared/i18n/de.ts index ee90d44..536c92c 100644 --- a/src/shared/i18n/de.ts +++ b/src/shared/i18n/de.ts @@ -395,6 +395,7 @@ export const de = { 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", + manager: "↑ ↓ ← → und {enter} bewegen sich durch die Verbindungsliste", sidebar: "↑ ↓ wechseln hier zwischen den Bereichen", }, names: { diff --git a/src/shared/i18n/en.ts b/src/shared/i18n/en.ts index b1e7ccf..d4ab021 100644 --- a/src/shared/i18n/en.ts +++ b/src/shared/i18n/en.ts @@ -403,6 +403,7 @@ export const en = { 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", + manager: "↑ ↓ ← → and {enter} move through the connection list", sidebar: "↑ ↓ move between these sections", }, // key names for "Ctrl+Shift+Del"-style hints (not used on macOS, which shows ⌘⇧⌫) diff --git a/src/shared/i18n/es.ts b/src/shared/i18n/es.ts index 1e37747..e7a7799 100644 --- a/src/shared/i18n/es.ts +++ b/src/shared/i18n/es.ts @@ -395,6 +395,7 @@ export const es = { 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", + manager: "↑ ↓ ← → y {enter} recorren la lista de conexiones", sidebar: "↑ ↓ cambian de sección aquí", }, names: { diff --git a/src/shared/i18n/eu.ts b/src/shared/i18n/eu.ts index 6156601..dc15e08 100644 --- a/src/shared/i18n/eu.ts +++ b/src/shared/i18n/eu.ts @@ -397,6 +397,7 @@ export const eu = { 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", + manager: "↑ ↓ ← → eta {enter} konexio-zerrendan mugitzen dira", sidebar: "↑ ↓ atal batetik bestera mugitzen dira hemen", }, names: { diff --git a/src/shared/i18n/fr.ts b/src/shared/i18n/fr.ts index 026bf54..94d54a1 100644 --- a/src/shared/i18n/fr.ts +++ b/src/shared/i18n/fr.ts @@ -395,6 +395,7 @@ export const fr = { 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", + manager: "↑ ↓ ← → et {enter} parcourent la liste des connexions", sidebar: "↑ ↓ changent de section ici", }, names: { diff --git a/src/shared/i18n/ja.ts b/src/shared/i18n/ja.ts index 5ce6abe..fe861f5 100644 --- a/src/shared/i18n/ja.ts +++ b/src/shared/i18n/ja.ts @@ -395,6 +395,7 @@ export const ja = { filter: "ファイル一覧で入力すると絞り込まれます。{backspace} で最後の文字を消します", shift: "Shift と移動キーで、カーソルが通過した行を選択します", find: "{enter} と {shiftEnter} で検索結果を移動します", + manager: "↑ ↓ ← → と {enter} で接続一覧を移動します", sidebar: "↑ ↓ でここのセクションを切り替えます", }, names: { diff --git a/src/shared/i18n/zh.ts b/src/shared/i18n/zh.ts index 53c34a8..6caae27 100644 --- a/src/shared/i18n/zh.ts +++ b/src/shared/i18n/zh.ts @@ -395,6 +395,7 @@ export const zh = { filter: "在文件列表中输入即可筛选;{backspace} 删除筛选的最后一个字符", shift: "Shift 加移动键会选中光标经过的行", find: "{enter} 和 {shiftEnter} 在查找结果间跳转", + manager: "↑ ↓ ← → 和 {enter} 在连接列表中移动", sidebar: "↑ ↓ 在这些分区间切换", }, names: { diff --git a/ui/src/app/skins/shadlike/connection-manager/connection-manager.html b/ui/src/app/skins/shadlike/connection-manager/connection-manager.html index 68fd1f1..78bb44b 100644 --- a/ui/src/app/skins/shadlike/connection-manager/connection-manager.html +++ b/ui/src/app/skins/shadlike/connection-manager/connection-manager.html @@ -15,7 +15,7 @@ -
+ +
@for (group of store.grouped().stacks; track group.stack.id) {
-
+