# Files changed · PR \#42 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/42/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[Improve terminal navigation shortcuts and streamline git action PR flow](https://git.cafe/pingdotgg/t3code/pull/42.md)

Version: 1

Head commit: b7bbcb205409bc0b4855f1b825941de99dd2e461

Base commit: 748ea2ae205122f8571287273599c5767bff1d55

Comparison base: 748ea2ae205122f8571287273599c5767bff1d55

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/web/src/components/ThreadTerminalDrawer.tsx

Status: modified; +16; −5

```diff
@@ -18,8 +18,11 @@
   preferredTerminalEditor,
   resolvePathLinkTarget,
 } from "../terminal-links";
-import { isTerminalClearShortcut } from "../keybindings";
 import {
+  isTerminalClearShortcut,
+  terminalNavigationShortcutData,
+} from "../keybindings";
+import {
   DEFAULT_THREAD_TERMINAL_HEIGHT,
   DEFAULT_THREAD_TERMINAL_ID,
   type ThreadTerminalGroup,
@@ -152,24 +155,32 @@
     terminalRef.current = terminal;
     fitAddonRef.current = fitAddon;
 
+    const sendTerminalInput = async (data: string, fallbackError: string) => {
-    const sendClearShortcut = async () => {
       const activeTerminal = terminalRef.current;
       if (!activeTerminal) return;
       try {
+        await api.terminal.write({ threadId, terminalId, data });
-        await api.terminal.write({ threadId, terminalId, data: "\u000c" });
       } catch (error) {
         writeSystemMessage(
           activeTerminal,
-          error instanceof Error ? error.message : "Failed to clear terminal",
+          error instanceof Error ? error.message : fallbackError,
         );
       }
     };
 
     terminal.attachCustomKeyEventHandler((event) => {
+      const navigationData = terminalNavigationShortcutData(event);
+      if (navigationData !== null) {
+        event.preventDefault();
+        event.stopPropagation();
+        void sendTerminalInput(navigationData, "Failed to move cursor");
+        return false;
+      }
+
       if (!isTerminalClearShortcut(event)) return true;
       event.preventDefault();
       event.stopPropagation();
-      void sendClearShortcut();
+      void sendTerminalInput("\u000c", "Failed to clear terminal");
       return false;
     });
 
```

## apps/web/src/keybindings.test.ts

Status: modified; +60; −0

```diff
@@ -15,6 +15,7 @@
   isTerminalSplitShortcut,
   isTerminalToggleShortcut,
   shortcutLabelForCommand,
+  terminalNavigationShortcutData,
   type ShortcutEventLike,
 } from "./keybindings";
 
@@ -324,8 +325,67 @@
   it("matches Cmd+K on macOS", () => {
     assert.isTrue(isTerminalClearShortcut(event({ key: "k", metaKey: true }), "MacIntel"));
   });
+
+  it("ignores non-keydown events", () => {
+    assert.isFalse(isTerminalClearShortcut(event({ type: "keyup", key: "l", ctrlKey: true }), "Linux"));
+  });
 });
 
+describe("terminalNavigationShortcutData", () => {
+  it("maps Option+Arrow on macOS to word movement", () => {
+    assert.strictEqual(
+      terminalNavigationShortcutData(event({ key: "ArrowLeft", altKey: true }), "MacIntel"),
+      "\u001bb",
+    );
+    assert.strictEqual(
+      terminalNavigationShortcutData(event({ key: "ArrowRight", altKey: true }), "MacIntel"),
+      "\u001bf",
+    );
+  });
+
+  it("maps Cmd+Arrow on macOS to line movement", () => {
+    assert.strictEqual(
+      terminalNavigationShortcutData(event({ key: "ArrowLeft", metaKey: true }), "MacIntel"),
+      "\u0001",
+    );
+    assert.strictEqual(
+      terminalNavigationShortcutData(event({ key: "ArrowRight", metaKey: true }), "MacIntel"),
+      "\u0005",
+    );
+  });
+
+  it("maps Ctrl+Arrow on non-macOS to word movement", () => {
+    assert.strictEqual(
+      terminalNavigationShortcutData(event({ key: "ArrowLeft", ctrlKey: true }), "Win32"),
+      "\u001bb",
+    );
+    assert.strictEqual(
+      terminalNavigationShortcutData(event({ key: "ArrowRight", ctrlKey: true }), "Linux"),
+      "\u001bf",
+    );
+  });
+
+  it("rejects unsupported combinations", () => {
+    assert.isNull(
+      terminalNavigationShortcutData(
+        event({ key: "ArrowLeft", shiftKey: true, altKey: true }),
+        "MacIntel",
+      ),
+    );
+    assert.isNull(terminalNavigationShortcutData(event({ key: "ArrowLeft", metaKey: true }), "Linux"));
+    assert.isNull(terminalNavigationShortcutData(event({ key: "a", altKey: true }), "MacIntel"));
+  });
+
+  it("ignores non-keydown events", () => {
+    assert.isNull(
+      terminalNavigationShortcutData(
+        event({ type: "keyup", key: "ArrowLeft", altKey: true }),
+        "MacIntel",
+      ),
+    );
+  });
+});
+
 describe("plus key parsing", () => {
   it("matches the plus key shortcut", () => {
     const plusBindings = compile([{ shortcut: modShortcut("+"), command: "terminal.toggle" }]);
```

## apps/web/src/keybindings.ts

Status: modified; +49; −0

```diff
@@ -7,6 +7,7 @@
 import { isMacPlatform } from "./lib/utils";
 
 export interface ShortcutEventLike {
+  type?: string;
   key: string;
   metaKey: boolean;
   ctrlKey: boolean;
@@ -25,6 +26,11 @@
   context?: Partial<ShortcutMatchContext>;
 }
 
+const TERMINAL_WORD_BACKWARD = "\u001bb";
+const TERMINAL_WORD_FORWARD = "\u001bf";
+const TERMINAL_LINE_START = "\u0001";
+const TERMINAL_LINE_END = "\u0005";
+
 function normalizeEventKey(key: string): string {
   const normalized = key.toLowerCase();
   if (normalized === "esc") return "escape";
@@ -204,6 +210,10 @@
   event: ShortcutEventLike,
   platform = navigator.platform,
 ): boolean {
+  if (event.type !== undefined && event.type !== "keydown") {
+    return false;
+  }
+
   const key = event.key.toLowerCase();
 
   if (key === "l" && event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
@@ -219,3 +229,42 @@
     !event.shiftKey
   );
 }
+
+export function terminalNavigationShortcutData(
+  event: ShortcutEventLike,
+  platform = navigator.platform,
+): string | null {
+  if (event.type !== undefined && event.type !== "keydown") {
+    return null;
+  }
+
+  if (event.shiftKey) return null;
+
+  const key = normalizeEventKey(event.key);
+  if (key !== "arrowleft" && key !== "arrowright") {
+    return null;
+  }
+
+  const moveWord = key === "arrowleft" ? TERMINAL_WORD_BACKWARD : TERMINAL_WORD_FORWARD;
+  const moveLine = key === "arrowleft" ? TERMINAL_LINE_START : TERMINAL_LINE_END;
+
+  if (isMacPlatform(platform)) {
+    if (event.altKey && !event.metaKey && !event.ctrlKey) {
+      return moveWord;
+    }
+    if (event.metaKey && !event.altKey && !event.ctrlKey) {
+      return moveLine;
+    }
+    return null;
+  }
+
+  if (event.ctrlKey && !event.metaKey && !event.altKey) {
+    return moveWord;
+  }
+
+  if (event.altKey && !event.metaKey && !event.ctrlKey) {
+    return moveWord;
+  }
+
+  return null;
+}
```
