Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
384b5e0
Add configurable terminal keybindings loaded from ~/.t3
juliusmarminge Feb 15, 2026
ce4ad7e
Rename terminal-shortcuts module to keybindings
juliusmarminge Feb 15, 2026
68f3184
Align web keybinding types with shared contract aliases
juliusmarminge Feb 15, 2026
9cebeee
Extract keybindings contracts into dedicated module
juliusmarminge Feb 15, 2026
0707d4a
Resolve terminal keybindings on server before sending config
juliusmarminge Feb 15, 2026
ea140aa
Add source links to keybinding schema references
juliusmarminge Feb 15, 2026
e94a47a
Load terminal keybindings via React Query server config helper
juliusmarminge Feb 15, 2026
5fac591
Add configurable shortcuts for new chat and open-in-editor
juliusmarminge Feb 15, 2026
1c80b4f
Resolve keybinding precedence across commands
juliusmarminge Feb 15, 2026
c70e2d5
Refactor Sidebar component to streamline keybindings handling
juliusmarminge Feb 15, 2026
be32ce9
Harden keybinding parsing and config resolution
juliusmarminge Feb 15, 2026
b81b561
Use LRU cache for keybinding when-expression AST parsing
juliusmarminge Feb 15, 2026
5a1a5de
Resolve and validate keybindings on server before sending to web
juliusmarminge Feb 15, 2026
3b2f559
Inline keybinding when AST type and update keybindings tests
juliusmarminge Feb 15, 2026
3e1bfcb
Use Vitest assert API in keybindings tests
juliusmarminge Feb 15, 2026
514570a
Simplify resolved keybinding payload and remove unused LRU cache
juliusmarminge Feb 15, 2026
e1660c6
Extract keybindings config loading into keybindings module
juliusmarminge Feb 15, 2026
b8dfe54
Harden keybinding schemas and align tests with server defaults
juliusmarminge Feb 15, 2026
aca3edd
update doc
juliusmarminge Feb 15, 2026
367d4f3
rm
juliusmarminge Feb 15, 2026
0cd10a9
tidy up
juliusmarminge Feb 15, 2026
6de0ca3
nit
juliusmarminge Feb 15, 2026
00d7519
change defailt for new termial
juliusmarminge Feb 15, 2026
59b1d5d
Prevent config refetches and tighten Windows platform detection
juliusmarminge Feb 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions KEYBINDINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Keybindings

T3 Code reads keybindings from:

- `~/.t3/keybindings.json`

The file must be a JSON array of rules:

```json
[
{ "key": "mod+g", "command": "terminal.toggle" },
{ "key": "mod+shift+g", "command": "terminal.new", "when": "terminalFocus" }
]
```

See the full schema for more details: [`packages/contracts/src/keybindings.ts`](packages/contracts/src/keybindings.ts)

## Defaults

```json
[
{ "key": "mod+j", "command": "terminal.toggle" },
{ "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" },
{ "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" },
{ "key": "mod+shift+o", "command": "chat.new" },
{ "key": "mod+o", "command": "editor.openFavorite" }
]
```

For most up to date defaults, see [`DEFAULT_KEYBINDINGS` in `apps/server/src/keybindings.ts`](apps/server/src/keybindings.ts)

## Configuration

### Rule Shape

Each entry supports:

- `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d`
- `command` (required): action ID
- `when` (optional): boolean expression controlling when the shortcut is active

Invalid rules are ignored. Invalid config files are ignored. Warnings are logged by the server.

### Available Commands

- `terminal.toggle`: open/close terminal drawer
- `terminal.split`: split terminal (in focused terminal context by default)
- `terminal.new`: create new terminal (in focused terminal context by default)
- `chat.new`: create a new chat thread for the active project
- `editor.openFavorite`: open current project/worktree in the last-used editor

### Key Syntax

Supported modifiers:

- `mod` (`cmd` on macOS, `ctrl` on non-macOS)
- `cmd` / `meta`
- `ctrl` / `control`
- `shift`
- `alt` / `option`

Examples:

- `mod+j`
- `mod+shift+d`
- `ctrl+l`
- `cmd+k`

### `when` Conditions

Currently available context keys:

- `terminalFocus`
- `terminalOpen`

Supported operators:

- `!` (not)
- `&&` (and)
- `||` (or)
- parentheses: `(` `)`

Examples:

- `"when": "terminalFocus"`
- `"when": "terminalOpen && !terminalFocus"`
- `"when": "terminalFocus || terminalOpen"`

Unknown condition keys evaluate to `false`.

### Precedence

- Rules are evaluated in array order.
- For a key event, the last rule where both `key` matches and `when` evaluates to `true` wins.
- That means precedence is across commands, not only within the same command.
69 changes: 69 additions & 0 deletions apps/server/src/keybindings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { assert, describe, it } from "vitest";

import { compileResolvedKeybindingRule, parseKeybindingShortcut } from "./keybindings";

describe("server keybindings", () => {
it("parses shortcuts including plus key", () => {
assert.deepEqual(parseKeybindingShortcut("mod+j"), {
key: "j",
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false,
modKey: true,
});
assert.deepEqual(parseKeybindingShortcut("mod++"), {
key: "+",
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false,
modKey: true,
});
});

it("compiles valid rule with parsed when AST", () => {
const compiled = compileResolvedKeybindingRule({
key: "mod+d",
command: "terminal.split",
when: "terminalOpen && !terminalFocus",
});

assert.deepEqual(compiled, {
command: "terminal.split",
shortcut: {
key: "d",
metaKey: false,
ctrlKey: false,
shiftKey: false,
altKey: false,
modKey: true,
},
whenAst: {
type: "and",
left: { type: "identifier", name: "terminalOpen" },
right: {
type: "not",
node: { type: "identifier", name: "terminalFocus" },
},
},
});
});

it("rejects invalid rules", () => {
assert.isNull(
compileResolvedKeybindingRule({
key: "mod+shift+d+o",
command: "terminal.new",
}),
);

assert.isNull(
compileResolvedKeybindingRule({
key: "mod+d",
command: "terminal.split",
when: "terminalFocus && (",
}),
);
});
});
Loading