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/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
+ }
package/AGENTS.md DELETED
@@ -1,106 +0,0 @@
1
-
2
- Default to using Bun instead of Node.js.
3
-
4
- - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
5
- - Use `bun test` instead of `jest` or `vitest`
6
- - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
7
- - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
8
- - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
9
- - Use `bunx <package> <command>` instead of `npx <package> <command>`
10
- - Bun automatically loads .env, so don't use dotenv.
11
-
12
- ## APIs
13
-
14
- - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
15
- - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
16
- - `Bun.redis` for Redis. Don't use `ioredis`.
17
- - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
18
- - `WebSocket` is built-in. Don't use `ws`.
19
- - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
20
- - Bun.$`ls` instead of execa.
21
-
22
- ## Testing
23
-
24
- Use `bun test` to run tests.
25
-
26
- ```ts#index.test.ts
27
- import { test, expect } from "bun:test";
28
-
29
- test("hello world", () => {
30
- expect(1).toBe(1);
31
- });
32
- ```
33
-
34
- ## Frontend
35
-
36
- Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
37
-
38
- Server:
39
-
40
- ```ts#index.ts
41
- import index from "./index.html"
42
-
43
- Bun.serve({
44
- routes: {
45
- "/": index,
46
- "/api/users/:id": {
47
- GET: (req) => {
48
- return new Response(JSON.stringify({ id: req.params.id }));
49
- },
50
- },
51
- },
52
- // optional websocket support
53
- websocket: {
54
- open: (ws) => {
55
- ws.send("Hello, world!");
56
- },
57
- message: (ws, message) => {
58
- ws.send(message);
59
- },
60
- close: (ws) => {
61
- // handle close
62
- }
63
- },
64
- development: {
65
- hmr: true,
66
- console: true,
67
- }
68
- })
69
- ```
70
-
71
- HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
72
-
73
- ```html#index.html
74
- <html>
75
- <body>
76
- <h1>Hello, world!</h1>
77
- <script type="module" src="./frontend.tsx"></script>
78
- </body>
79
- </html>
80
- ```
81
-
82
- With the following `frontend.tsx`:
83
-
84
- ```tsx#frontend.tsx
85
- import React from "react";
86
- import { createRoot } from "react-dom/client";
87
-
88
- // import .css files directly and it works
89
- import './index.css';
90
-
91
- const root = createRoot(document.body);
92
-
93
- export default function Frontend() {
94
- return <h1>Hello, world!</h1>;
95
- }
96
-
97
- root.render(<Frontend />);
98
- ```
99
-
100
- Then, run index.ts
101
-
102
- ```sh
103
- bun --hot ./index.ts
104
- ```
105
-
106
- For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
package/bun.lock DELETED
@@ -1,26 +0,0 @@
1
- {
2
- "lockfileVersion": 1,
3
- "configVersion": 1,
4
- "workspaces": {
5
- "": {
6
- "name": "thock",
7
- "devDependencies": {
8
- "@types/bun": "latest",
9
- },
10
- "peerDependencies": {
11
- "typescript": "^5",
12
- },
13
- },
14
- },
15
- "packages": {
16
- "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
17
-
18
- "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
19
-
20
- "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
21
-
22
- "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
23
-
24
- "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
25
- }
26
- }
package/tsconfig.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- // Environment setup & latest features
4
- "lib": ["ESNext"],
5
- "target": "ESNext",
6
- "module": "Preserve",
7
- "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
- "allowJs": true,
10
-
11
- // Bundler mode
12
- "moduleResolution": "bundler",
13
- "allowImportingTsExtensions": true,
14
- "verbatimModuleSyntax": true,
15
- "noEmit": true,
16
-
17
- // Best practices
18
- "strict": true,
19
- "skipLibCheck": true,
20
- "noFallthroughCasesInSwitch": true,
21
- "noUncheckedIndexedAccess": true,
22
- "noImplicitOverride": true,
23
-
24
- // Some stricter flags (disabled by default)
25
- "noUnusedLocals": false,
26
- "noUnusedParameters": false,
27
- "noPropertyAccessFromIndexSignature": false
28
- }
29
- }