authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-15 13:56:37-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-15 15:51:05-08:00
log3db020a683398511a169c411513a945a1cd2f384
treebad857551683630ee9baeab075a30757af72c260
parent20d3ad5d50ba9afb0df99bad1f71dddc762d2032

Simplify resolved keybinding payload and remove unused LRU cache

- drop `key` and `when` from resolved keybinding rule schema/payload - update server/web/contracts tests and keybindings docs for `whenAst`-only resolution - delete unused web `LruCache` implementation and tests

8 files changed, 7 insertions(+), 83 deletions(-)

KEYBINDINGS.md+7-1
......@@ -97,7 +97,13 @@ If you define any rules for a command, the default rule for that command is remo
9797
9898## Server-Resolved Payload
9999
100`~/.t3/keybindings.json` is parsed and compiled on the server. The web client receives resolved rules (including parsed shortcut/when fields) and does not parse raw keybinding strings.
100`~/.t3/keybindings.json` is parsed and compiled on the server. The web client receives resolved rules and does not parse raw keybinding strings.
101
102Resolved rule payload fields:
103
104- `command`
105- `shortcut`
106- `whenAst` (optional)
101107
102108See:
103109
apps/server/src/keybindings.test.ts-2
......@@ -30,9 +30,7 @@ describe("server keybindings", () => {
3030 });
3131
3232 assert.deepEqual(compiled, {
33 key: "mod+d",
3433 command: "terminal.split",
35 when: "terminalOpen && !terminalFocus",
3634 shortcut: {
3735 key: "d",
3836 metaKey: false,
apps/server/src/keybindings.ts-3
......@@ -216,16 +216,13 @@ export function compileResolvedKeybindingRule(
216216 const whenAst = parseKeybindingWhenExpression(when);
217217 if (!whenAst) return null;
218218 return {
219 key,
220219 command: rule.command,
221 when,
222220 shortcut,
223221 whenAst,
224222 };
225223 }
226224
227225 return {
228 key,
229226 command: rule.command,
230227 shortcut,
231228 };
apps/web/src/keybindings.test.ts-2
......@@ -125,7 +125,6 @@ function compile(bindings: TestBinding[]): ResolvedKeybindingsConfig {
125125 }
126126 if (binding.whenAst) {
127127 resolved.push({
128 key: binding.key.trim(),
129128 command: binding.command,
130129 shortcut: parsedShortcut,
131130 whenAst: binding.whenAst,
......@@ -133,7 +132,6 @@ function compile(bindings: TestBinding[]): ResolvedKeybindingsConfig {
133132 continue;
134133 }
135134 resolved.push({
136 key: binding.key.trim(),
137135 command: binding.command,
138136 shortcut: parsedShortcut,
139137 });
apps/web/src/lib/lru.test.ts deleted-33
......@@ -1,33 +0,0 @@
1import { assert, describe, it } from "vitest";
2import { LruCache } from "./lru";
3
4describe("LruCache", () => {
5 it("evicts the least recently used entry", () => {
6 const cache = new LruCache<string, number>(2);
7 cache.set("a", 1);
8 cache.set("b", 2);
9 assert.strictEqual(cache.get("a"), 1);
10
11 cache.set("c", 3);
12
13 assert.strictEqual(cache.get("b"), undefined);
14 assert.strictEqual(cache.get("a"), 1);
15 assert.strictEqual(cache.get("c"), 3);
16 });
17
18 it("refreshes recency on update", () => {
19 const cache = new LruCache<string, number>(2);
20 cache.set("a", 1);
21 cache.set("b", 2);
22 cache.set("a", 10);
23 cache.set("c", 3);
24
25 assert.strictEqual(cache.get("b"), undefined);
26 assert.strictEqual(cache.get("a"), 10);
27 assert.strictEqual(cache.get("c"), 3);
28 });
29
30 it("rejects non-positive capacities", () => {
31 assert.throws(() => new LruCache<string, number>(0), "positive integer");
32 });
33});
apps/web/src/lib/lru.ts deleted-38
......@@ -1,38 +0,0 @@
1export class LruCache<K, V> {
2 private readonly entries = new Map<K, V>();
3
4 constructor(private readonly maxEntries: number) {
5 if (!Number.isInteger(maxEntries) || maxEntries <= 0) {
6 throw new Error("LruCache maxEntries must be a positive integer.");
7 }
8 }
9
10 get size(): number {
11 return this.entries.size;
12 }
13
14 get(key: K): V | undefined {
15 const value = this.entries.get(key);
16 if (value === undefined && !this.entries.has(key)) {
17 return undefined;
18 }
19
20 // Refresh recency on successful lookup.
21 this.entries.delete(key);
22 this.entries.set(key, value as V);
23 return value;
24 }
25
26 set(key: K, value: V): void {
27 if (this.entries.has(key)) {
28 this.entries.delete(key);
29 }
30 this.entries.set(key, value);
31
32 while (this.entries.size > this.maxEntries) {
33 const oldest = this.entries.keys().next().value as K | undefined;
34 if (oldest === undefined) break;
35 this.entries.delete(oldest);
36 }
37 }
38}
packages/contracts/src/keybindings.test.ts-2
......@@ -37,9 +37,7 @@ describe("keybindings contracts", () => {
3737
3838 it("parses resolved keybinding rules", () => {
3939 const parsed = resolvedKeybindingRuleSchema.parse({
40 key: "mod+d",
4140 command: "terminal.split",
42 when: "terminalOpen && !terminalFocus",
4341 shortcut: {
4442 key: "d",
4543 metaKey: false,
packages/contracts/src/keybindings.ts-2
......@@ -58,9 +58,7 @@ export const keybindingWhenNodeSchema: z.ZodType<KeybindingWhenNode> = z.lazy(()
5858);
5959
6060export const resolvedKeybindingRuleSchema = z.object({
61 key: keybindingValueSchema,
6261 command: keybindingCommandSchema,
63 when: keybindingWhenSchema.optional(),
6462 shortcut: keybindingShortcutSchema,
6563 whenAst: keybindingWhenNodeSchema.optional(),
6664});