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/rethocker.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rethocker() — the main entry point.
|
|
3
|
+
*
|
|
4
|
+
* Wires together the daemon, rule engine, and rule registration into
|
|
5
|
+
* the public RethockerHandle interface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { createDaemon } from "./daemon.ts";
|
|
10
|
+
import { registerRule } from "./register-rule.ts";
|
|
11
|
+
import type {
|
|
12
|
+
RethockerHandle,
|
|
13
|
+
RethockerOptions,
|
|
14
|
+
RethockerRule,
|
|
15
|
+
} from "./rule-types.ts";
|
|
16
|
+
import type { RethockerEvents, RuleHandle, SequenceHandle } from "./types.ts";
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
HandlerRule,
|
|
20
|
+
RemapRule,
|
|
21
|
+
RethockerHandle,
|
|
22
|
+
RethockerOptions,
|
|
23
|
+
RethockerRule,
|
|
24
|
+
ShellRule,
|
|
25
|
+
} from "./rule-types.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a rethocker instance with an optional initial set of rules.
|
|
29
|
+
* The native daemon starts automatically in the background.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const rk = rethocker([
|
|
33
|
+
* { key: "capsLock", remap: "escape" },
|
|
34
|
+
* { key: "Cmd+Shift+Space", execute: "open -a 'Alfred 5'" },
|
|
35
|
+
* { key: "Ctrl+J Ctrl+K", handler: () => console.log("sequence!"), consume: true },
|
|
36
|
+
* ])
|
|
37
|
+
*
|
|
38
|
+
* rk.on("accessibilityDenied", () => { ... })
|
|
39
|
+
* rk.disable() // pause all rules
|
|
40
|
+
* rk.enable() // resume all rules
|
|
41
|
+
*/
|
|
42
|
+
export function rethocker(
|
|
43
|
+
rules: RethockerRule | RethockerRule[] = [],
|
|
44
|
+
options: RethockerOptions = {},
|
|
45
|
+
): RethockerHandle {
|
|
46
|
+
const binaryPath =
|
|
47
|
+
options.binaryPath ??
|
|
48
|
+
join(import.meta.dir, "..", "bin", "rethocker-native");
|
|
49
|
+
|
|
50
|
+
const daemon = createDaemon(binaryPath);
|
|
51
|
+
|
|
52
|
+
// ─── Rule handles ─────────────────────────────────────────────────────────
|
|
53
|
+
const handles = new Map<string, RuleHandle | SequenceHandle>();
|
|
54
|
+
|
|
55
|
+
// Start daemon in background; errors surface via rk.on("error") or await rk.start()
|
|
56
|
+
// Silent — errors surface via rk.on("error") or await rk.start()
|
|
57
|
+
daemon.start().catch((_e: unknown) => {
|
|
58
|
+
/* intentional */
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function add(toAdd: RethockerRule | RethockerRule[]): void {
|
|
62
|
+
const list = Array.isArray(toAdd) ? toAdd : [toAdd];
|
|
63
|
+
for (const rule of list) {
|
|
64
|
+
const handle = registerRule(daemon.send, daemon.emitter, rule);
|
|
65
|
+
handles.set(handle.id, handle);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
add(rules);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
add,
|
|
73
|
+
|
|
74
|
+
remove(id: string): void {
|
|
75
|
+
handles.get(id)?.remove();
|
|
76
|
+
handles.delete(id);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
enable(id?: string): void {
|
|
80
|
+
if (id !== undefined) {
|
|
81
|
+
handles.get(id)?.enable();
|
|
82
|
+
} else {
|
|
83
|
+
for (const h of handles.values()) h.enable();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
disable(id?: string): void {
|
|
88
|
+
if (id !== undefined) {
|
|
89
|
+
handles.get(id)?.disable();
|
|
90
|
+
} else {
|
|
91
|
+
for (const h of handles.values()) h.disable();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
on<K extends keyof RethockerEvents>(
|
|
96
|
+
event: K,
|
|
97
|
+
listener: (...args: RethockerEvents[K]) => void,
|
|
98
|
+
): () => void {
|
|
99
|
+
daemon.emitter.on(event, listener);
|
|
100
|
+
if (event === "key") daemon.send({ cmd: "listen_all", enabled: true });
|
|
101
|
+
return () => {
|
|
102
|
+
daemon.emitter.off(event, listener);
|
|
103
|
+
if (event === "key" && daemon.emitter.listenerCount("key") === 0) {
|
|
104
|
+
daemon.send({ cmd: "listen_all", enabled: false });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async execute(command: string | string[]): Promise<void> {
|
|
110
|
+
const cmd = Array.isArray(command) ? command.join(" && ") : command;
|
|
111
|
+
const proc = Bun.spawn(["/bin/sh", "-c", cmd], {
|
|
112
|
+
stdout: "inherit",
|
|
113
|
+
stderr: "inherit",
|
|
114
|
+
});
|
|
115
|
+
await proc.exited;
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
start: () => daemon.start(),
|
|
119
|
+
stop: () => daemon.stop(),
|
|
120
|
+
unref: () => daemon.unref(),
|
|
121
|
+
get ready() {
|
|
122
|
+
return daemon.ready;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule engine — low-level rule/sequence registration and handle creation.
|
|
3
|
+
*
|
|
4
|
+
* Knows how to translate typed rule objects into IPC commands (via `send`),
|
|
5
|
+
* and wires up emitter listeners for intercept/handler patterns.
|
|
6
|
+
* Has no knowledge of the high-level string-based rule syntax.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TypedEmitter } from "./daemon.ts";
|
|
10
|
+
import type {
|
|
11
|
+
KeyCombo,
|
|
12
|
+
KeyEvent,
|
|
13
|
+
RuleAction,
|
|
14
|
+
RuleHandle,
|
|
15
|
+
RuleOptions,
|
|
16
|
+
SequenceHandle,
|
|
17
|
+
SequenceOptions,
|
|
18
|
+
} from "./types.ts";
|
|
19
|
+
import { DEFAULT_SEQUENCE_TIMEOUT_MS } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
let _seq = 0;
|
|
22
|
+
export function genID(prefix: string) {
|
|
23
|
+
return `${prefix}_${Date.now()}_${++_seq}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeHandle(
|
|
27
|
+
id: string,
|
|
28
|
+
send: (obj: Record<string, unknown>) => void,
|
|
29
|
+
removeCmd: string,
|
|
30
|
+
toggleCmd: string,
|
|
31
|
+
): RuleHandle {
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
remove: () => send({ cmd: removeCmd, id }),
|
|
35
|
+
enable: () => send({ cmd: toggleCmd, id, enabled: true }),
|
|
36
|
+
disable: () => send({ cmd: toggleCmd, id, enabled: false }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function addRule(
|
|
41
|
+
send: (obj: Record<string, unknown>) => void,
|
|
42
|
+
trigger: KeyCombo,
|
|
43
|
+
action: RuleAction,
|
|
44
|
+
opts: RuleOptions = {},
|
|
45
|
+
): RuleHandle {
|
|
46
|
+
const id = opts.id ?? genID("rule");
|
|
47
|
+
send({
|
|
48
|
+
cmd: "add_rule",
|
|
49
|
+
id,
|
|
50
|
+
trigger,
|
|
51
|
+
action,
|
|
52
|
+
conditions: opts.conditions ?? {},
|
|
53
|
+
onKeyUp: opts.onKeyUp ?? false,
|
|
54
|
+
enabled: !(opts.disabled ?? false),
|
|
55
|
+
});
|
|
56
|
+
return makeHandle(id, send, "remove_rule", "set_rule_enabled");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function addSequence(
|
|
60
|
+
send: (obj: Record<string, unknown>) => void,
|
|
61
|
+
steps: KeyCombo[],
|
|
62
|
+
action: RuleAction,
|
|
63
|
+
opts: SequenceOptions = {},
|
|
64
|
+
): SequenceHandle {
|
|
65
|
+
const id = opts.id ?? genID("seq");
|
|
66
|
+
send({
|
|
67
|
+
cmd: "add_sequence",
|
|
68
|
+
id,
|
|
69
|
+
steps,
|
|
70
|
+
action,
|
|
71
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_SEQUENCE_TIMEOUT_MS,
|
|
72
|
+
consume: opts.consume ?? false,
|
|
73
|
+
conditions: opts.conditions ?? {},
|
|
74
|
+
enabled: !(opts.disabled ?? false),
|
|
75
|
+
});
|
|
76
|
+
return makeHandle(id, send, "remove_sequence", "set_sequence_enabled");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function intercept(
|
|
80
|
+
send: (obj: Record<string, unknown>) => void,
|
|
81
|
+
emitter: TypedEmitter,
|
|
82
|
+
trigger: KeyCombo,
|
|
83
|
+
handler: (e: KeyEvent) => void,
|
|
84
|
+
opts: RuleOptions = {},
|
|
85
|
+
): RuleHandle {
|
|
86
|
+
const eventID = genID("intercept");
|
|
87
|
+
const handle = addRule(send, trigger, { type: "emit", eventID }, opts);
|
|
88
|
+
emitter.on("event", (eid, ruleID) => {
|
|
89
|
+
if (eid === eventID) {
|
|
90
|
+
handler({
|
|
91
|
+
type: "keydown",
|
|
92
|
+
keyCode: trigger.keyCode,
|
|
93
|
+
modifiers: trigger.modifiers ?? [],
|
|
94
|
+
ruleID,
|
|
95
|
+
eventID,
|
|
96
|
+
suppressed: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return handle;
|
|
101
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-facing rule type definitions for the high-level rethocker() API.
|
|
3
|
+
*
|
|
4
|
+
* Three discriminated variants — TypeScript narrows to the correct set of
|
|
5
|
+
* fields based on which discriminant key is present, giving users precise
|
|
6
|
+
* autocomplete for each rule type.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { KeyEvent, RethockerEvents, RuleConditions } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
// ─── Rule variants ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
/** Common fields available on every rule. */
|
|
14
|
+
interface RuleBase {
|
|
15
|
+
/**
|
|
16
|
+
* The key or key sequence that triggers this rule.
|
|
17
|
+
*
|
|
18
|
+
* Single key: `"escape"` | `"Cmd+A"` | `"Cmd+Shift+K"`
|
|
19
|
+
* Sequence: `"Cmd+R T"` | `"Ctrl+J Ctrl+K"` (space-separated steps)
|
|
20
|
+
*
|
|
21
|
+
* Modifier names are case-insensitive: Cmd, Shift, Alt/Opt/Option, Ctrl/Control, Fn
|
|
22
|
+
* Key names match the `Key` constant (also case-insensitive).
|
|
23
|
+
* Common aliases: esc, enter, backspace, del, caps
|
|
24
|
+
*/
|
|
25
|
+
key: string;
|
|
26
|
+
/** Optional stable ID. Auto-generated if omitted. */
|
|
27
|
+
id?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Restrict this rule to fire only when one of these apps is frontmost.
|
|
30
|
+
*
|
|
31
|
+
* Pass a bundle ID (contains a dot) or a display name (prefix match,
|
|
32
|
+
* case-insensitive). Multiple values are OR-ed.
|
|
33
|
+
*
|
|
34
|
+
* Auto-detection: `"com.figma.Desktop"` → bundle ID, `"Figma"` → display name.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* app: "com.figma.Desktop"
|
|
38
|
+
* app: "Terminal"
|
|
39
|
+
* app: ["Safari", "Chrome", "Firefox"]
|
|
40
|
+
*/
|
|
41
|
+
app?: string | string[];
|
|
42
|
+
/** Advanced: full condition control. Merged with `app` if both provided. */
|
|
43
|
+
conditions?: RuleConditions;
|
|
44
|
+
/** Start the rule disabled. */
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Remap a key to a different key. */
|
|
49
|
+
export interface RemapRule extends RuleBase {
|
|
50
|
+
/**
|
|
51
|
+
* The key to emit instead. Same syntax as `key`:
|
|
52
|
+
* `"escape"` | `"Cmd+Enter"` etc.
|
|
53
|
+
*/
|
|
54
|
+
remap: string;
|
|
55
|
+
execute?: never;
|
|
56
|
+
handler?: never;
|
|
57
|
+
consume?: never;
|
|
58
|
+
sequenceTimeoutMs?: never;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Run a shell command (or multiple) when the key fires. The key is consumed. */
|
|
62
|
+
export interface ShellRule extends RuleBase {
|
|
63
|
+
/**
|
|
64
|
+
* Shell command to run (via `/bin/sh -c`).
|
|
65
|
+
* Pass an array to run multiple commands sequentially — they are joined with `&&`.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* execute: actions.window.halfLeft()
|
|
69
|
+
* execute: [actions.window.halfLeft(), actions.app.focus("Slack")]
|
|
70
|
+
*/
|
|
71
|
+
execute: string | string[];
|
|
72
|
+
/**
|
|
73
|
+
* For sequences: consume all intermediate key events so they never reach
|
|
74
|
+
* the active app. Single-key execute rules always consume the trigger key.
|
|
75
|
+
* @default false
|
|
76
|
+
*/
|
|
77
|
+
consume?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* For sequences: max ms between consecutive steps.
|
|
80
|
+
* @default DEFAULT_SEQUENCE_TIMEOUT_MS (5000)
|
|
81
|
+
*/
|
|
82
|
+
sequenceTimeoutMs?: number;
|
|
83
|
+
remap?: never;
|
|
84
|
+
handler?: never;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Call a TypeScript handler when the key fires. The key is consumed. */
|
|
88
|
+
export interface HandlerRule extends RuleBase {
|
|
89
|
+
/** Called when the key fires. */
|
|
90
|
+
handler: (event: KeyEvent) => void;
|
|
91
|
+
/**
|
|
92
|
+
* For sequences: consume all intermediate key events.
|
|
93
|
+
* @default false
|
|
94
|
+
*/
|
|
95
|
+
consume?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* For sequences: max ms between consecutive steps.
|
|
98
|
+
* @default DEFAULT_SEQUENCE_TIMEOUT_MS (5000)
|
|
99
|
+
*/
|
|
100
|
+
sequenceTimeoutMs?: number;
|
|
101
|
+
remap?: never;
|
|
102
|
+
execute?: never;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type RethockerRule = RemapRule | ShellRule | HandlerRule;
|
|
106
|
+
|
|
107
|
+
// ─── Public handle ────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
export interface RethockerHandle {
|
|
110
|
+
/** Add one or more rules. */
|
|
111
|
+
add(rules: RethockerRule | RethockerRule[]): void;
|
|
112
|
+
/** Remove a rule permanently by ID. */
|
|
113
|
+
remove(id: string): void;
|
|
114
|
+
/**
|
|
115
|
+
* Enable rules.
|
|
116
|
+
* - No argument: enable all rules on this handle.
|
|
117
|
+
* - With id: enable a specific rule by ID.
|
|
118
|
+
*/
|
|
119
|
+
enable(id?: string): void;
|
|
120
|
+
/**
|
|
121
|
+
* Disable rules without removing them.
|
|
122
|
+
* - No argument: disable all rules on this handle.
|
|
123
|
+
* - With id: disable a specific rule by ID.
|
|
124
|
+
*/
|
|
125
|
+
disable(id?: string): void;
|
|
126
|
+
/**
|
|
127
|
+
* Subscribe to an event. Returns an unsubscribe function.
|
|
128
|
+
* Subscribing to `"key"` automatically activates the key stream.
|
|
129
|
+
*/
|
|
130
|
+
on<K extends keyof RethockerEvents>(
|
|
131
|
+
event: K,
|
|
132
|
+
listener: (...args: RethockerEvents[K]) => void,
|
|
133
|
+
): () => void;
|
|
134
|
+
/**
|
|
135
|
+
* Run a shell command (or multiple) immediately, outside of any rule.
|
|
136
|
+
* Accepts the same value as the `execute` field on a rule.
|
|
137
|
+
* Returns a promise that resolves when the command exits.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* await rk.execute(actions.media.playPause())
|
|
141
|
+
* await rk.execute([actions.window.halfLeft(), actions.app.focus("Slack")])
|
|
142
|
+
*
|
|
143
|
+
* // Inside a handler:
|
|
144
|
+
* { key: "Ctrl+J", handler: async () => { await rk.execute(actions.system.sleep()) } }
|
|
145
|
+
*/
|
|
146
|
+
execute(command: string | string[]): Promise<void>;
|
|
147
|
+
|
|
148
|
+
/** Stop the native daemon and clean up. */
|
|
149
|
+
stop(): Promise<void>;
|
|
150
|
+
/**
|
|
151
|
+
* Allow the process to exit while the daemon is running.
|
|
152
|
+
* By default rethocker keeps the event loop alive.
|
|
153
|
+
*/
|
|
154
|
+
unref(): void;
|
|
155
|
+
/**
|
|
156
|
+
* Await daemon readiness. Optional — the daemon starts automatically.
|
|
157
|
+
* Useful for explicitly handling startup errors.
|
|
158
|
+
*/
|
|
159
|
+
start(): Promise<void>;
|
|
160
|
+
/** Whether the daemon is running and ready. */
|
|
161
|
+
readonly ready: boolean;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Options ──────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export interface RethockerOptions {
|
|
167
|
+
/** Override the path to the native binary. Defaults to the bundled binary. */
|
|
168
|
+
binaryPath?: string;
|
|
169
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key code explorer: prints every key event with its keyCode, modifiers,
|
|
3
|
+
* and the currently active app.
|
|
4
|
+
*
|
|
5
|
+
* Useful for discovering key codes and verifying app conditions before
|
|
6
|
+
* writing rules.
|
|
7
|
+
*
|
|
8
|
+
* Note on device discrimination: CGEventTap does not expose which physical
|
|
9
|
+
* keyboard generated an event. Use key codes to distinguish devices — numpad
|
|
10
|
+
* keys have dedicated codes (Numpad0–Numpad9, NumpadEnter, etc.) that differ
|
|
11
|
+
* from the main keyboard, so "NumpadEnter" and "return" are already distinct.
|
|
12
|
+
*
|
|
13
|
+
* Run with:
|
|
14
|
+
* bun src/scripts/debug-keys.ts
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { KeyEvent } from "../index.ts";
|
|
18
|
+
import { rethocker } from "../index.ts";
|
|
19
|
+
|
|
20
|
+
const rk = rethocker();
|
|
21
|
+
|
|
22
|
+
rk.on("accessibilityDenied", () => {
|
|
23
|
+
console.error(
|
|
24
|
+
"Accessibility permission required.\n" +
|
|
25
|
+
"Go to System Settings → Privacy & Security → Accessibility\n" +
|
|
26
|
+
"and enable this terminal / app.",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await rk.start();
|
|
31
|
+
console.log("Press any key (Ctrl+C to quit):\n");
|
|
32
|
+
|
|
33
|
+
rk.on("key", ({ type, keyCode, modifiers, app, appBundleID }: KeyEvent) => {
|
|
34
|
+
const parts: string[] = [
|
|
35
|
+
`${type.padEnd(7)} keyCode: ${String(keyCode).padEnd(4)}`,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (modifiers.length > 0) {
|
|
39
|
+
parts.push(`mods: [${modifiers.join(", ")}]`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (app) parts.push(`app: ${app}${appBundleID ? ` (${appBundleID})` : ""}`);
|
|
43
|
+
|
|
44
|
+
console.log(parts.join(" "));
|
|
45
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: rethocker capabilities showcase
|
|
3
|
+
*
|
|
4
|
+
* Run with:
|
|
5
|
+
* bun src/scripts/example.ts
|
|
6
|
+
*
|
|
7
|
+
* Requires Accessibility permission on first run.
|
|
8
|
+
* macOS will prompt automatically.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { actions, Key, rethocker } from "../index.ts";
|
|
12
|
+
|
|
13
|
+
const rk = rethocker([
|
|
14
|
+
// ── Remap Caps Lock → Escape ──────────────────────────────────────────────
|
|
15
|
+
//
|
|
16
|
+
// Caps Lock fires a special "flagsChanged" event (not a normal keydown),
|
|
17
|
+
// but rethocker handles it transparently — this just works.
|
|
18
|
+
{
|
|
19
|
+
key: "capsLock",
|
|
20
|
+
remap: "escape",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
// you can also remap chords
|
|
24
|
+
key: "Ctrl+H E",
|
|
25
|
+
// or remap to a chord, use `Key` for autocomplete and typesafety
|
|
26
|
+
remap: `h e l l o Shift+n1 n1 ${Key.delete}`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: `${Key.brightnessDown} ${Key.brightnessUp}`,
|
|
30
|
+
/**
|
|
31
|
+
* install zwussh first:
|
|
32
|
+
* brew tap benjamine/homebrew-tap
|
|
33
|
+
* brew install zwuush
|
|
34
|
+
*/
|
|
35
|
+
execute: "zwuush https://benjamine.github.io/zwuush/wow.mov",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "Cmd+R T",
|
|
39
|
+
sequenceTimeoutMs: 10_000,
|
|
40
|
+
// optionally include or exclude specific apps (by bundle identifier).
|
|
41
|
+
app: ["!com.google.Chrome", "!com.apple.Safari"],
|
|
42
|
+
// optionally consume the key event so it doesn't reach the app
|
|
43
|
+
consume: true,
|
|
44
|
+
// execute: `osascript -e 'display notification "Cmd+R → T sequence detected" with title "rethocker"'`,
|
|
45
|
+
handler: async () => {
|
|
46
|
+
// using a custom handler allows for more complex actions, e.g. multiple commands, async/await, etc.
|
|
47
|
+
await rk.execute(actions.window.halfTop());
|
|
48
|
+
// actions. provide quick access to common OSX tasks like window management
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
rk.on("accessibilityDenied", () => {
|
|
54
|
+
console.error(
|
|
55
|
+
"Accessibility permission required.\n" +
|
|
56
|
+
"Go to System Settings → Privacy & Security → Accessibility\n" +
|
|
57
|
+
"and enable this terminal / app.",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
rk.on("error", (code, message) => {
|
|
62
|
+
console.error(`[rethocker error] ${code}: ${message}`);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
rk.on("exit", (code) => {
|
|
66
|
+
console.error(
|
|
67
|
+
`Native daemon exited unexpectedly (code ${code}). Restarting...`,
|
|
68
|
+
);
|
|
69
|
+
rk.start().catch(console.error);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log("rethocker running. Press Ctrl+C to quit.");
|
|
73
|
+
console.log(" • Caps Lock → Escape");
|
|
74
|
+
console.log(" • Cmd+R then T (within 10s, consumed) → macOS notification");
|