rethocker 0.1.0 → 0.1.2

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.
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * rethocker log
5
+ *
6
+ * Live key monitor: shows every keypress in rethocker rule syntax so you can
7
+ * copy-paste directly into your config. Keys pressed in quick succession appear
8
+ * on the same line separated by spaces (like a sequence trigger). A new line
9
+ * starts after 3 seconds of silence.
10
+ *
11
+ * Run with:
12
+ * bunx rethocker log
13
+ */
14
+
15
+ import { KEY_CODE_MAP, Key } from "../keys.ts";
16
+ import { rethocker } from "../rethocker.ts";
17
+ import type { KeyEvent, Modifier } from "../types.ts";
18
+
19
+ // ─── Reverse map: keyCode → Key name ─────────────────────────────────────────
20
+
21
+ const CODE_TO_KEY: Map<number, string> = new Map(
22
+ Object.entries(KEY_CODE_MAP).map(([name, code]) => [code, name]),
23
+ );
24
+
25
+ // KEY_CODE_MAP has lowercase keys (e.g. "capslock") but Key has camelCase
26
+ // ("capsLock"). Build a lookup from lowercase → camelCase Key name.
27
+ const LOWER_TO_KEY_NAME: Map<string, string> = new Map(
28
+ Object.keys(Key).map((k) => [k.toLowerCase(), k]),
29
+ );
30
+
31
+ function keyCodeToName(code: number): string | null {
32
+ const lower = CODE_TO_KEY.get(code);
33
+ if (!lower) return null;
34
+ return LOWER_TO_KEY_NAME.get(lower) ?? lower;
35
+ }
36
+
37
+ // ─── Modifier display ─────────────────────────────────────────────────────────
38
+
39
+ // Maps each modifier to its display label, preserving side-specificity.
40
+ // When a side-specific modifier is present (e.g. leftCmd), we show "LeftCmd"
41
+ // rather than the generic "Cmd" — so the output is copy-pasteable as a rule.
42
+ const MODIFIER_LABEL: Record<Modifier, string> = {
43
+ cmd: "Cmd",
44
+ shift: "Shift",
45
+ alt: "Alt",
46
+ ctrl: "Ctrl",
47
+ fn: "Fn",
48
+ leftCmd: "LeftCmd",
49
+ rightCmd: "RightCmd",
50
+ leftShift: "LeftShift",
51
+ rightShift: "RightShift",
52
+ leftAlt: "LeftAlt",
53
+ rightAlt: "RightAlt",
54
+ leftCtrl: "LeftCtrl",
55
+ rightCtrl: "RightCtrl",
56
+ };
57
+
58
+ // Order in which modifiers appear in the display string (conventional).
59
+ const MODIFIER_ORDER: Modifier[] = [
60
+ "leftCtrl",
61
+ "rightCtrl",
62
+ "ctrl",
63
+ "leftAlt",
64
+ "rightAlt",
65
+ "alt",
66
+ "leftShift",
67
+ "rightShift",
68
+ "shift",
69
+ "leftCmd",
70
+ "rightCmd",
71
+ "cmd",
72
+ "fn",
73
+ ];
74
+
75
+ function formatModifiers(modifiers: Modifier[]): string[] {
76
+ const mods = new Set(modifiers);
77
+ // If a side-specific modifier is present, suppress the generic one to avoid
78
+ // "LeftCmd+Cmd+A" — macOS always sends both, but we only want the specific.
79
+ const suppress = new Set<Modifier>();
80
+ if (mods.has("leftCmd") || mods.has("rightCmd")) suppress.add("cmd");
81
+ if (mods.has("leftShift") || mods.has("rightShift")) suppress.add("shift");
82
+ if (mods.has("leftAlt") || mods.has("rightAlt")) suppress.add("alt");
83
+ if (mods.has("leftCtrl") || mods.has("rightCtrl")) suppress.add("ctrl");
84
+
85
+ return MODIFIER_ORDER.filter((m) => mods.has(m) && !suppress.has(m)).map(
86
+ (m) => MODIFIER_LABEL[m],
87
+ );
88
+ }
89
+
90
+ function formatCombo(event: KeyEvent): string | null {
91
+ const key = keyCodeToName(event.keyCode);
92
+ if (key === null) return null;
93
+ const mods = formatModifiers(event.modifiers);
94
+ return mods.length > 0 ? `${mods.join("+")}+${key}` : key;
95
+ }
96
+
97
+ // ─── Chord / sequence state ───────────────────────────────────────────────────
98
+
99
+ const SILENCE_MS = 3000;
100
+
101
+ // Bare modifier key codes — suppress these so they only appear when combined
102
+ // with a real key (e.g. Cmd+A, not a lone Cmd press).
103
+ const MODIFIER_ONLY_CODES: Set<number> = new Set(
104
+ [
105
+ KEY_CODE_MAP.leftcmd,
106
+ KEY_CODE_MAP.rightcmd,
107
+ KEY_CODE_MAP.leftshift,
108
+ KEY_CODE_MAP.rightshift,
109
+ KEY_CODE_MAP.leftalt,
110
+ KEY_CODE_MAP.rightalt,
111
+ KEY_CODE_MAP.leftctrl,
112
+ KEY_CODE_MAP.rightctrl,
113
+ KEY_CODE_MAP.fn,
114
+ ].filter((c): c is number => c !== undefined),
115
+ );
116
+
117
+ // Map modifier keyCode → its display label (same as MODIFIER_LABEL values)
118
+ const MODIFIER_CODE_LABEL: Map<number, string> = new Map(
119
+ (
120
+ [
121
+ ["leftcmd", "LeftCmd"],
122
+ ["rightcmd", "RightCmd"],
123
+ ["leftshift", "LeftShift"],
124
+ ["rightshift", "RightShift"],
125
+ ["leftalt", "LeftAlt"],
126
+ ["rightalt", "RightAlt"],
127
+ ["leftctrl", "LeftCtrl"],
128
+ ["rightctrl", "RightCtrl"],
129
+ ["fn", "Fn"],
130
+ ] as const
131
+ ).flatMap(([key, label]) => {
132
+ const code = KEY_CODE_MAP[key];
133
+ return code !== undefined ? [[code, label] as [number, string]] : [];
134
+ }),
135
+ );
136
+
137
+ const CAPS_LOCK_CODE = KEY_CODE_MAP.capslock;
138
+
139
+ // ─── Main entry point (called by cli.ts) ─────────────────────────────────────
140
+
141
+ export async function runLog() {
142
+ const tokens: string[] = [];
143
+ let silenceTimer: ReturnType<typeof setTimeout> | null = null;
144
+ const pendingModifiers: Map<number, string> = new Map();
145
+
146
+ function endLine() {
147
+ tokens.length = 0;
148
+ pendingModifiers.clear();
149
+ silenceTimer = null;
150
+ process.stdout.write("\n");
151
+ }
152
+
153
+ function resetSilenceTimer() {
154
+ if (silenceTimer !== null) clearTimeout(silenceTimer);
155
+ silenceTimer = setTimeout(endLine, SILENCE_MS);
156
+ }
157
+
158
+ function onKey(event: KeyEvent) {
159
+ if (event.type === "flags") {
160
+ if (event.keyCode === CAPS_LOCK_CODE) {
161
+ tokens.push("capsLock");
162
+ process.stdout.write(`\r\x1b[K${tokens.join(" ")}`);
163
+ resetSilenceTimer();
164
+ return;
165
+ }
166
+
167
+ const label = MODIFIER_CODE_LABEL.get(event.keyCode);
168
+ if (label === undefined) return;
169
+
170
+ if (pendingModifiers.has(event.keyCode)) {
171
+ pendingModifiers.delete(event.keyCode);
172
+ tokens.push(label);
173
+ process.stdout.write(`\r\x1b[K${tokens.join(" ")}`);
174
+ resetSilenceTimer();
175
+ } else {
176
+ pendingModifiers.set(event.keyCode, label);
177
+ }
178
+ return;
179
+ }
180
+
181
+ if (event.type === "keyup") return;
182
+
183
+ if (MODIFIER_ONLY_CODES.has(event.keyCode)) return;
184
+
185
+ pendingModifiers.clear();
186
+
187
+ const combo = formatCombo(event);
188
+ if (combo === null) return;
189
+ tokens.push(combo);
190
+
191
+ process.stdout.write(`\r\x1b[K${tokens.join(" ")}`);
192
+ resetSilenceTimer();
193
+ }
194
+
195
+ const rk = rethocker();
196
+
197
+ rk.on("accessibilityDenied", () => {
198
+ console.error(
199
+ "\nAccessibility permission required.\n" +
200
+ "Go to System Settings → Privacy & Security → Accessibility\n" +
201
+ "and enable your terminal app, then try again.",
202
+ );
203
+ process.exit(1);
204
+ });
205
+
206
+ await rk.start();
207
+
208
+ function cleanup() {
209
+ if (silenceTimer !== null) {
210
+ clearTimeout(silenceTimer);
211
+ if (tokens.length > 0) process.stdout.write("\n");
212
+ }
213
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
214
+ }
215
+ process.on("exit", cleanup);
216
+
217
+ if (process.stdin.isTTY) {
218
+ process.stdin.setRawMode(true);
219
+ process.stdin.on("data", (chunk: Buffer) => {
220
+ if (chunk[0] === 0x03) {
221
+ cleanup();
222
+ process.exit(0);
223
+ }
224
+ });
225
+ }
226
+
227
+ console.log("rethocker log — press any key (Ctrl+C to quit)");
228
+ console.log(
229
+ "(note: media keys always show without modifiers — macOS limitation)\n",
230
+ );
231
+
232
+ rk.on("key", onKey);
233
+ }
package/src/types.ts CHANGED
@@ -147,19 +147,6 @@ export interface SequenceHandle {
147
147
  disable(): void;
148
148
  }
149
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
150
  // ─── Events ──────────────────────────────────────────────────────────────────
164
151
 
165
152
  export interface KeyEvent {
@@ -191,8 +178,6 @@ export interface RethockerEvents {
191
178
  error: [code: string, message: string];
192
179
  /** Accessibility permission denied */
193
180
  accessibilityDenied: [];
194
- /** List of connected keyboards/keypads */
195
- devices: [devices: DeviceInfo[]];
196
181
  /** Native process exited unexpectedly */
197
182
  exit: [code: number | null];
198
183
  }
@@ -289,10 +274,4 @@ export interface RethockerInstance {
289
274
  */
290
275
  startListening(): void;
291
276
  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
277
  }