rethocker 0.0.2 → 0.1.1
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/README.md +379 -3
- package/bin/rethocker-native +0 -0
- package/package.json +22 -2
- package/src/actions.ts +282 -0
- package/src/daemon.ts +244 -0
- package/src/index.test.ts +63 -0
- package/src/index.ts +8 -1
- package/src/keys.ts +346 -0
- package/src/parse-key.ts +169 -0
- package/src/register-rule.ts +186 -0
- package/src/rethocker.ts +125 -0
- package/src/rule-engine.ts +101 -0
- package/src/rule-types.ts +169 -0
- package/src/scripts/debug-keys.ts +45 -0
- package/src/scripts/example.ts +74 -0
- package/src/types.ts +298 -0
- package/AGENTS.md +0 -106
- package/bun.lock +0 -26
- package/tsconfig.json +0 -29
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>;
|
package/src/parse-key.ts
ADDED
|
@@ -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,186 @@
|
|
|
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
|
+
const target = parseKey(rule.remap);
|
|
68
|
+
const action =
|
|
69
|
+
target.kind === "sequence"
|
|
70
|
+
? ({ type: "remap_sequence", steps: target.steps } as const)
|
|
71
|
+
: ({
|
|
72
|
+
type: "remap",
|
|
73
|
+
keyCode: target.combo.keyCode,
|
|
74
|
+
modifiers: target.combo.modifiers,
|
|
75
|
+
} as const);
|
|
76
|
+
|
|
77
|
+
if (parsed.kind === "single") {
|
|
78
|
+
return addRule(send, parsed.combo, action, {
|
|
79
|
+
id: rule.id,
|
|
80
|
+
conditions: buildConditions(rule),
|
|
81
|
+
disabled: rule.disabled,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sequence trigger: intercept the full sequence then post the remap target
|
|
86
|
+
return addSequence(send, parsed.steps, action, {
|
|
87
|
+
id: rule.id,
|
|
88
|
+
conditions: rule.conditions,
|
|
89
|
+
disabled: rule.disabled,
|
|
90
|
+
consume: true, // always consume — we're replacing the sequence
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveExecute(execute: string | string[]): string {
|
|
95
|
+
return Array.isArray(execute) ? execute.join(" && ") : execute;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function registerShell(
|
|
99
|
+
send: (obj: Record<string, unknown>) => void,
|
|
100
|
+
rule: ShellRule,
|
|
101
|
+
): RuleHandle | SequenceHandle {
|
|
102
|
+
const parsed = parseKey(rule.key);
|
|
103
|
+
const conditions = buildConditions(rule);
|
|
104
|
+
const command = resolveExecute(rule.execute);
|
|
105
|
+
if (parsed.kind === "single") {
|
|
106
|
+
return addRule(
|
|
107
|
+
send,
|
|
108
|
+
parsed.combo,
|
|
109
|
+
{ type: "run", command },
|
|
110
|
+
{ id: rule.id, conditions, disabled: rule.disabled },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return addSequence(
|
|
114
|
+
send,
|
|
115
|
+
parsed.steps,
|
|
116
|
+
{ type: "run", command },
|
|
117
|
+
{
|
|
118
|
+
id: rule.id,
|
|
119
|
+
conditions,
|
|
120
|
+
disabled: rule.disabled,
|
|
121
|
+
consume: rule.consume,
|
|
122
|
+
timeoutMs: rule.sequenceTimeoutMs,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function registerHandler(
|
|
128
|
+
send: (obj: Record<string, unknown>) => void,
|
|
129
|
+
emitter: TypedEmitter,
|
|
130
|
+
rule: HandlerRule,
|
|
131
|
+
): RuleHandle | SequenceHandle {
|
|
132
|
+
const parsed = parseKey(rule.key);
|
|
133
|
+
const conditions = buildConditions(rule);
|
|
134
|
+
|
|
135
|
+
if (parsed.kind === "single") {
|
|
136
|
+
return intercept(send, emitter, parsed.combo, rule.handler, {
|
|
137
|
+
id: rule.id,
|
|
138
|
+
conditions,
|
|
139
|
+
disabled: rule.disabled,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Sequence + handler: register as emit, wire up listener internally
|
|
144
|
+
const ruleID = rule.id ?? genID("seq");
|
|
145
|
+
const eventID = `${ruleID}_event`;
|
|
146
|
+
const lastStep = parsed.steps.at(-1);
|
|
147
|
+
|
|
148
|
+
const handle = addSequence(
|
|
149
|
+
send,
|
|
150
|
+
parsed.steps,
|
|
151
|
+
{ type: "emit", eventID },
|
|
152
|
+
{
|
|
153
|
+
id: ruleID,
|
|
154
|
+
conditions,
|
|
155
|
+
disabled: rule.disabled,
|
|
156
|
+
consume: rule.consume,
|
|
157
|
+
timeoutMs: rule.sequenceTimeoutMs,
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
emitter.on("sequence", (seqRuleID, eid) => {
|
|
162
|
+
if (eid === eventID) {
|
|
163
|
+
const event: KeyEvent = {
|
|
164
|
+
type: "keydown",
|
|
165
|
+
keyCode: lastStep?.keyCode ?? 0,
|
|
166
|
+
modifiers: lastStep?.modifiers ?? [],
|
|
167
|
+
ruleID: seqRuleID,
|
|
168
|
+
eventID,
|
|
169
|
+
suppressed: true,
|
|
170
|
+
};
|
|
171
|
+
rule.handler(event);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return handle;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function registerRule(
|
|
179
|
+
send: (obj: Record<string, unknown>) => void,
|
|
180
|
+
emitter: TypedEmitter,
|
|
181
|
+
rule: RethockerRule,
|
|
182
|
+
): RuleHandle | SequenceHandle {
|
|
183
|
+
if (rule.remap !== undefined) return registerRemap(send, rule);
|
|
184
|
+
if (rule.execute !== undefined) return registerShell(send, rule);
|
|
185
|
+
return registerHandler(send, emitter, rule);
|
|
186
|
+
}
|