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/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
+ }