From 9949d0711e4269a1d8ae33f01c2c7f5917d181f4 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 16 Oct 2026 23:28:24 -0500 Subject: [PATCH] fix: move macOS split pane navigation to Cmd+Option+Arrow --- CHANGELOG.md | 1 + src/ui/AppShell.tsx | 20 +++------ src/ui/lib/pane-nav-hotkey.ts | 25 +++++++++++ src/ui/shell/split/SplitLayoutMenu.tsx | 3 +- src/ui/tests/lib/pane-nav-hotkey.test.ts | 54 ++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 src/ui/lib/pane-nav-hotkey.ts create mode 100644 src/ui/tests/lib/pane-nav-hotkey.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d51093674..1fd669051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - The Docker Compose files no longer start a guacd container. npm run dev -- --guacd also points remote desktop at the guacd it starts - The side menu now opens over the page when it expands on hover, so open sessions like remote desktop are not resized each time. Turn on Push content when it expands in Settings to get the old behavior back +- On macOS, moving between split panes is now Cmd+Option+Arrow, so Option+Arrow can move the cursor by word in a terminal ### Fixed diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index 2a2f0075d..f546042da 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -125,6 +125,7 @@ import type { } from "@/types/ui-types"; import { applyAccentColor, applyFontSize } from "@/lib/theme"; import { globalShortcutHandler } from "@/lib/global-shortcut-handler"; +import { getPaneNavDirection } from "@/lib/pane-nav-hotkey"; import { getTabJumpDigit } from "@/lib/tab-jump-hotkey"; import { useTheme } from "@/components/theme-provider"; import { @@ -829,20 +830,11 @@ export function AppShell({ } } - // Alt+Arrow moves between panes of the active split - if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) { - const direction = ( - { - ArrowLeft: "left", - ArrowRight: "right", - ArrowUp: "up", - ArrowDown: "down", - } as Record - )[e.code]; - if (direction && splitActionsRef.current?.navigate(direction)) { - e.preventDefault(); - return; - } + // Alt+Arrow (Cmd+Option+Arrow on macOS) moves between panes of the active split + const paneDirection = getPaneNavDirection(e); + if (paneDirection && splitActionsRef.current?.navigate(paneDirection)) { + e.preventDefault(); + return; } // Cmd+1..9 on macOS, Alt+1..9 elsewhere: jump directly to the tab at that position diff --git a/src/ui/lib/pane-nav-hotkey.ts b/src/ui/lib/pane-nav-hotkey.ts new file mode 100644 index 000000000..051ba5ba1 --- /dev/null +++ b/src/ui/lib/pane-nav-hotkey.ts @@ -0,0 +1,25 @@ +import { isMacPlatform } from "./tab-jump-hotkey"; + +export type PaneNavDirection = "left" | "right" | "up" | "down"; + +const ARROWS: Record = { + ArrowLeft: "left", + ArrowRight: "right", + ArrowUp: "up", + ArrowDown: "down", +}; + +/** + * The split pane to move to for this key, if any. Cmd+Option+Arrow on macOS, + * where Option+Arrow moves the shell cursor by word, Alt+Arrow elsewhere. + */ +export function getPaneNavDirection(e: KeyboardEvent): PaneNavDirection | null { + const direction = ARROWS[e.code]; + if (!direction || e.ctrlKey || e.shiftKey || !e.altKey) return null; + if (e.metaKey !== isMacPlatform()) return null; + return direction; +} + +export function paneNavHotkeyLabel(): string { + return isMacPlatform() ? "Cmd+Option+Arrows" : "Alt+Arrows"; +} diff --git a/src/ui/shell/split/SplitLayoutMenu.tsx b/src/ui/shell/split/SplitLayoutMenu.tsx index b2cb4c1df..3ef8a057d 100644 --- a/src/ui/shell/split/SplitLayoutMenu.tsx +++ b/src/ui/shell/split/SplitLayoutMenu.tsx @@ -18,6 +18,7 @@ import { type SplitPresetId, } from "./split-presets"; import { MAX_PANES, type PaneEdge } from "./split-tree"; +import { paneNavHotkeyLabel } from "@/lib/pane-nav-hotkey"; function NodeThumb({ node }: { node: LayoutNode }) { if (node.kind === "pane") { @@ -129,7 +130,7 @@ export function SplitLayoutMenu({
{t("splitScreen.shortcutNavigate")} - Alt+Arrows + {paneNavHotkeyLabel()}
{t("splitScreen.zoom")} diff --git a/src/ui/tests/lib/pane-nav-hotkey.test.ts b/src/ui/tests/lib/pane-nav-hotkey.test.ts new file mode 100644 index 000000000..d5d23df15 --- /dev/null +++ b/src/ui/tests/lib/pane-nav-hotkey.test.ts @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + getPaneNavDirection, + paneNavHotkeyLabel, +} from "../../lib/pane-nav-hotkey"; + +function keyEvent(init: Partial): KeyboardEvent { + return { + code: "ArrowLeft", + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + ...init, + } as KeyboardEvent; +} + +afterEach(() => vi.unstubAllGlobals()); + +describe("pane-nav-hotkey", () => { + it("uses Alt+Arrow outside macOS", () => { + vi.stubGlobal("navigator", { platform: "Win32" }); + expect(getPaneNavDirection(keyEvent({ altKey: true }))).toBe("left"); + expect( + getPaneNavDirection(keyEvent({ code: "ArrowDown", altKey: true })), + ).toBe("down"); + expect( + getPaneNavDirection(keyEvent({ altKey: true, metaKey: true })), + ).toBeNull(); + expect(paneNavHotkeyLabel()).toBe("Alt+Arrows"); + }); + + it("leaves Option+Arrow to the shell on macOS", () => { + vi.stubGlobal("navigator", { platform: "MacIntel" }); + expect(getPaneNavDirection(keyEvent({ altKey: true }))).toBeNull(); + expect( + getPaneNavDirection( + keyEvent({ code: "ArrowRight", altKey: true, metaKey: true }), + ), + ).toBe("right"); + expect(paneNavHotkeyLabel()).toBe("Cmd+Option+Arrows"); + }); + + it("ignores other keys and modifiers", () => { + vi.stubGlobal("navigator", { platform: "Win32" }); + expect( + getPaneNavDirection(keyEvent({ code: "KeyA", altKey: true })), + ).toBeNull(); + expect( + getPaneNavDirection(keyEvent({ altKey: true, shiftKey: true })), + ).toBeNull(); + expect(getPaneNavDirection(keyEvent({}))).toBeNull(); + }); +});