@tobq/boardclip 2.1.113 → 2.1.115

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,245 @@
1
+ 'use strict';
2
+ // What the main thread (lib/windows-hook.js) and the hook worker
3
+ // (lib/windows-hook-worker.js) must agree on, and the hook's whole claim
4
+ // decision, kept Win32-free so it is unit-tested without installing a hook
5
+ // (test/windows-hook.test.js):
6
+ // - STATE: the SharedArrayBuffer layout main writes and the worker reads;
7
+ // - the chord table: the Win chords main hands the hook (writeChords) and
8
+ // the accelerator <-> key-code names both sides read them by;
9
+ // - createHookClaimer: which key presses the hook takes.
10
+
11
+ // --- Shared state layout (one byte each; main writes, the worker reads) ---
12
+ const STATE = Object.freeze({
13
+ POPUP_VISIBLE: 0, // the popup is open: plain numpad keys type into it
14
+ SLOT_FIRST: 1, // bytes 1-9: numpad slot N holds a clip
15
+ SLOT_LAST: 9,
16
+ RECORDING: 10, // a Settings shortcut row is recording: chords go to it
17
+ CHORD_COUNT: 11, // live entries in the chord table
18
+ CHORDS: 12, // CHORD_BYTES each: key code, MOD bits, action, slot
19
+ CHORD_BYTES: 4,
20
+ MAX_CHORDS: 12, // show + new note + quick paste 1-9 = 11
21
+ SIZE: 64,
22
+ });
23
+
24
+ const VK = Object.freeze({
25
+ SHIFT: 0x10, CONTROL: 0x11, MENU: 0x12,
26
+ LWIN: 0x5B, RWIN: 0x5C,
27
+ N: 0x4E, V: 0x56,
28
+ NUMPAD1: 0x61, NUMPAD9: 0x69,
29
+ OEM_PLUS: 0xBB,
30
+ });
31
+ const LLKHF_ALTDOWN = 0x20;
32
+ // The modifiers held at a keydown, as bits (a chord matches on all four).
33
+ const MOD = Object.freeze({ WIN: 1, CTRL: 2, ALT: 4, SHIFT: 8 });
34
+ const PRIMARY = MOD.WIN | MOD.CTRL | MOD.ALT; // what makes a press a shortcut
35
+ // Shift / Ctrl / Alt, generic and left / right (the hook sees the sided codes).
36
+ const MODIFIER_KEYS = new Set([VK.SHIFT, VK.CONTROL, VK.MENU, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5]);
37
+
38
+ function modBits(mods) {
39
+ return (mods.win ? MOD.WIN : 0) | (mods.ctrl ? MOD.CTRL : 0) | (mods.alt ? MOD.ALT : 0) | (mods.shift ? MOD.SHIFT : 0);
40
+ }
41
+
42
+ // --- Key names: the accelerator spelling of each key code ---
43
+ // The names the Settings recorder writes for a key it sees (index.html
44
+ // shortcutKeyFromEvent), so a chord the hook hears while recording reads and
45
+ // saves exactly like one the page heard; punctuation at its US position.
46
+ const KEY_NAMES = new Map([
47
+ [0x08, 'Backspace'], [0x09, 'Tab'], [0x0D, 'Enter'], [0x14, 'Capslock'], [0x20, 'Space'],
48
+ [0x21, 'PageUp'], [0x22, 'PageDown'], [0x23, 'End'], [0x24, 'Home'],
49
+ [0x25, 'Left'], [0x26, 'Up'], [0x27, 'Right'], [0x28, 'Down'], [0x2D, 'Insert'], [0x2E, 'Delete'],
50
+ [0x90, 'Numlock'], [0x91, 'Scrolllock'],
51
+ [0xBA, ';'], [VK.OEM_PLUS, '='], [0xBC, ','], [0xBD, 'Minus'], [0xBE, '.'], [0xBF, '/'], [0xC0, '`'],
52
+ [0xDB, '['], [0xDC, 'Backslash'], [0xDD, ']'], [0xDE, "'"],
53
+ ]);
54
+ for (let i = 0; i <= 9; i++) KEY_NAMES.set(0x30 + i, String(i));
55
+ for (let i = 0; i < 26; i++) KEY_NAMES.set(0x41 + i, String.fromCharCode(0x41 + i));
56
+ for (let i = 1; i <= 24; i++) KEY_NAMES.set(0x6F + i, `F${i}`);
57
+ for (let i = 0; i <= 9; i++) KEY_NAMES.set(0x60 + i, `num${i}`);
58
+ // Numpad operators last: the page names them like the main keys, so a saved
59
+ // name reads back as the main key (what Electron registers for it too).
60
+ for (const [vk, name] of [[0x6A, '*'], [0x6B, 'Plus'], [0x6D, 'Minus'], [0x6E, '.'], [0x6F, '/']]) KEY_NAMES.set(vk, name);
61
+
62
+ const KEY_CODES = new Map([['plus', VK.OEM_PLUS], ['esc', 0x1B], ['escape', 0x1B], ['return', 0x0D],
63
+ ['numadd', 0x6B], ['numsub', 0x6D], ['nummult', 0x6A], ['numdiv', 0x6F], ['numdec', 0x6E]]);
64
+ for (const [vk, name] of KEY_NAMES) if (!KEY_CODES.has(name.toLowerCase())) KEY_CODES.set(name.toLowerCase(), vk);
65
+
66
+ const MOD_NAMES = new Map([
67
+ ['super', MOD.WIN], ['meta', MOD.WIN],
68
+ ['control', MOD.CTRL], ['ctrl', MOD.CTRL], ['commandorcontrol', MOD.CTRL], ['commandorctrl', MOD.CTRL], ['cmdorctrl', MOD.CTRL],
69
+ ['alt', MOD.ALT], ['option', MOD.ALT],
70
+ ['shift', MOD.SHIFT],
71
+ ]);
72
+
73
+ function acceleratorParts(accelerator) {
74
+ return String(accelerator || '').split('+').map(p => p.trim().toLowerCase()).filter(Boolean);
75
+ }
76
+
77
+ // A Win chord: Windows' shell holds most of them at the RegisterHotKey layer
78
+ // (Win+V Clipboard History, Win+N notifications, Win+1-9 the taskbar), where
79
+ // a globalShortcut silently loses, so on Windows every one is the hook's.
80
+ function isWinChord(accelerator) {
81
+ return acceleratorParts(accelerator).some(part => MOD_NAMES.get(part) === MOD.WIN);
82
+ }
83
+
84
+ // accelerator -> { vk, mods } for the chord table; null when it is not one
85
+ // key with a shortcut modifier, or names a key the hook has no code for.
86
+ function chordFromAccelerator(accelerator) {
87
+ let mods = 0;
88
+ let vk = 0;
89
+ for (const part of acceleratorParts(accelerator)) {
90
+ if (MOD_NAMES.has(part)) { mods |= MOD_NAMES.get(part); continue; }
91
+ if (vk || !KEY_CODES.has(part)) return null;
92
+ vk = KEY_CODES.get(part);
93
+ }
94
+ return vk && (mods & PRIMARY) ? { vk, mods } : null;
95
+ }
96
+
97
+ // What a recording heard, as the page recorder writes it: Super, Control,
98
+ // Alt, Shift, then the key. key '' = a key with no name ("Press a
99
+ // non-modifier key."); shortcut '' with a key = no shortcut modifier.
100
+ function recordedChord(vk, mods) {
101
+ const key = vk === VK.OEM_PLUS && (mods & MOD.SHIFT) ? 'Plus' : (KEY_NAMES.get(vk) || '');
102
+ const parts = [[MOD.WIN, 'Super'], [MOD.CTRL, 'Control'], [MOD.ALT, 'Alt'], [MOD.SHIFT, 'Shift']]
103
+ .filter(([bit]) => mods & bit).map(([, name]) => name);
104
+ return { key, shortcut: key && (mods & PRIMARY) ? [...parts, key].join('+') : '' };
105
+ }
106
+
107
+ // --- Chord table (main writes it whole, the hook reads it per key) ---
108
+ const ACTIONS = Object.freeze(['show', 'new-note', 'quick-paste']); // stored as index + 1
109
+
110
+ // chords = [{ vk, mods, type, slot? }]. The count drops to 0 while the
111
+ // entries change, so the hook never reads a half-written one. Returns
112
+ // whether every chord fit.
113
+ function writeChords(state, chords) {
114
+ const list = chords.slice(0, STATE.MAX_CHORDS);
115
+ Atomics.store(state, STATE.CHORD_COUNT, 0);
116
+ list.forEach((chord, i) => {
117
+ const at = STATE.CHORDS + i * STATE.CHORD_BYTES;
118
+ state[at] = chord.vk;
119
+ state[at + 1] = chord.mods;
120
+ state[at + 2] = ACTIONS.indexOf(chord.type) + 1;
121
+ state[at + 3] = chord.slot || 0;
122
+ });
123
+ Atomics.store(state, STATE.CHORD_COUNT, list.length);
124
+ return list.length === chords.length;
125
+ }
126
+
127
+ // One verdict per press: a key's auto-repeats and its keyup follow its first
128
+ // keydown's verdict, so a claimed press fires once and swallows its keyup, and
129
+ // an unclaimed one passes untouched to its end (letting go of Ctrl while
130
+ // Win+Ctrl+N is held never turns the repeats into a Win+N).
131
+ // claim() decides a fresh press (only then: repeats read nothing);
132
+ // wasDown() says the key was already down before this keydown. Asked only
133
+ // for an UNCLAIMED held key, whose keydown reached the system: up
134
+ // means its keyup never reached us (released on a desktop the hook
135
+ // cannot see), so this is a fresh press. A claimed key's keydowns
136
+ // were swallowed, so its state says nothing.
137
+ function createPressTracker() {
138
+ const held = new Map(); // vk -> claimed, for keys down since their first keydown
139
+ return {
140
+ holds(vk) { return held.has(vk); },
141
+ event(vk, down, claim, wasDown) {
142
+ if (!down) {
143
+ const claimed = held.get(vk) === true;
144
+ held.delete(vk);
145
+ return { swallow: claimed, fire: false };
146
+ }
147
+ if (held.has(vk)) {
148
+ const claimed = held.get(vk);
149
+ if (claimed || !wasDown || wasDown()) return { swallow: claimed, fire: false };
150
+ }
151
+ const claimed = !!claim();
152
+ held.set(vk, claimed);
153
+ return { swallow: claimed, fire: claimed };
154
+ },
155
+ };
156
+ }
157
+
158
+ // The hook's claim decision for one key event. `state` is the shared-state
159
+ // byte array; io.readMods(flags) and io.isKeyDown(vk) are the worker's Win32
160
+ // reads (GetAsyncKeyState), called only for a key the shared bytes say could
161
+ // be claimed, so any other key costs a few byte reads.
162
+ // recording -> every press with Win, Ctrl or Alt, while the popup is open:
163
+ // post { type: 'record', vk, mods } (Windows never acts on it,
164
+ // so Win+1 records instead of switching to a taskbar app);
165
+ // a chord -> its EXACT modifiers: post { type } ('show', 'new-note') or
166
+ // { type: 'quick-paste', slot };
167
+ // Numpad1-9 -> plain or with Win, the popup closed and the slot assigned:
168
+ // post { type: 'numpad', slot } (open, the digit types into
169
+ // the search; Ctrl/Shift/Alt+digit stay the apps', Alt codes).
170
+ // The Win key itself always passes. A press swallowed while Win was held
171
+ // leaves Windows a bare Win tap, so the Win release after one says
172
+ // markWinUsed and the worker taps a key that keeps the Start menu shut.
173
+ // Returns { swallow, post, markWinUsed }.
174
+ const PASS = Object.freeze({ swallow: false, post: null, markWinUsed: false });
175
+ function createHookClaimer(state, io) {
176
+ const recording = () => state[STATE.RECORDING] === 1 && state[STATE.POPUP_VISIBLE] === 1;
177
+ // The table entry for vk (with exactly `mods`, or any when null), else -1.
178
+ const chordIndex = (vk, mods) => {
179
+ const count = Math.min(state[STATE.CHORD_COUNT], STATE.MAX_CHORDS);
180
+ for (let i = 0; i < count; i++) {
181
+ const at = STATE.CHORDS + i * STATE.CHORD_BYTES;
182
+ if (state[at] === vk && (mods == null || state[at + 1] === mods)) return i;
183
+ }
184
+ return -1;
185
+ };
186
+ const isNumpadDigit = (vk) => vk >= VK.NUMPAD1 && vk <= VK.NUMPAD9;
187
+ const numpadSlot = (vk) => {
188
+ const slot = isNumpadDigit(vk) ? vk - VK.NUMPAD1 + 1 : 0;
189
+ return slot && state[STATE.POPUP_VISIBLE] !== 1 && state[STATE.SLOT_FIRST + slot - 1] === 1 ? slot : 0;
190
+ };
191
+ // Tracked keys get one verdict per press. A numpad digit always is, so a
192
+ // press that began as a digit stays one when the popup closes mid-press.
193
+ const tracked = (vk) => !MODIFIER_KEYS.has(vk) && (recording() || isNumpadDigit(vk) || chordIndex(vk, null) >= 0);
194
+ // Whether this press could be claimed at all, from the shared bytes alone:
195
+ // only then are the modifiers read.
196
+ const claimable = (vk) => recording() || numpadSlot(vk) > 0 || chordIndex(vk, null) >= 0;
197
+ const decide = (vk, mods) => {
198
+ if (recording()) return mods & PRIMARY ? { type: 'record', vk, mods } : null;
199
+ const i = chordIndex(vk, mods);
200
+ if (i >= 0) {
201
+ const at = STATE.CHORDS + i * STATE.CHORD_BYTES;
202
+ const type = ACTIONS[state[at + 2] - 1];
203
+ if (type === 'quick-paste') return { type, slot: state[at + 3] };
204
+ if (type) return { type };
205
+ }
206
+ const slot = numpadSlot(vk);
207
+ return slot && !(mods & (MOD.CTRL | MOD.ALT | MOD.SHIFT)) ? { type: 'numpad', slot } : null;
208
+ };
209
+ const tracker = createPressTracker();
210
+ let winUsed = false;
211
+ return {
212
+ event(vk, down, flags = 0) {
213
+ if (vk === VK.LWIN || vk === VK.RWIN) {
214
+ if (down || !winUsed) return PASS;
215
+ winUsed = false;
216
+ return { swallow: false, post: null, markWinUsed: true };
217
+ }
218
+ if (!tracker.holds(vk) && !(down && tracked(vk))) return PASS;
219
+ let mods = 0;
220
+ let post = null;
221
+ const r = tracker.event(vk, down,
222
+ () => claimable(vk) && !!(post = decide(vk, mods = modBits(io.readMods(flags)))),
223
+ () => io.isKeyDown(vk));
224
+ if (!r.swallow) return PASS;
225
+ if (r.fire && (mods & MOD.WIN)) winUsed = true;
226
+ return { swallow: true, post: r.fire ? post : null, markWinUsed: false };
227
+ },
228
+ };
229
+ }
230
+
231
+ // The worker's io.readMods: GetAsyncKeyState-style isKeyDown(vk) + the hook
232
+ // event's flags (LLKHF_ALTDOWN covers an Alt the async state has not caught).
233
+ function modsFrom(isKeyDown, flags) {
234
+ return {
235
+ win: isKeyDown(VK.LWIN) || isKeyDown(VK.RWIN),
236
+ ctrl: isKeyDown(VK.CONTROL),
237
+ shift: isKeyDown(VK.SHIFT),
238
+ alt: (flags & LLKHF_ALTDOWN) !== 0 || isKeyDown(VK.MENU),
239
+ };
240
+ }
241
+
242
+ module.exports = {
243
+ STATE, VK, MOD, LLKHF_ALTDOWN, modBits, isWinChord, chordFromAccelerator, recordedChord,
244
+ writeChords, createPressTracker, createHookClaimer, modsFrom,
245
+ };
@@ -22,12 +22,13 @@
22
22
 
23
23
  const { parentPort, workerData } = require('worker_threads');
24
24
  const koffi = require('koffi');
25
+ // Which presses the hook claims, and the shared-state layout main agrees on.
26
+ const { STATE, VK, createHookClaimer, modsFrom } = require('./windows-hook-policy');
25
27
 
26
- // --- Shared state (worker reads, main writes) ---
27
- // Layout: [popupVisible, slot1, slot2, ..., slot9, reserved, ...]
28
+ // --- Shared state (worker reads, main writes; layout = STATE) ---
28
29
  const sharedState = workerData && workerData.sharedStateBuffer
29
30
  ? new Uint8Array(workerData.sharedStateBuffer)
30
- : new Uint8Array(16);
31
+ : new Uint8Array(STATE.SIZE);
31
32
 
32
33
  // --- Win32 constants ---
33
34
  const WH_KEYBOARD_LL = 13;
@@ -35,12 +36,6 @@ const WM_KEYDOWN = 0x0100;
35
36
  const WM_KEYUP = 0x0101;
36
37
  const WM_SYSKEYDOWN = 0x0104;
37
38
  const WM_SYSKEYUP = 0x0105;
38
- const VK_LWIN = 0x5B;
39
- const VK_RWIN = 0x5C;
40
- const VK_CONTROL = 0x11;
41
- const VK_V = 0x56;
42
- const VK_NUMPAD1 = 0x61;
43
- const VK_NUMPAD9 = 0x69;
44
39
  const KEYEVENTF_KEYUP = 0x0002;
45
40
 
46
41
  // --- koffi types ---
@@ -107,29 +102,21 @@ const keybd_event = user32.func(
107
102
  'void __stdcall keybd_event(uint8 bVk, uint8 bScan, uint32 dwFlags, uintptr_t dwExtraInfo)'
108
103
  );
109
104
 
110
- let winVChordActive = false;
111
- let winVChordConsumed = false;
112
-
113
- // Auto-repeat suppression for numpad quick-paste. When a key is physically
114
- // held, Windows emits repeated WM_KEYDOWN with no intervening WM_KEYUP. Without
115
- // tracking this, one held (or lag-stretched) press fires the paste many times.
116
- // numpadHeld = keys currently down; numpadIntercepted = keys whose keydown we
117
- // swallowed, so we also swallow their paired keyup.
118
- const numpadHeld = new Set();
119
- const numpadIntercepted = new Set();
120
-
121
- function isWinHeld() {
122
- return (GetAsyncKeyState(VK_LWIN) & 0x8000) !== 0 ||
123
- (GetAsyncKeyState(VK_RWIN) & 0x8000) !== 0;
124
- }
105
+ const isKeyDown = (vk) => (GetAsyncKeyState(vk) & 0x8000) !== 0;
106
+ // Main's chord table, a Settings recording and Numpad1-9: each claimed only
107
+ // with exactly its modifiers, one verdict per press (lib/windows-hook-policy.js).
108
+ const claimer = createHookClaimer(sharedState, {
109
+ isKeyDown,
110
+ readMods: (flags) => modsFrom(isKeyDown, flags),
111
+ });
125
112
 
126
113
  function markWinKeyUsed() {
127
- // If V is swallowed, Windows can still react to the original Win release.
114
+ // If a key is swallowed while Win is held, Windows can still react to the original Win release.
128
115
  // A tiny Ctrl tap marks the Win key as used while allowing the real Win-up
129
116
  // through, so we avoid both Clipboard History and stuck modifier state.
130
117
  try {
131
- keybd_event(VK_CONTROL, 0, 0, 0);
132
- keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
118
+ keybd_event(VK.CONTROL, 0, 0, 0);
119
+ keybd_event(VK.CONTROL, 0, KEYEVENTF_KEYUP, 0);
133
120
  } catch {}
134
121
  }
135
122
 
@@ -149,56 +136,10 @@ const hookCallback = koffi.register((nCode, wParam, lParamExt) => {
149
136
  return CallNextHookEx(null, nCode, wParam, lParamExt);
150
137
  }
151
138
 
152
- const isWinKey = vk === VK_LWIN || vk === VK_RWIN;
153
-
154
- // Win+V → show popup. Checked before Windows Clipboard History can.
155
- if (isWinKey && keyUp && winVChordConsumed) {
156
- markWinKeyUsed();
157
- winVChordConsumed = false;
158
- }
159
-
160
- if (vk === VK_V) {
161
- if (keyDown) {
162
- if (isWinHeld() || winVChordActive) {
163
- if (!winVChordActive) {
164
- winVChordActive = true;
165
- winVChordConsumed = true;
166
- parentPort.postMessage({ type: 'show' });
167
- }
168
- return 1;
169
- }
170
- } else if (winVChordActive) {
171
- winVChordActive = false;
172
- return 1;
173
- }
174
- }
175
-
176
- // Plain Numpad1-9 -> quick-paste when a slot is assigned and the popup
177
- // is closed. When the popup is visible, pass through so the search box
178
- // can accept normal numpad typing. Auto-repeat (held key) fires the paste
179
- // exactly once — repeats are swallowed but not re-dispatched.
180
- if (vk >= VK_NUMPAD1 && vk <= VK_NUMPAD9) {
181
- const slot = vk - VK_NUMPAD1 + 1;
182
- if (keyDown) {
183
- const popupVisible = sharedState[0] === 1;
184
- const slotAssigned = sharedState[slot] === 1;
185
- if (!popupVisible && slotAssigned) {
186
- if (numpadHeld.has(vk)) return 1; // auto-repeat: swallow, don't re-fire
187
- numpadHeld.add(vk);
188
- numpadIntercepted.add(vk);
189
- parentPort.postMessage({ type: 'numpad', slot });
190
- return 1;
191
- }
192
- // Not intercepting this press (popup open or slot unassigned): let it
193
- // type normally and don't track it.
194
- } else if (keyUp) {
195
- numpadHeld.delete(vk);
196
- if (numpadIntercepted.has(vk)) {
197
- numpadIntercepted.delete(vk);
198
- return 1; // swallow the keyup paired with a swallowed keydown
199
- }
200
- }
201
- }
139
+ const verdict = claimer.event(vk, keyDown, kb.flags);
140
+ if (verdict.markWinUsed) markWinKeyUsed();
141
+ if (verdict.post) parentPort.postMessage(verdict.post);
142
+ if (verdict.swallow) return 1;
202
143
  }
203
144
  } catch (err) {
204
145
  // Never let JS exceptions escape the hook — Windows will stall the
@@ -4,34 +4,39 @@
4
4
  // On Windows we can't use Electron's `globalShortcut.register('Super+V')`
5
5
  // because Windows Clipboard History owns Win+V at the RegisterHotKey layer
6
6
  // and silently rejects our registration. This module installs a
7
- // WH_KEYBOARD_LL hook on a dedicated worker thread which intercepts Win+V
8
- // and numpad keys *before* Windows Clipboard History sees them.
7
+ // WH_KEYBOARD_LL hook on a dedicated worker thread which intercepts every
8
+ // Win chord BoardClip uses (Win+V, Win+N and any recorded one, see the chord
9
+ // table in lib/windows-hook-policy.js), the numpad slot keys, and the chord a
10
+ // Settings row is recording, *before* Windows' own handlers see them.
9
11
  //
10
12
  // See lib/windows-hook-worker.js for the hook implementation and the
11
13
  // rationale for running it in a worker.
12
14
  //
13
- // Shared state (popup visible + which numpad slots are assigned) is passed
14
- // via a SharedArrayBuffer because the worker is synchronously blocked in
15
+ // Shared state (popup visible, which numpad slots are assigned, recording,
16
+ // the chord table; layout = STATE in lib/windows-hook-policy.js) is passed via a
17
+ // SharedArrayBuffer because the worker is synchronously blocked in
15
18
  // GetMessageW and can't process postMessage events from its JS event loop.
16
19
 
17
20
  const { Worker } = require('worker_threads');
18
21
  const path = require('path');
22
+ const { STATE, writeChords } = require('./windows-hook-policy');
19
23
 
20
24
  let currentHook = null;
21
25
 
22
- function install({ onShowPopup, onNumpadPaste }) {
26
+ function install({ onShowPopup, onNumpadPaste, onQuickPaste, onNewNote, onRecordKey }) {
23
27
  if (process.platform !== 'win32') {
24
28
  // Non-Windows platforms use Electron's globalShortcut; no-op API here.
25
29
  return {
26
30
  setPopupVisible() {},
27
31
  setSlotAssignments() {},
32
+ setChords() { return true; },
33
+ setRecording() {},
28
34
  uninstall() {},
29
35
  };
30
36
  }
31
37
  if (currentHook) return currentHook;
32
38
 
33
- // Layout: [popupVisible, slot1, slot2, ..., slot9, reserved...]
34
- const sharedStateBuffer = new SharedArrayBuffer(16);
39
+ const sharedStateBuffer = new SharedArrayBuffer(STATE.SIZE);
35
40
  const sharedState = new Uint8Array(sharedStateBuffer);
36
41
 
37
42
  const worker = new Worker(path.join(__dirname, 'windows-hook-worker.js'), {
@@ -46,6 +51,15 @@ function install({ onShowPopup, onNumpadPaste }) {
46
51
  case 'numpad':
47
52
  try { onNumpadPaste(msg.slot); } catch (e) { console.error('[windows-hook] onNumpadPaste:', e); }
48
53
  break;
54
+ case 'quick-paste':
55
+ try { onQuickPaste(msg.slot); } catch (e) { console.error('[windows-hook] onQuickPaste:', e); }
56
+ break;
57
+ case 'new-note':
58
+ try { onNewNote(); } catch (e) { console.error('[windows-hook] onNewNote:', e); }
59
+ break;
60
+ case 'record':
61
+ try { onRecordKey(msg.vk, msg.mods); } catch (e) { console.error('[windows-hook] onRecordKey:', e); }
62
+ break;
49
63
  case 'ready':
50
64
  // Hook installed successfully — no action needed.
51
65
  break;
@@ -67,14 +81,23 @@ function install({ onShowPopup, onNumpadPaste }) {
67
81
 
68
82
  currentHook = {
69
83
  setPopupVisible(visible) {
70
- sharedState[0] = visible ? 1 : 0;
84
+ sharedState[STATE.POPUP_VISIBLE] = visible ? 1 : 0;
71
85
  },
72
86
  setSlotAssignments(assignedSet) {
73
87
  // assignedSet: Set<number> of assigned numpad slot numbers (1-9)
74
88
  for (let n = 1; n <= 9; n++) {
75
- sharedState[n] = assignedSet && assignedSet.has(n) ? 1 : 0;
89
+ sharedState[STATE.SLOT_FIRST + n - 1] = assignedSet && assignedSet.has(n) ? 1 : 0;
76
90
  }
77
91
  },
92
+ // The Win chords the hook claims: [{ vk, mods, type, slot? }] (main's
93
+ // registerShortcuts; empty while a recording runs). False = some did not fit.
94
+ setChords(chords) {
95
+ return writeChords(sharedState, chords);
96
+ },
97
+ // A Settings row is recording: every Win/Ctrl/Alt press goes to it.
98
+ setRecording(on) {
99
+ sharedState[STATE.RECORDING] = on ? 1 : 0;
100
+ },
78
101
  uninstall() {
79
102
  if (!currentHook) return;
80
103
  currentHook = null;