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/README.md +382 -6
- package/bin/rethocker-native +0 -0
- package/package.json +22 -7
- 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 +180 -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/src/types.ts
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
/** Default timeout between consecutive key presses in a sequence. */
|
|
4
|
+
export const DEFAULT_SEQUENCE_TIMEOUT_MS = 5000;
|
|
5
|
+
|
|
6
|
+
// ─── Modifiers ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export type Modifier =
|
|
9
|
+
| "cmd"
|
|
10
|
+
| "shift"
|
|
11
|
+
| "alt"
|
|
12
|
+
| "ctrl"
|
|
13
|
+
| "fn"
|
|
14
|
+
| "leftCmd"
|
|
15
|
+
| "rightCmd"
|
|
16
|
+
| "leftShift"
|
|
17
|
+
| "rightShift"
|
|
18
|
+
| "leftAlt"
|
|
19
|
+
| "rightAlt"
|
|
20
|
+
| "leftCtrl"
|
|
21
|
+
| "rightCtrl";
|
|
22
|
+
|
|
23
|
+
// ─── Key combo ───────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface KeyCombo {
|
|
26
|
+
/** macOS virtual key code (e.g. 0 = A, 36 = Return, 53 = Escape) */
|
|
27
|
+
keyCode: number;
|
|
28
|
+
modifiers?: Modifier[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── App conditions ──────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface AppCondition {
|
|
34
|
+
/** Match by bundle ID (exact), e.g. "com.apple.Terminal" */
|
|
35
|
+
bundleID?: string;
|
|
36
|
+
/** Match by app display name (prefix, case-insensitive), e.g. "Terminal" */
|
|
37
|
+
name?: string;
|
|
38
|
+
/** If true, invert the match (i.e. "not this app") */
|
|
39
|
+
invert?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RuleConditions {
|
|
43
|
+
/**
|
|
44
|
+
* Rule fires only when one of these apps is frontmost.
|
|
45
|
+
* Items are OR-ed; omit for any app.
|
|
46
|
+
*/
|
|
47
|
+
activeApp?: AppCondition[];
|
|
48
|
+
/**
|
|
49
|
+
* Rule fires only when one of these apps is currently running.
|
|
50
|
+
* Items are OR-ed; omit to not care.
|
|
51
|
+
*/
|
|
52
|
+
runningApps?: AppCondition[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Actions ─────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** Eat the keypress silently */
|
|
58
|
+
export interface SuppressAction {
|
|
59
|
+
type: "suppress";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Replace the keypress with a different key combo */
|
|
63
|
+
export interface RemapAction {
|
|
64
|
+
type: "remap";
|
|
65
|
+
keyCode: number;
|
|
66
|
+
modifiers?: Modifier[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Replace the keypress with a sequence of key combos posted in order */
|
|
70
|
+
export interface RemapSequenceAction {
|
|
71
|
+
type: "remap_sequence";
|
|
72
|
+
steps: Array<{ keyCode: number; modifiers?: Modifier[] }>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Run a shell command (key is suppressed) */
|
|
76
|
+
export interface RunAction {
|
|
77
|
+
type: "run";
|
|
78
|
+
command: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Suppress the key and emit a named event on the rethocker instance.
|
|
83
|
+
* Use this to react in TypeScript without spawning a shell.
|
|
84
|
+
*/
|
|
85
|
+
export interface EmitAction {
|
|
86
|
+
type: "emit";
|
|
87
|
+
eventID: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type RuleAction =
|
|
91
|
+
| SuppressAction
|
|
92
|
+
| RemapAction
|
|
93
|
+
| RemapSequenceAction
|
|
94
|
+
| RunAction
|
|
95
|
+
| EmitAction;
|
|
96
|
+
|
|
97
|
+
// ─── Rule options ─────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export interface RuleOptions {
|
|
100
|
+
/** Unique ID. Auto-generated if omitted. */
|
|
101
|
+
id?: string;
|
|
102
|
+
conditions?: RuleConditions;
|
|
103
|
+
/** If true, fire on key-up instead of key-down (only valid for suppress/emit) */
|
|
104
|
+
onKeyUp?: boolean;
|
|
105
|
+
/** Start disabled */
|
|
106
|
+
disabled?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface SequenceOptions {
|
|
110
|
+
/** Unique ID. Auto-generated if omitted. */
|
|
111
|
+
id?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Max milliseconds between consecutive key presses in the sequence.
|
|
114
|
+
* @default DEFAULT_SEQUENCE_TIMEOUT_MS (5000)
|
|
115
|
+
*/
|
|
116
|
+
timeoutMs?: number;
|
|
117
|
+
conditions?: Pick<RuleConditions, "activeApp">;
|
|
118
|
+
/**
|
|
119
|
+
* When true, all key events that are part of the sequence are consumed —
|
|
120
|
+
* they never reach the active app. Intermediate steps and the final key are
|
|
121
|
+
* all consumed, regardless of the action type.
|
|
122
|
+
* @default false
|
|
123
|
+
*/
|
|
124
|
+
consume?: boolean;
|
|
125
|
+
/** Start disabled */
|
|
126
|
+
disabled?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Handles (returned to callers) ───────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
/** Returned by addRule() and intercept(). */
|
|
132
|
+
export interface RuleHandle {
|
|
133
|
+
readonly id: string;
|
|
134
|
+
/** Remove the rule permanently. */
|
|
135
|
+
remove(): void;
|
|
136
|
+
/** Enable the rule (no-op if already enabled). */
|
|
137
|
+
enable(): void;
|
|
138
|
+
/** Disable the rule without removing it. */
|
|
139
|
+
disable(): void;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Returned by addSequence(). Same shape as RuleHandle. */
|
|
143
|
+
export interface SequenceHandle {
|
|
144
|
+
readonly id: string;
|
|
145
|
+
remove(): void;
|
|
146
|
+
enable(): void;
|
|
147
|
+
disable(): void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Device info ─────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
export interface DeviceInfo {
|
|
153
|
+
/** String used in `deviceIDs` conditions, format "vendorID:productID" */
|
|
154
|
+
id: string;
|
|
155
|
+
name?: string;
|
|
156
|
+
manufacturer?: string;
|
|
157
|
+
vendorID?: number;
|
|
158
|
+
productID?: number;
|
|
159
|
+
transport?: string;
|
|
160
|
+
locationID?: number;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Events ──────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
export interface KeyEvent {
|
|
166
|
+
type: "keydown" | "keyup" | "flags";
|
|
167
|
+
keyCode: number;
|
|
168
|
+
modifiers: Modifier[];
|
|
169
|
+
/** Set when the event was matched by a rule */
|
|
170
|
+
ruleID?: string;
|
|
171
|
+
/** Set for emit-action rules */
|
|
172
|
+
eventID?: string;
|
|
173
|
+
suppressed: boolean;
|
|
174
|
+
/** Display name of the frontmost app at the time of the event */
|
|
175
|
+
app?: string;
|
|
176
|
+
/** Bundle ID of the frontmost app at the time of the event */
|
|
177
|
+
appBundleID?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Map of event name → tuple of listener argument types */
|
|
181
|
+
export interface RethockerEvents {
|
|
182
|
+
/** Native daemon is ready */
|
|
183
|
+
ready: [];
|
|
184
|
+
/** Every key event when listening is active */
|
|
185
|
+
key: [event: KeyEvent];
|
|
186
|
+
/** A rule with action.type="emit" fired */
|
|
187
|
+
event: [eventID: string, ruleID: string];
|
|
188
|
+
/** A sequence rule matched */
|
|
189
|
+
sequence: [ruleID: string, eventID: string | undefined];
|
|
190
|
+
/** Error from the native daemon */
|
|
191
|
+
error: [code: string, message: string];
|
|
192
|
+
/** Accessibility permission denied */
|
|
193
|
+
accessibilityDenied: [];
|
|
194
|
+
/** List of connected keyboards/keypads */
|
|
195
|
+
devices: [devices: DeviceInfo[]];
|
|
196
|
+
/** Native process exited unexpectedly */
|
|
197
|
+
exit: [code: number | null];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Public instance type ─────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export interface RethockerInstance {
|
|
203
|
+
// Lifecycle
|
|
204
|
+
/**
|
|
205
|
+
* Await daemon readiness. The daemon starts automatically in the background
|
|
206
|
+
* when the instance is created, so this is optional. Call it explicitly if
|
|
207
|
+
* you want to handle startup errors (e.g. Accessibility permission denied).
|
|
208
|
+
*/
|
|
209
|
+
start(): Promise<void>;
|
|
210
|
+
stop(): Promise<void>;
|
|
211
|
+
readonly ready: boolean;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Subscribe to an event. Returns an unsubscribe function.
|
|
215
|
+
*
|
|
216
|
+
* Subscribing to `"key"` automatically activates the key stream from the
|
|
217
|
+
* native daemon. When the last `"key"` listener is removed (via the returned
|
|
218
|
+
* unsubscribe function), the stream is deactivated automatically — so there
|
|
219
|
+
* is no overhead when nothing is listening.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* const off = instance.on("key", (e) => console.log(e))
|
|
223
|
+
* off() // unsubscribe — stream stops if this was the last listener
|
|
224
|
+
*/
|
|
225
|
+
on<K extends keyof RethockerEvents>(
|
|
226
|
+
event: K,
|
|
227
|
+
listener: (...args: RethockerEvents[K]) => void,
|
|
228
|
+
): () => void;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Add a key interception rule.
|
|
232
|
+
* @example
|
|
233
|
+
* const rule = instance.addRule(
|
|
234
|
+
* { keyCode: 0, modifiers: ["cmd"] },
|
|
235
|
+
* { type: "suppress" },
|
|
236
|
+
* )
|
|
237
|
+
* rule.disable() // temporarily disable
|
|
238
|
+
* rule.remove() // remove permanently
|
|
239
|
+
*/
|
|
240
|
+
addRule(
|
|
241
|
+
trigger: KeyCombo,
|
|
242
|
+
action: RuleAction,
|
|
243
|
+
options?: RuleOptions,
|
|
244
|
+
): RuleHandle;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Intercept a key combo and call a TypeScript handler.
|
|
248
|
+
* Shorthand for addRule with type:"emit" + on("event").
|
|
249
|
+
* @example
|
|
250
|
+
* const rule = instance.intercept({ keyCode: 0, modifiers: ["cmd"] }, (e) => {
|
|
251
|
+
* console.log("intercepted Cmd+A")
|
|
252
|
+
* })
|
|
253
|
+
*/
|
|
254
|
+
intercept(
|
|
255
|
+
trigger: KeyCombo,
|
|
256
|
+
handler: (event: KeyEvent) => void,
|
|
257
|
+
options?: RuleOptions,
|
|
258
|
+
): RuleHandle;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Add a key sequence rule. Fires when combos are pressed in order within the timeout.
|
|
262
|
+
* @example
|
|
263
|
+
* const seq = instance.addSequence(
|
|
264
|
+
* [{ keyCode: 38, modifiers: ["ctrl"] }, { keyCode: 40, modifiers: ["ctrl"] }],
|
|
265
|
+
* { type: "emit", eventID: "leader" },
|
|
266
|
+
* )
|
|
267
|
+
*/
|
|
268
|
+
addSequence(
|
|
269
|
+
steps: KeyCombo[],
|
|
270
|
+
action: RuleAction,
|
|
271
|
+
options?: SequenceOptions,
|
|
272
|
+
): SequenceHandle;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Allow the process to exit even while the daemon is running. By default,
|
|
276
|
+
* rethocker keeps the event loop alive (so a script with only key rules
|
|
277
|
+
* doesn't exit immediately). Call `unref()` if you want process exit to be
|
|
278
|
+
* determined by your own code, not the daemon's lifetime.
|
|
279
|
+
*
|
|
280
|
+
* The native binary cleans itself up automatically when the parent process
|
|
281
|
+
* exits, so no explicit `stop()` is needed in that case.
|
|
282
|
+
*/
|
|
283
|
+
unref(): void;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Explicitly activate the key event stream (all keypresses emitted on `"key"`).
|
|
287
|
+
* Not needed if you use `on("key", ...)` — that activates the stream automatically.
|
|
288
|
+
* Useful for temporarily pausing the stream without removing listeners.
|
|
289
|
+
*/
|
|
290
|
+
startListening(): void;
|
|
291
|
+
stopListening(): void;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Request the list of connected keyboards/keypads.
|
|
295
|
+
* Results are delivered via the "devices" event.
|
|
296
|
+
*/
|
|
297
|
+
listDevices(): void;
|
|
298
|
+
}
|