Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4c72577b9 | ||
|
|
aed9a5eec0 |
@@ -48,6 +48,10 @@ const DEFAULT_NORMAL: &[(&str, &str)] = &[
|
|||||||
("c", "ToggleSidebarGroup"),
|
("c", "ToggleSidebarGroup"),
|
||||||
// Catalog actions
|
// Catalog actions
|
||||||
("enter", "AttachHighlighted"),
|
("enter", "AttachHighlighted"),
|
||||||
|
// Tab focuses the highlighted pane, mirroring the web keybind — bare
|
||||||
|
// because Normal mode only exists while the sidebar is focused, so
|
||||||
|
// nothing forwards it to a pty.
|
||||||
|
("tab", "AttachHighlighted"),
|
||||||
("s", "OpenSpawnModal"),
|
("s", "OpenSpawnModal"),
|
||||||
// Fast-forward spawn (#67): the spawn key's neighbour does the same
|
// Fast-forward spawn (#67): the spawn key's neighbour does the same
|
||||||
// thing with every default resolved and no form.
|
// thing with every default resolved and no form.
|
||||||
@@ -63,14 +67,17 @@ const DEFAULT_NORMAL: &[(&str, &str)] = &[
|
|||||||
("ctrl-w b", "ToggleSidebar"),
|
("ctrl-w b", "ToggleSidebar"),
|
||||||
("?", "ShowHelp"),
|
("?", "ShowHelp"),
|
||||||
("ctrl-w q", "Quit"),
|
("ctrl-w q", "Quit"),
|
||||||
|
// Bare quit, AoE-style: only reachable from the sidebar (Normal mode
|
||||||
|
// never forwards keys to a pty), same reasoning as bare `tab` above.
|
||||||
|
("q", "Quit"),
|
||||||
// Mouse
|
// Mouse
|
||||||
("mouse:dblclick(sidebar-row)", "AttachHighlighted"),
|
("mouse:dblclick(sidebar-row)", "AttachHighlighted"),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Default Insert-mode bindings. Empty by default — the only Insert binding
|
/// Default Insert-mode bindings. The leader (`LeaveInsert`) is inserted
|
||||||
/// is the leader, which is inserted dynamically by [`TuiConfig::load`] so
|
/// dynamically by [`TuiConfig::load`] so rebinding it rebinds its Insert
|
||||||
/// rebinding the leader rebinds its Insert escape automatically.
|
/// escape automatically; `esc` is a second, fixed way back to the sidebar.
|
||||||
const DEFAULT_INSERT: &[(&str, &str)] = &[];
|
const DEFAULT_INSERT: &[(&str, &str)] = &[("esc", "LeaveInsert")];
|
||||||
|
|
||||||
impl TuiConfig {
|
impl TuiConfig {
|
||||||
/// Build a runtime config from a [`TuiSection`] (typically the `tui`
|
/// Build a runtime config from a [`TuiSection`] (typically the `tui`
|
||||||
@@ -222,6 +229,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AoE-style defaults (#shortcuts): `q` and `tab` are bare because Normal
|
||||||
|
/// mode only exists while the sidebar is focused, so neither can leak
|
||||||
|
/// into a pty; `esc` is a second way out of Insert alongside the leader.
|
||||||
|
#[test]
|
||||||
|
fn aoe_style_defaults_are_bound() {
|
||||||
|
let cfg = TuiConfig::load(&TuiSection::default()).unwrap();
|
||||||
|
assert_eq!(cfg.keymap.resolve(Mode::Normal, "q"), Some(Action::Quit));
|
||||||
|
assert_eq!(
|
||||||
|
cfg.keymap.resolve(Mode::Normal, "tab"),
|
||||||
|
Some(Action::AttachHighlighted),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.keymap.resolve(Mode::Insert, "esc"),
|
||||||
|
Some(Action::LeaveInsert),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_leader_is_ctrl_a_and_escapes_insert() {
|
fn default_leader_is_ctrl_a_and_escapes_insert() {
|
||||||
let cfg = TuiConfig::load(&TuiSection::default()).unwrap();
|
let cfg = TuiConfig::load(&TuiSection::default()).unwrap();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Terminal } from "@xterm/xterm";
|
|||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
import { attachWsUrl } from "./routes.js";
|
import { attachWsUrl } from "./routes.js";
|
||||||
|
import { FOCUS_SIDEBAR_EVENT } from "./catalog.js";
|
||||||
import { escapeHtml } from "./html.js";
|
import { escapeHtml } from "./html.js";
|
||||||
import { INITIAL_RECONNECT_MS, MAX_RECONNECT_MS } from "./reconnect.js";
|
import { INITIAL_RECONNECT_MS, MAX_RECONNECT_MS } from "./reconnect.js";
|
||||||
import { applyModsToChar, cursorSeq, tildeSeq } from "./keys.js";
|
import { applyModsToChar, cursorSeq, tildeSeq } from "./keys.js";
|
||||||
@@ -134,6 +135,15 @@ export function createAttachConnection(
|
|||||||
term.loadAddon(fit);
|
term.loadAddon(fit);
|
||||||
term.open(body);
|
term.open(body);
|
||||||
|
|
||||||
|
// Plain Escape hands the keyboard back to the session list instead of the
|
||||||
|
// shell/app. `false` tells xterm to skip its own handling of this event.
|
||||||
|
term.attachCustomKeyEventHandler((e) => {
|
||||||
|
if (e.type !== "keydown" || e.key !== "Escape") return true;
|
||||||
|
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return true;
|
||||||
|
document.dispatchEvent(new CustomEvent(FOCUS_SIDEBAR_EVENT));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
// Touch scrolling: xterm's built-in touch handling is jittery (drags crawl a
|
// Touch scrolling: xterm's built-in touch handling is jittery (drags crawl a
|
||||||
// line at a time and stall), so map finger drags ourselves. Two routes:
|
// line at a time and stall), so map finger drags ourselves. Two routes:
|
||||||
// • normal buffer → term.scrollLines walks the scrollback.
|
// • normal buffer → term.scrollLines walks the scrollback.
|
||||||
|
|||||||
+6
-2
@@ -36,10 +36,14 @@ export function requestOpenSpawn(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Ask the shell to hand the keyboard back to the focused pane's terminal —
|
/** Ask the shell to hand the keyboard back to the focused pane's terminal —
|
||||||
* what Escape in the sidebar list does. Handled in main.ts, which owns the
|
* what Tab in the sidebar list does. Handled in main.ts, which owns the
|
||||||
* connection pool. */
|
* connection pool. */
|
||||||
export const FOCUS_PANE_EVENT = "catalog-focus-pane";
|
export const FOCUS_PANE_EVENT = "catalog-focus-pane";
|
||||||
|
|
||||||
|
/** Ask the shell to hand the keyboard back to the session list — what Escape
|
||||||
|
* does when the pane's terminal is focused. Handled in main.ts. */
|
||||||
|
export const FOCUS_SIDEBAR_EVENT = "catalog-focus-sidebar";
|
||||||
|
|
||||||
/** Ask the shell to open the spawn modal seeded from an existing session
|
/** Ask the shell to open the spawn modal seeded from an existing session
|
||||||
* ("Spawn similar"). Detail is the session being cloned. */
|
* ("Spawn similar"). Detail is the session being cloned. */
|
||||||
export const SPAWN_SIMILAR_EVENT = "catalog-spawn-similar";
|
export const SPAWN_SIMILAR_EVENT = "catalog-spawn-similar";
|
||||||
@@ -568,7 +572,7 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S
|
|||||||
if (e.key === "j" || e.key === "ArrowDown") moveNav(1);
|
if (e.key === "j" || e.key === "ArrowDown") moveNav(1);
|
||||||
else if (e.key === "k" || e.key === "ArrowUp") moveNav(-1);
|
else if (e.key === "k" || e.key === "ArrowUp") moveNav(-1);
|
||||||
else if (e.key === "Enter") activateNav();
|
else if (e.key === "Enter") activateNav();
|
||||||
else if (e.key === "Escape") document.dispatchEvent(new CustomEvent(FOCUS_PANE_EVENT));
|
else if (e.key === "Tab") document.dispatchEvent(new CustomEvent(FOCUS_PANE_EVENT));
|
||||||
else return;
|
else return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -31,7 +31,8 @@ export const SHORTCUTS: ShortcutGroup[] = [
|
|||||||
{ keys: ["j"], description: "Highlight the next session" },
|
{ keys: ["j"], description: "Highlight the next session" },
|
||||||
{ keys: ["k"], description: "Highlight the previous session" },
|
{ keys: ["k"], description: "Highlight the previous session" },
|
||||||
{ keys: ["Enter"], description: "Open the highlighted session, or expand a folder" },
|
{ keys: ["Enter"], description: "Open the highlighted session, or expand a folder" },
|
||||||
{ keys: ["Esc"], description: "Hand the keyboard back to the pane" },
|
{ keys: ["Tab"], description: "Send the keyboard to the pane" },
|
||||||
|
{ keys: ["Esc"], description: "Hand the keyboard back to the session list, from the pane" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+7
-1
@@ -31,6 +31,7 @@ import { createMultiView, type MultiView } from "./multiview.js";
|
|||||||
import { loadLayout, saveLayout } from "./persistence.js";
|
import { loadLayout, saveLayout } from "./persistence.js";
|
||||||
import {
|
import {
|
||||||
FOCUS_PANE_EVENT,
|
FOCUS_PANE_EVENT,
|
||||||
|
FOCUS_SIDEBAR_EVENT,
|
||||||
OPEN_SPAWN_EVENT,
|
OPEN_SPAWN_EVENT,
|
||||||
SESSION_SELECT_EVENT,
|
SESSION_SELECT_EVENT,
|
||||||
SPAWN_SIMILAR_EVENT,
|
SPAWN_SIMILAR_EVENT,
|
||||||
@@ -372,8 +373,13 @@ window.addEventListener("keydown", (e) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Escape in the session list hands the keyboard back to the pane.
|
// Tab in the session list hands the keyboard to the pane; Escape in the pane
|
||||||
|
// hands it back — the pair the TUI gets from `tab` and `esc`.
|
||||||
document.addEventListener(FOCUS_PANE_EVENT, () => pool.focusTerminal());
|
document.addEventListener(FOCUS_PANE_EVENT, () => pool.focusTerminal());
|
||||||
|
document.addEventListener(FOCUS_SIDEBAR_EVENT, () => {
|
||||||
|
drawer.open(); // no-op on desktop; on mobile the list is off-canvas
|
||||||
|
sidebar.focusList();
|
||||||
|
});
|
||||||
|
|
||||||
/** Open/focus a session and reflect it in the shell chrome. Shared by the
|
/** Open/focus a session and reflect it in the shell chrome. Shared by the
|
||||||
* hash router and the sidebar's click-driven path (#133), which can't rely
|
* hash router and the sidebar's click-driven path (#133), which can't rely
|
||||||
|
|||||||
@@ -109,11 +109,11 @@ describe("sidebar keyboard navigation", () => {
|
|||||||
expect(handle.hasListFocus()).toBe(true);
|
expect(handle.hasListFocus()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Escape asks the shell for the pane back", () => {
|
it("Tab asks the shell for the pane back", () => {
|
||||||
const { el } = twoFolders();
|
const { el } = twoFolders();
|
||||||
let asked = 0;
|
let asked = 0;
|
||||||
document.addEventListener(FOCUS_PANE_EVENT, () => (asked += 1));
|
document.addEventListener(FOCUS_PANE_EVENT, () => (asked += 1));
|
||||||
press(tree(el), "Escape");
|
press(tree(el), "Tab");
|
||||||
expect(asked).toBe(1);
|
expect(asked).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user