rethocker 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/keys.ts ADDED
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Key name constants — use these for autocomplete and safe string interpolation
3
+ * in rule `key` fields.
4
+ *
5
+ * Every value is the key's canonical name string, exactly as the parser accepts.
6
+ * This means you can safely interpolate them:
7
+ *
8
+ * `${Key.brightnessDown} ${Key.playPause}` → "brightnessDown playPause"
9
+ * `Cmd+${Key.v}` → "Cmd+v"
10
+ *
11
+ * @example
12
+ * rethocker([
13
+ * { key: Key.capsLock, remap: Key.escape },
14
+ * { key: `${Key.brightnessDown}`, execute: "..." },
15
+ * { key: `${Key.mediaNext} ${Key.mediaPrevious}`, handler: () => {} },
16
+ * ])
17
+ */
18
+ export const Key = {
19
+ // ── Letters ──────────────────────────────────────────────────────────────
20
+ a: "a",
21
+ s: "s",
22
+ d: "d",
23
+ f: "f",
24
+ h: "h",
25
+ g: "g",
26
+ z: "z",
27
+ x: "x",
28
+ c: "c",
29
+ v: "v",
30
+ b: "b",
31
+ q: "q",
32
+ w: "w",
33
+ e: "e",
34
+ r: "r",
35
+ y: "y",
36
+ t: "t",
37
+ o: "o",
38
+ u: "u",
39
+ i: "i",
40
+ p: "p",
41
+ l: "l",
42
+ j: "j",
43
+ k: "k",
44
+ n: "n",
45
+ m: "m",
46
+
47
+ // ── Number row ───────────────────────────────────────────────────────────
48
+ n0: "n0",
49
+ n1: "n1",
50
+ n2: "n2",
51
+ n3: "n3",
52
+ n4: "n4",
53
+ n5: "n5",
54
+ n6: "n6",
55
+ n7: "n7",
56
+ n8: "n8",
57
+ n9: "n9",
58
+
59
+ // ── Punctuation ──────────────────────────────────────────────────────────
60
+ minus: "minus",
61
+ equal: "equal",
62
+ leftBracket: "leftBracket",
63
+ rightBracket: "rightBracket",
64
+ backslash: "backslash",
65
+ semicolon: "semicolon",
66
+ quote: "quote",
67
+ grave: "grave",
68
+ comma: "comma",
69
+ period: "period",
70
+ slash: "slash",
71
+
72
+ // ── Special / control ────────────────────────────────────────────────────
73
+ return: "return",
74
+ tab: "tab",
75
+ space: "space",
76
+ delete: "delete",
77
+ forwardDelete: "forwardDelete",
78
+ escape: "escape",
79
+ capsLock: "capsLock",
80
+ fn: "fn",
81
+ help: "help",
82
+ contextualMenu: "contextualMenu",
83
+
84
+ // ── Modifier keys ────────────────────────────────────────────────────────
85
+ leftCmd: "leftCmd",
86
+ rightCmd: "rightCmd",
87
+ leftShift: "leftShift",
88
+ rightShift: "rightShift",
89
+ leftAlt: "leftAlt",
90
+ rightAlt: "rightAlt",
91
+ leftCtrl: "leftCtrl",
92
+ rightCtrl: "rightCtrl",
93
+
94
+ // ── Arrow keys ───────────────────────────────────────────────────────────
95
+ left: "left",
96
+ right: "right",
97
+ down: "down",
98
+ up: "up",
99
+
100
+ // ── Navigation ───────────────────────────────────────────────────────────
101
+ home: "home",
102
+ end: "end",
103
+ pageUp: "pageUp",
104
+ pageDown: "pageDown",
105
+
106
+ // ── Function keys ────────────────────────────────────────────────────────
107
+ f1: "f1",
108
+ f2: "f2",
109
+ f3: "f3",
110
+ f4: "f4",
111
+ f5: "f5",
112
+ f6: "f6",
113
+ f7: "f7",
114
+ f8: "f8",
115
+ f9: "f9",
116
+ f10: "f10",
117
+ f11: "f11",
118
+ f12: "f12",
119
+ f13: "f13",
120
+ f14: "f14",
121
+ f15: "f15",
122
+ f16: "f16",
123
+ f17: "f17",
124
+ f18: "f18",
125
+ f19: "f19",
126
+ f20: "f20",
127
+
128
+ // ── Media / system keys ──────────────────────────────────────────────────
129
+ volumeUp: "volumeUp",
130
+ volumeDown: "volumeDown",
131
+ brightnessUp: "brightnessUp",
132
+ brightnessDown: "brightnessDown",
133
+ mute: "mute",
134
+ eject: "eject",
135
+ playPause: "playPause",
136
+ mediaNext: "mediaNext",
137
+ mediaPrevious: "mediaPrevious",
138
+ mediaFastForward: "mediaFastForward",
139
+ mediaRewind: "mediaRewind",
140
+ illuminationUp: "illuminationUp",
141
+ illuminationDown: "illuminationDown",
142
+ illuminationToggle: "illuminationToggle",
143
+
144
+ // ── Numpad ───────────────────────────────────────────────────────────────
145
+ numpad0: "numpad0",
146
+ numpad1: "numpad1",
147
+ numpad2: "numpad2",
148
+ numpad3: "numpad3",
149
+ numpad4: "numpad4",
150
+ numpad5: "numpad5",
151
+ numpad6: "numpad6",
152
+ numpad7: "numpad7",
153
+ numpad8: "numpad8",
154
+ numpad9: "numpad9",
155
+ numpadDecimal: "numpadDecimal",
156
+ numpadMultiply: "numpadMultiply",
157
+ numpadAdd: "numpadAdd",
158
+ numpadClear: "numpadClear",
159
+ numpadDivide: "numpadDivide",
160
+ numpadEnter: "numpadEnter",
161
+ numpadSubtract: "numpadSubtract",
162
+ numpadEquals: "numpadEquals",
163
+
164
+ // ── International / non-ANSI ─────────────────────────────────────────────
165
+ isoSection: "isoSection",
166
+ jisYen: "jisYen",
167
+ jisUnderscore: "jisUnderscore",
168
+ jisKeypadComma: "jisKeypadComma",
169
+ jisEisu: "jisEisu",
170
+ jisKana: "jisKana",
171
+ } as const satisfies Record<string, string>;
172
+
173
+ export type KeyName = keyof typeof Key;
174
+
175
+ // ─── Private: name → keyCode (used only by parse-key.ts) ─────────────────────
176
+
177
+ /** @internal */
178
+ export const KEY_CODE_MAP: Record<string, number> = {
179
+ // Letters
180
+ a: 0,
181
+ s: 1,
182
+ d: 2,
183
+ f: 3,
184
+ h: 4,
185
+ g: 5,
186
+ z: 6,
187
+ x: 7,
188
+ c: 8,
189
+ v: 9,
190
+ b: 11,
191
+ q: 12,
192
+ w: 13,
193
+ e: 14,
194
+ r: 15,
195
+ y: 16,
196
+ t: 17,
197
+ o: 31,
198
+ u: 32,
199
+ i: 34,
200
+ p: 35,
201
+ l: 37,
202
+ j: 38,
203
+ k: 40,
204
+ n: 45,
205
+ m: 46,
206
+ // Number row
207
+ n0: 29,
208
+ n1: 18,
209
+ n2: 19,
210
+ n3: 20,
211
+ n4: 21,
212
+ n5: 23,
213
+ n6: 22,
214
+ n7: 26,
215
+ n8: 28,
216
+ n9: 25,
217
+ // Punctuation
218
+ minus: 27,
219
+ equal: 24,
220
+ leftbracket: 33,
221
+ rightbracket: 30,
222
+ backslash: 42,
223
+ semicolon: 41,
224
+ quote: 39,
225
+ grave: 50,
226
+ comma: 43,
227
+ period: 47,
228
+ slash: 44,
229
+ // Special
230
+ return: 36,
231
+ tab: 48,
232
+ space: 49,
233
+ delete: 51,
234
+ forwarddelete: 117,
235
+ escape: 53,
236
+ capslock: 57,
237
+ fn: 63,
238
+ help: 114,
239
+ contextualmenu: 110,
240
+ // Modifiers
241
+ leftcmd: 55,
242
+ rightcmd: 54,
243
+ leftshift: 56,
244
+ rightshift: 60,
245
+ leftalt: 58,
246
+ rightalt: 61,
247
+ leftctrl: 59,
248
+ rightctrl: 62,
249
+ // Arrows
250
+ left: 123,
251
+ right: 124,
252
+ down: 125,
253
+ up: 126,
254
+ // Navigation
255
+ home: 115,
256
+ end: 119,
257
+ pageup: 116,
258
+ pagedown: 121,
259
+ // Function keys
260
+ f1: 122,
261
+ f2: 120,
262
+ f3: 99,
263
+ f4: 118,
264
+ f5: 96,
265
+ f6: 97,
266
+ f7: 98,
267
+ f8: 100,
268
+ f9: 101,
269
+ f10: 109,
270
+ f11: 103,
271
+ f12: 111,
272
+ f13: 105,
273
+ f14: 107,
274
+ f15: 113,
275
+ f16: 106,
276
+ f17: 64,
277
+ f18: 79,
278
+ f19: 80,
279
+ f20: 90,
280
+ // Media / system keys (NX_KEYTYPE_* + 1000 offset)
281
+ volumeup: 1000,
282
+ volumedown: 1001,
283
+ brightnessup: 1002,
284
+ brightnessdown: 1003,
285
+ mute: 1007,
286
+ eject: 1014,
287
+ playpause: 1016,
288
+ medianext: 1017,
289
+ mediaprevious: 1018,
290
+ mediafastforward: 1019,
291
+ mediarewind: 1020,
292
+ illuminationup: 1021,
293
+ illuminationdown: 1022,
294
+ illuminationtoggle: 1023,
295
+ // Numpad
296
+ numpad0: 82,
297
+ numpad1: 83,
298
+ numpad2: 84,
299
+ numpad3: 85,
300
+ numpad4: 86,
301
+ numpad5: 87,
302
+ numpad6: 88,
303
+ numpad7: 89,
304
+ numpad8: 91,
305
+ numpad9: 92,
306
+ numpaddecimal: 65,
307
+ numpadmultiply: 67,
308
+ numpadadd: 69,
309
+ numpadclear: 71,
310
+ numpaddivide: 75,
311
+ numpadenter: 76,
312
+ numpadsubtract: 78,
313
+ numpadequals: 81,
314
+ // International
315
+ isosection: 10,
316
+ jisyen: 93,
317
+ jisunderscore: 94,
318
+ jiskeypadcomma: 95,
319
+ jiseisu: 102,
320
+ jiskana: 104,
321
+ };
322
+
323
+ /**
324
+ * Modifier key constants for use in rule triggers and remap targets.
325
+ * Values match the Modifier string union type exactly.
326
+ *
327
+ * @example
328
+ * { key: `Cmd+${Key.a}`, modifiers: [KeyModifier.Cmd] }
329
+ */
330
+ export const KeyModifier = {
331
+ // ── Side-agnostic (matches either left or right) ──────────────────────────
332
+ Cmd: "cmd",
333
+ Shift: "shift",
334
+ Alt: "alt",
335
+ Ctrl: "ctrl",
336
+ Fn: "fn",
337
+ // ── Side-specific ─────────────────────────────────────────────────────────
338
+ LeftCmd: "leftCmd",
339
+ RightCmd: "rightCmd",
340
+ LeftShift: "leftShift",
341
+ RightShift: "rightShift",
342
+ LeftAlt: "leftAlt",
343
+ RightAlt: "rightAlt",
344
+ LeftCtrl: "leftCtrl",
345
+ RightCtrl: "rightCtrl",
346
+ } as const satisfies Record<string, import("./types.ts").Modifier>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Parses the key string syntax used in the high-level rethocker() API.
3
+ *
4
+ * Single key / combo: "escape" | "Cmd+A" | "Cmd+Shift+K"
5
+ * Sequence (space-sep): "Cmd+R T" | "Ctrl+J Ctrl+K"
6
+ *
7
+ * Modifier names are case-insensitive. Key names match the Key constant
8
+ * object (also case-insensitive). Modifier aliases: "opt" = "alt",
9
+ * "option" = "alt", "command" = "cmd", "control" = "ctrl", "win" = "ctrl".
10
+ */
11
+
12
+ import { KEY_CODE_MAP } from "./keys.ts";
13
+ import type { KeyCombo, Modifier } from "./types.ts";
14
+
15
+ // Helper: look up a guaranteed-present key from KEY_CODE_MAP
16
+ function kc(name: string): number {
17
+ return KEY_CODE_MAP[name] as number;
18
+ }
19
+
20
+ // ─── Modifier aliases ─────────────────────────────────────────────────────────
21
+
22
+ const MODIFIER_MAP: Record<string, Modifier> = {
23
+ cmd: "cmd",
24
+ command: "cmd",
25
+ shift: "shift",
26
+ alt: "alt",
27
+ opt: "alt",
28
+ option: "alt",
29
+ ctrl: "ctrl",
30
+ control: "ctrl",
31
+ win: "ctrl",
32
+ fn: "fn",
33
+ leftcmd: "leftCmd",
34
+ rightcmd: "rightCmd",
35
+ leftshift: "leftShift",
36
+ rightshift: "rightShift",
37
+ leftalt: "leftAlt",
38
+ leftopt: "leftAlt",
39
+ righttalt: "rightAlt",
40
+ rightopt: "rightAlt",
41
+ leftctrl: "leftCtrl",
42
+ rightctrl: "rightCtrl",
43
+ };
44
+
45
+ // ─── Key name → key code lookup (case-insensitive) ───────────────────────────
46
+ //
47
+ // Primary lookup: KEY_CODE_MAP (all canonical names, lowercased).
48
+ // Aliases: common alternate names for the same key.
49
+
50
+ const KEY_ALIASES: Record<string, number> = {
51
+ esc: kc("escape"),
52
+ enter: kc("return"),
53
+ backspace: kc("delete"),
54
+ back: kc("delete"),
55
+ del: kc("forwarddelete"),
56
+ caps: kc("capslock"),
57
+ arrowleft: kc("left"),
58
+ arrowright: kc("right"),
59
+ arrowup: kc("up"),
60
+ arrowdown: kc("down"),
61
+
62
+ // Short "num" prefix aliases for numpad keys
63
+ num0: kc("numpad0"),
64
+ num1: kc("numpad1"),
65
+ num2: kc("numpad2"),
66
+ num3: kc("numpad3"),
67
+ num4: kc("numpad4"),
68
+ num5: kc("numpad5"),
69
+ num6: kc("numpad6"),
70
+ num7: kc("numpad7"),
71
+ num8: kc("numpad8"),
72
+ num9: kc("numpad9"),
73
+ numenter: kc("numpadenter"),
74
+ numdecimal: kc("numpaddecimal"),
75
+ numpadperiod: kc("numpaddecimal"),
76
+ numadd: kc("numpadadd"),
77
+ numpadplus: kc("numpadadd"),
78
+ numplus: kc("numpadadd"),
79
+ numsubtract: kc("numpadsubtract"),
80
+ numpadminus: kc("numpadsubtract"),
81
+ numminus: kc("numpadsubtract"),
82
+ nummultiply: kc("numpadmultiply"),
83
+ numpadstar: kc("numpadmultiply"),
84
+ numdivide: kc("numpaddivide"),
85
+ numpadslash: kc("numpaddivide"),
86
+ numequals: kc("numpadequals"),
87
+ numclear: kc("numpadclear"),
88
+
89
+ // Numpad with "Numpad" prefix (VSCode style) — also auto-resolved via KEY_CODE_MAP
90
+ // since numpad* keys are already in there lowercased.
91
+
92
+ // Media / system key aliases
93
+ "volume up": kc("volumeup"),
94
+ "volume down": kc("volumedown"),
95
+ "brightness up": kc("brightnessup"),
96
+ "brightness down": kc("brightnessdown"),
97
+ playpause: kc("playpause"),
98
+ "play/pause": kc("playpause"),
99
+ play: kc("playpause"),
100
+ pause: kc("playpause"),
101
+ nextrack: kc("medianext"),
102
+ nexttrack: kc("medianext"),
103
+ "media next": kc("medianext"),
104
+ prevtrack: kc("mediaprevious"),
105
+ previoustrack: kc("mediaprevious"),
106
+ "media previous": kc("mediaprevious"),
107
+ "media prev": kc("mediaprevious"),
108
+ fastforward: kc("mediafastforward"),
109
+ "fast forward": kc("mediafastforward"),
110
+ rewind: kc("mediarewind"),
111
+ "keyboard brightness up": kc("illuminationup"),
112
+ "keyboard brightness down": kc("illuminationdown"),
113
+ "keyboard brightness toggle": kc("illuminationtoggle"),
114
+ };
115
+
116
+ function resolveKeyCode(name: string): number {
117
+ const lower = name.toLowerCase();
118
+ const code = KEY_CODE_MAP[lower] ?? KEY_ALIASES[lower];
119
+ if (code !== undefined) return code;
120
+
121
+ throw new Error(
122
+ `Unknown key name: "${name}". Use Key.* constants or check the Key reference.`,
123
+ );
124
+ }
125
+
126
+ // ─── Parse a single combo token, e.g. "Cmd+Shift+A" ─────────────────────────
127
+
128
+ function parseCombo(token: string): KeyCombo {
129
+ const parts = token.split("+");
130
+ const keyPart = parts[parts.length - 1];
131
+ const modParts = parts.slice(0, parts.length - 1);
132
+
133
+ if (!keyPart) {
134
+ throw new Error(`Invalid key combo: "${token}"`);
135
+ }
136
+
137
+ const modifiers: Modifier[] = [];
138
+ for (const mod of modParts) {
139
+ const resolved = MODIFIER_MAP[mod.toLowerCase()];
140
+ if (!resolved) {
141
+ throw new Error(
142
+ `Unknown modifier: "${mod}" in "${token}". Valid modifiers: Cmd, Shift, Alt, Ctrl, Fn (and Left/Right variants).`,
143
+ );
144
+ }
145
+ modifiers.push(resolved);
146
+ }
147
+
148
+ return {
149
+ keyCode: resolveKeyCode(keyPart),
150
+ ...(modifiers.length > 0 ? { modifiers } : {}),
151
+ };
152
+ }
153
+
154
+ // ─── Public: parse a full key string ─────────────────────────────────────────
155
+
156
+ export type ParsedKey =
157
+ | { kind: "single"; combo: KeyCombo }
158
+ | { kind: "sequence"; steps: KeyCombo[] };
159
+
160
+ export function parseKey(keyString: string): ParsedKey {
161
+ const tokens = keyString.trim().split(/\s+/);
162
+ if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === "")) {
163
+ throw new Error(`Empty key string`);
164
+ }
165
+ if (tokens.length === 1) {
166
+ return { kind: "single", combo: parseCombo(tokens[0] ?? "") };
167
+ }
168
+ return { kind: "sequence", steps: tokens.map((t) => parseCombo(t)) };
169
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Translates high-level RethockerRule objects into low-level rule-engine calls.
3
+ *
4
+ * This is the "compiler" layer: it parses key strings, resolves rule variants,
5
+ * and dispatches to addRule / addSequence / intercept accordingly.
6
+ */
7
+
8
+ import type { TypedEmitter } from "./daemon.ts";
9
+ import { parseKey } from "./parse-key.ts";
10
+ import { addRule, addSequence, genID, intercept } from "./rule-engine.ts";
11
+ import type {
12
+ HandlerRule,
13
+ RemapRule,
14
+ RethockerRule,
15
+ ShellRule,
16
+ } from "./rule-types.ts";
17
+ import type {
18
+ AppCondition,
19
+ KeyEvent,
20
+ RuleConditions,
21
+ RuleHandle,
22
+ SequenceHandle,
23
+ } from "./types.ts";
24
+
25
+ // ─── Condition builder ────────────────────────────────────────────────────────
26
+ // Merges the ergonomic `app` shorthand field into a RuleConditions object,
27
+ // combined with any explicitly passed `conditions`.
28
+ //
29
+ // Prefix a value with "!" to negate (invert) the match:
30
+ // app: "!VSCode" → fire when VSCode is NOT frontmost
31
+ // app: ["!Chrome", "!Safari"] → fire in any app except Chrome and Safari
32
+
33
+ function toAppCondition(value: string): AppCondition {
34
+ const invert = value.startsWith("!");
35
+ const name = invert ? value.slice(1) : value;
36
+ // Bundle IDs contain dots (e.g. "com.apple.Terminal"); display names don't
37
+ return name.includes(".") ? { bundleID: name, invert } : { name, invert };
38
+ }
39
+
40
+ function buildConditions(rule: {
41
+ app?: string | string[];
42
+ conditions?: RuleConditions;
43
+ }): RuleConditions | undefined {
44
+ const base = rule.conditions ?? {};
45
+
46
+ // app → activeApp conditions (supports "!" prefix for negation)
47
+ const activeApp =
48
+ rule.app !== undefined
49
+ ? [...(base.activeApp ?? []), ...[rule.app].flat().map(toAppCondition)]
50
+ : base.activeApp;
51
+
52
+ const merged: RuleConditions = { ...base, activeApp };
53
+
54
+ const hasAny =
55
+ merged.activeApp !== undefined || merged.runningApps !== undefined;
56
+
57
+ return hasAny ? merged : undefined;
58
+ }
59
+
60
+ // ─── Rule registration ────────────────────────────────────────────────────────
61
+
62
+ function registerRemap(
63
+ send: (obj: Record<string, unknown>) => void,
64
+ rule: RemapRule,
65
+ ): RuleHandle {
66
+ const parsed = parseKey(rule.key);
67
+ if (parsed.kind === "sequence") {
68
+ throw new Error(
69
+ `remap does not support sequences as trigger: "${rule.key}"`,
70
+ );
71
+ }
72
+ const target = parseKey(rule.remap);
73
+ const action =
74
+ target.kind === "sequence"
75
+ ? ({ type: "remap_sequence", steps: target.steps } as const)
76
+ : ({
77
+ type: "remap",
78
+ keyCode: target.combo.keyCode,
79
+ modifiers: target.combo.modifiers,
80
+ } as const);
81
+ return addRule(send, parsed.combo, action, {
82
+ id: rule.id,
83
+ conditions: buildConditions(rule),
84
+ disabled: rule.disabled,
85
+ });
86
+ }
87
+
88
+ function resolveExecute(execute: string | string[]): string {
89
+ return Array.isArray(execute) ? execute.join(" && ") : execute;
90
+ }
91
+
92
+ function registerShell(
93
+ send: (obj: Record<string, unknown>) => void,
94
+ rule: ShellRule,
95
+ ): RuleHandle | SequenceHandle {
96
+ const parsed = parseKey(rule.key);
97
+ const conditions = buildConditions(rule);
98
+ const command = resolveExecute(rule.execute);
99
+ if (parsed.kind === "single") {
100
+ return addRule(
101
+ send,
102
+ parsed.combo,
103
+ { type: "run", command },
104
+ { id: rule.id, conditions, disabled: rule.disabled },
105
+ );
106
+ }
107
+ return addSequence(
108
+ send,
109
+ parsed.steps,
110
+ { type: "run", command },
111
+ {
112
+ id: rule.id,
113
+ conditions,
114
+ disabled: rule.disabled,
115
+ consume: rule.consume,
116
+ timeoutMs: rule.sequenceTimeoutMs,
117
+ },
118
+ );
119
+ }
120
+
121
+ function registerHandler(
122
+ send: (obj: Record<string, unknown>) => void,
123
+ emitter: TypedEmitter,
124
+ rule: HandlerRule,
125
+ ): RuleHandle | SequenceHandle {
126
+ const parsed = parseKey(rule.key);
127
+ const conditions = buildConditions(rule);
128
+
129
+ if (parsed.kind === "single") {
130
+ return intercept(send, emitter, parsed.combo, rule.handler, {
131
+ id: rule.id,
132
+ conditions,
133
+ disabled: rule.disabled,
134
+ });
135
+ }
136
+
137
+ // Sequence + handler: register as emit, wire up listener internally
138
+ const ruleID = rule.id ?? genID("seq");
139
+ const eventID = `${ruleID}_event`;
140
+ const lastStep = parsed.steps.at(-1);
141
+
142
+ const handle = addSequence(
143
+ send,
144
+ parsed.steps,
145
+ { type: "emit", eventID },
146
+ {
147
+ id: ruleID,
148
+ conditions,
149
+ disabled: rule.disabled,
150
+ consume: rule.consume,
151
+ timeoutMs: rule.sequenceTimeoutMs,
152
+ },
153
+ );
154
+
155
+ emitter.on("sequence", (seqRuleID, eid) => {
156
+ if (eid === eventID) {
157
+ const event: KeyEvent = {
158
+ type: "keydown",
159
+ keyCode: lastStep?.keyCode ?? 0,
160
+ modifiers: lastStep?.modifiers ?? [],
161
+ ruleID: seqRuleID,
162
+ eventID,
163
+ suppressed: true,
164
+ };
165
+ rule.handler(event);
166
+ }
167
+ });
168
+
169
+ return handle;
170
+ }
171
+
172
+ export function registerRule(
173
+ send: (obj: Record<string, unknown>) => void,
174
+ emitter: TypedEmitter,
175
+ rule: RethockerRule,
176
+ ): RuleHandle | SequenceHandle {
177
+ if (rule.remap !== undefined) return registerRemap(send, rule);
178
+ if (rule.execute !== undefined) return registerShell(send, rule);
179
+ return registerHandler(send, emitter, rule);
180
+ }