Skip to content
Source
Sign inSign up

keybindings.ts

280 lines7.7 KB

1import * as Schema from "effect/Schema";
2import { ForwardCompatibleArray, TrimmedString } from "./baseSchemas.ts";
3​
4const MAX_KEYBINDING_VALUE_LENGTH = 64;
5const MAX_KEYBINDING_WHEN_LENGTH = 256;
6export const MAX_WHEN_EXPRESSION_DEPTH = 64;
7export const MAX_SCRIPT_ID_LENGTH = 24;
8export const MAX_KEYBINDINGS_COUNT = 256;
9​
10export const THREAD_JUMP_KEYBINDING_COMMANDS = [
11 "thread.jump.1",
12 "thread.jump.2",
13 "thread.jump.3",
14 "thread.jump.4",
15 "thread.jump.5",
16 "thread.jump.6",
17 "thread.jump.7",
18 "thread.jump.8",
19 "thread.jump.9",
20] as const;
21export type ThreadJumpKeybindingCommand = (typeof THREAD_JUMP_KEYBINDING_COMMANDS)[number];
22​
23export const MODEL_PICKER_JUMP_KEYBINDING_COMMANDS = [
24 "modelPicker.jump.1",
25 "modelPicker.jump.2",
26 "modelPicker.jump.3",
27 "modelPicker.jump.4",
28 "modelPicker.jump.5",
29 "modelPicker.jump.6",
30 "modelPicker.jump.7",
31 "modelPicker.jump.8",
32 "modelPicker.jump.9",
33] as const;
34export type ModelPickerJumpKeybindingCommand =
35 (typeof MODEL_PICKER_JUMP_KEYBINDING_COMMANDS)[number];
36​
37const THREAD_KEYBINDING_COMMANDS = [
38 "thread.stop",
39 "thread.steerQueuedMessage",
40 "thread.editQueuedMessage",
41 "thread.previous",
42 "thread.next",
43 "thread.copyReference",
44 "thread.settle",
45 "thread.pin",
46 "thread.regenerateTitle",
47 "thread.undo",
48 ...THREAD_JUMP_KEYBINDING_COMMANDS,
49] as const;
50​
51const MODEL_PICKER_KEYBINDING_COMMANDS = [
52 "modelPicker.toggle",
53 "modelPicker.previousProvider",
54 "modelPicker.nextProvider",
55 ...MODEL_PICKER_JUMP_KEYBINDING_COMMANDS,
56] as const;
57​
58/**
59 * The layout keyboard map: cmd makes (split), alt+cmd looks (focus, fill,
60 * widths), ctrl+alt+cmd moves. Same map as niri, with alt+cmd as the
61 * modifier.
62 */
63export const LAYOUT_KEYBINDING_COMMANDS = [
64 "layout.splitRight",
65 "layout.splitDown",
66 "layout.fill",
67 "layout.focusLeft",
68 "layout.focusRight",
69 "layout.focusUp",
70 "layout.focusDown",
71 "layout.moveLeft",
72 "layout.moveRight",
73 "layout.moveUp",
74 "layout.moveDown",
75 "layout.focusFirstColumn",
76 "layout.focusLastColumn",
77 "layout.cycleColumnWidth",
78 "layout.centerColumn",
79 "layout.pullLeft",
80 "layout.pullRight",
81 "layout.shortcutSheet",
82 "layout.focusWorkspaceDown",
83 "layout.focusWorkspaceUp",
84 "layout.moveToWorkspaceDown",
85 "layout.moveToWorkspaceUp",
86 "layout.moveWorkspaceDown",
87 "layout.moveWorkspaceUp",
88 "layout.overview",
89] as const;
90export type LayoutKeybindingCommand = (typeof LAYOUT_KEYBINDING_COMMANDS)[number];
91​
92export const STATIC_KEYBINDING_COMMANDS = [
93 "workspace.focusNext",
94 "workspace.focusPrevious",
95 "sidebar.toggle",
96 "navigation.back",
97 "navigation.forward",
98 "terminal.toggle",
99 "terminal.split",
100 "terminal.splitVertical",
101 "terminal.new",
102 "terminal.close",
103 "rightPanel.toggle",
104 "threadPanel.toggle",
105 "rightPanel.toggleMaximized",
106 "rightPanel.close",
107 "view.reopenClosed",
108 "pullRequest.copyNumber",
109 "diff.toggle",
110 "pullRequests.toggle",
111 "preview.toggle",
112 "preview.refresh",
113 "preview.focusUrl",
114 "preview.zoomIn",
115 "preview.zoomOut",
116 "preview.resetZoom",
117 "preview.find",
118 "commandPalette.toggle",
119 "filePicker.toggle",
120 "projectSearch.toggle",
121 "usage.open",
122 "theme.select",
123 "appearance.cycle",
124 "themeEditor.toggle",
125 "composer.stash",
126 "composer.sendAlternate",
127 "composer.sendBackground",
128 "composer.sendAndNewThread",
129 "composer.dictation",
130 "composer.host",
131 "composer.cycleHost",
132 "composer.effort",
133 "composer.increaseEffort",
134 "composer.decreaseEffort",
135 "composer.mode",
136 "composer.workspace",
137 "composer.previousWorktree",
138 "composer.branch",
139 "chat.new",
140 "chat.newLocal",
141 "chat.newWithoutProject",
142 "chat.citeSelection",
143 "tab.new",
144 "tab.next",
145 "tab.previous",
146 "tab.close",
147 ...LAYOUT_KEYBINDING_COMMANDS,
148 "chat.find",
149 "editor.openFavorite",
150 "git.commitPush",
151 "usage.cost",
152 "usage.tokens",
153 "usage.limits",
154 "usage.period.day",
155 "usage.period.week",
156 "usage.period.month",
157 "usage.period.quarter",
158 ...MODEL_PICKER_KEYBINDING_COMMANDS,
159 ...THREAD_KEYBINDING_COMMANDS,
160] as const;
161​
162export const SCRIPT_RUN_COMMAND_PATTERN = Schema.TemplateLiteral([
163 Schema.Literal("script."),
164 Schema.NonEmptyString.check(
165 Schema.isMaxLength(MAX_SCRIPT_ID_LENGTH),
166 Schema.isPattern(/^[a-z0-9][a-z0-9-]*$/),
167 ),
168 Schema.Literal(".run"),
169]);
170​
171/**
172 * Commands that no longer do anything. They still decode, so a keybindings.json
173 * that names one stays valid; settings no longer offers them.
174 */
175const RETIRED_KEYBINDING_COMMANDS = ["rightPanel.new", "rightPanel.toggleMaximized"] as const;
176​
177export const KeybindingCommand = Schema.Union([
178 Schema.Literals(STATIC_KEYBINDING_COMMANDS),
179 Schema.Literals(RETIRED_KEYBINDING_COMMANDS),
180 SCRIPT_RUN_COMMAND_PATTERN,
181]);
182export type KeybindingCommand = typeof KeybindingCommand.Type;
183​
184export const KeybindingValue = TrimmedString.check(
185 Schema.isMinLength(1),
186 Schema.isMaxLength(MAX_KEYBINDING_VALUE_LENGTH),
187);
188​
189export const KeybindingWhen = TrimmedString.check(
190 Schema.isMinLength(1),
191 Schema.isMaxLength(MAX_KEYBINDING_WHEN_LENGTH),
192);
193export const KeybindingRule = Schema.Struct({
194 key: KeybindingValue,
195 command: KeybindingCommand,
196 when: Schema.optional(KeybindingWhen),
197});
198export type KeybindingRule = typeof KeybindingRule.Type;
199​
200export const KeybindingsConfig = Schema.Array(KeybindingRule).check(
201 Schema.isMaxLength(MAX_KEYBINDINGS_COUNT),
202);
203export type KeybindingsConfig = typeof KeybindingsConfig.Type;
204​
205// Untrimmed: the `space` token resolves to the key " ".
206const KeybindingShortcutKey = Schema.String.check(
207 Schema.isMinLength(1),
208 Schema.isMaxLength(MAX_KEYBINDING_VALUE_LENGTH),
209);
210​
211export const KeybindingShortcut = Schema.Struct({
212 key: KeybindingShortcutKey,
213 metaKey: Schema.Boolean,
214 ctrlKey: Schema.Boolean,
215 shiftKey: Schema.Boolean,
216 altKey: Schema.Boolean,
217 modKey: Schema.Boolean,
218});
219export type KeybindingShortcut = typeof KeybindingShortcut.Type;
220​
221export const PreviewForwardedShortcut = Schema.Struct({
222 command: KeybindingCommand,
223 shortcut: KeybindingShortcut,
224});
225export type PreviewForwardedShortcut = typeof PreviewForwardedShortcut.Type;
226​
227const KeybindingWhenNodeRef = Schema.suspend(
228 (): Schema.Codec<KeybindingWhenNode> => KeybindingWhenNode,
229);
230export const KeybindingWhenNode = Schema.Union([
231 Schema.Struct({
232 type: Schema.Literal("identifier"),
233 name: Schema.NonEmptyString,
234 }),
235 Schema.Struct({
236 type: Schema.Literal("not"),
237 node: KeybindingWhenNodeRef,
238 }),
239 Schema.Struct({
240 type: Schema.Literal("and"),
241 left: KeybindingWhenNodeRef,
242 right: KeybindingWhenNodeRef,
243 }),
244 Schema.Struct({
245 type: Schema.Literal("or"),
246 left: KeybindingWhenNodeRef,
247 right: KeybindingWhenNodeRef,
248 }),
249]);
250export type KeybindingWhenNode =
251 | { type: "identifier"; name: string }
252 | { type: "not"; node: KeybindingWhenNode }
253 | { type: "and"; left: KeybindingWhenNode; right: KeybindingWhenNode }
254 | { type: "or"; left: KeybindingWhenNode; right: KeybindingWhenNode };
255​
256export const ResolvedKeybindingRule = Schema.Struct({
257 command: KeybindingCommand,
258 shortcut: KeybindingShortcut,
259 whenAst: Schema.optional(KeybindingWhenNode),
260}).annotate({ parseOptions: { onExcessProperty: "ignore" } });
261export type ResolvedKeybindingRule = typeof ResolvedKeybindingRule.Type;
262​
263/** Drops undecodable rules without failing the connection. */
264export const ResolvedKeybindingsConfig = ForwardCompatibleArray(ResolvedKeybindingRule).check(
265 Schema.isMaxLength(MAX_KEYBINDINGS_COUNT),
266);
267export type ResolvedKeybindingsConfig = typeof ResolvedKeybindingsConfig.Type;
268​
269export class KeybindingsConfigError extends Schema.TaggedError<KeybindingsConfigError>()(
270 "KeybindingsConfigParseError",
271 {
272 configPath: Schema.String,
273 detail: Schema.String,
274 cause: Schema.optional(Schema.Defect()),
275 },
276) {
277 override get message(): string {
278 return `Unable to parse keybindings config at ${this.configPath}: ${this.detail}`;
279 }