@tobq/boardclip 0.0.0-stage → 2.1.107

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -2
  3. package/assets/boardclip-icon.png +0 -0
  4. package/bin/boardclip.js +43 -0
  5. package/editor-preload.js +71 -0
  6. package/editor.html +220 -0
  7. package/icon.png +0 -0
  8. package/icon@2x.png +0 -0
  9. package/iconTemplate.png +0 -0
  10. package/iconTemplate@2x.png +0 -0
  11. package/index.html +1339 -0
  12. package/lib/appearance.js +183 -0
  13. package/lib/auto-update.js +306 -0
  14. package/lib/backup-worker.js +46 -0
  15. package/lib/backup.js +275 -0
  16. package/lib/blob-field-store.js +275 -0
  17. package/lib/blob-store.js +68 -0
  18. package/lib/build-info.js +89 -0
  19. package/lib/clip-blob-store.js +161 -0
  20. package/lib/clip-revision.js +81 -0
  21. package/lib/clipboard-capture.js +95 -0
  22. package/lib/clipboard-follow.js +98 -0
  23. package/lib/clipboard-model.js +1235 -0
  24. package/lib/cloud-accounts.js +250 -0
  25. package/lib/conflict-model.js +186 -0
  26. package/lib/control-client.js +87 -0
  27. package/lib/control-server.js +149 -0
  28. package/lib/diagnostics.js +121 -0
  29. package/lib/drag-files.js +48 -0
  30. package/lib/ensure-directory.js +56 -0
  31. package/lib/fork-names.js +35 -0
  32. package/lib/fs-probe.js +132 -0
  33. package/lib/history-feed.js +48 -0
  34. package/lib/hmac-auth.js +78 -0
  35. package/lib/macos-hotkey.js +297 -0
  36. package/lib/macos-paste.js +112 -0
  37. package/lib/mcp-core.js +270 -0
  38. package/lib/mcp-installers.js +284 -0
  39. package/lib/mcp-paths.js +99 -0
  40. package/lib/p2p-crypto.js +54 -0
  41. package/lib/p2p-discovery.js +191 -0
  42. package/lib/quick-paste.js +187 -0
  43. package/lib/retention.js +43 -0
  44. package/lib/sync-delta.js +227 -0
  45. package/lib/sync-journal.js +181 -0
  46. package/lib/sync-paths.js +73 -0
  47. package/lib/tailscale.js +80 -0
  48. package/lib/text-blob-store.js +38 -0
  49. package/lib/windows-clipboard.js +213 -0
  50. package/lib/windows-console.js +63 -0
  51. package/lib/windows-dwm.js +183 -0
  52. package/lib/windows-hook-worker.js +236 -0
  53. package/lib/windows-hook.js +91 -0
  54. package/lib/windows-paste.js +185 -0
  55. package/main.js +7540 -0
  56. package/mcp/boardclip-mcp.js +255 -0
  57. package/mcp-approval-preload.js +22 -0
  58. package/mcp-approval.html +218 -0
  59. package/package.json +55 -4
  60. package/preload.js +106 -0
  61. package/site/index.html +1624 -0
  62. package/site/shared/clip-search.js +2330 -0
  63. package/site/shared/clipboard-popup.css +1046 -0
  64. package/site/shared/clipboard-tokens.css +326 -0
  65. package/site/shared/clipboard-ui-core.js +6921 -0
  66. package/site/shared/clipboard-window.css +36 -0
  67. package/site/shared/vendor/cm5/README.md +52 -0
  68. package/site/shared/vendor/cm5/codemirror.css +344 -0
  69. package/site/shared/vendor/cm5/codemirror.js +9884 -0
  70. package/site/shared/vendor/cm5/diff-match-patch.js +2228 -0
  71. package/site/shared/vendor/cm5/merge.css +119 -0
  72. package/site/shared/vendor/cm5/merge.js +1182 -0
  73. package/site/shared/vendor/fonts/README.md +19 -0
  74. package/site/shared/vendor/fonts/material-symbols-rounded.woff2 +0 -0
  75. package/viewer-preload.js +51 -0
  76. package/viewer.html +129 -0
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+ // Worker thread running a Win32 low-level keyboard hook (WH_KEYBOARD_LL).
3
+ //
4
+ // This exists because Electron's `globalShortcut.register('Super+V')` fails
5
+ // on Windows — Windows Clipboard History has already claimed Win+V via the
6
+ // built-in RegisterHotKey API. A low-level hook sits *below* that system
7
+ // shortcut layer and can swallow the key before Windows Clipboard History
8
+ // sees it.
9
+ //
10
+ // Why a worker thread (not Electron's main thread):
11
+ // 1. LL hooks are called via messages posted to the installing thread, so
12
+ // that thread must pump its own GetMessage loop. Electron's main thread
13
+ // has a Chromium message pump that doesn't guarantee LL hook delivery
14
+ // and can starve it under load.
15
+ // 2. LowLevelHooksTimeout defaults to 300ms — if any JS task on the main
16
+ // thread runs longer, Windows silently unregisters the hook. Isolating
17
+ // it in a worker with a dedicated GetMessage loop removes that risk.
18
+ //
19
+ // State sharing with main thread uses a SharedArrayBuffer rather than
20
+ // postMessage because the worker is blocked in a synchronous GetMessageW
21
+ // call and can't process its JS event loop.
22
+
23
+ const { parentPort, workerData } = require('worker_threads');
24
+ const koffi = require('koffi');
25
+
26
+ // --- Shared state (worker reads, main writes) ---
27
+ // Layout: [popupVisible, slot1, slot2, ..., slot9, reserved, ...]
28
+ const sharedState = workerData && workerData.sharedStateBuffer
29
+ ? new Uint8Array(workerData.sharedStateBuffer)
30
+ : new Uint8Array(16);
31
+
32
+ // --- Win32 constants ---
33
+ const WH_KEYBOARD_LL = 13;
34
+ const WM_KEYDOWN = 0x0100;
35
+ const WM_KEYUP = 0x0101;
36
+ const WM_SYSKEYDOWN = 0x0104;
37
+ 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
+ const KEYEVENTF_KEYUP = 0x0002;
45
+
46
+ // --- koffi types ---
47
+ // KBDLLHOOKSTRUCT is the struct Windows passes to a WH_KEYBOARD_LL hook via
48
+ // lParam. We declare it so koffi can decode the pointer in the callback.
49
+ const KBDLLHOOKSTRUCT = koffi.struct('KBDLLHOOKSTRUCT', {
50
+ vkCode: 'uint32',
51
+ scanCode: 'uint32',
52
+ flags: 'uint32',
53
+ time: 'uint32',
54
+ dwExtraInfo: 'uintptr_t',
55
+ });
56
+
57
+ // MSG is needed for the GetMessageW pump loop below.
58
+ const MSG = koffi.struct('MSG', {
59
+ hwnd: 'void *',
60
+ message: 'uint32',
61
+ wParam: 'uintptr_t',
62
+ lParam: 'intptr_t',
63
+ time: 'uint32',
64
+ pt_x: 'int32',
65
+ pt_y: 'int32',
66
+ lPrivate: 'uint32',
67
+ });
68
+
69
+ // Hook procedure prototype. Declaring lParam as KBDLLHOOKSTRUCT* means koffi
70
+ // gives us an External we can both decode *and* pass straight back through
71
+ // to CallNextHookEx without reinterpreting the pointer ourselves.
72
+ const HookProc = koffi.proto(
73
+ 'intptr_t __stdcall HookProc(int nCode, uintptr_t wParam, KBDLLHOOKSTRUCT *lParam)'
74
+ );
75
+
76
+ // --- Load user32.dll and bind functions ---
77
+ let user32;
78
+ try {
79
+ user32 = koffi.load('user32.dll');
80
+ } catch (err) {
81
+ parentPort.postMessage({ type: 'error', error: 'koffi.load user32.dll: ' + String(err) });
82
+ process.exit(1);
83
+ }
84
+
85
+ const SetWindowsHookExW = user32.func(
86
+ 'void * __stdcall SetWindowsHookExW(int idHook, HookProc *lpfn, void *hmod, uint32 dwThreadId)'
87
+ );
88
+ const UnhookWindowsHookEx = user32.func(
89
+ 'int __stdcall UnhookWindowsHookEx(void *hhk)'
90
+ );
91
+ const CallNextHookEx = user32.func(
92
+ 'intptr_t __stdcall CallNextHookEx(void *hhk, int nCode, uintptr_t wParam, KBDLLHOOKSTRUCT *lParam)'
93
+ );
94
+ const GetMessageW = user32.func(
95
+ 'int __stdcall GetMessageW(_Out_ MSG *lpMsg, void *hWnd, uint32 wMsgFilterMin, uint32 wMsgFilterMax)'
96
+ );
97
+ const TranslateMessage = user32.func(
98
+ 'int __stdcall TranslateMessage(MSG *lpMsg)'
99
+ );
100
+ const DispatchMessageW = user32.func(
101
+ 'intptr_t __stdcall DispatchMessageW(MSG *lpMsg)'
102
+ );
103
+ const GetAsyncKeyState = user32.func(
104
+ 'int16 __stdcall GetAsyncKeyState(int vKey)'
105
+ );
106
+ const keybd_event = user32.func(
107
+ 'void __stdcall keybd_event(uint8 bVk, uint8 bScan, uint32 dwFlags, uintptr_t dwExtraInfo)'
108
+ );
109
+
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
+ }
125
+
126
+ function markWinKeyUsed() {
127
+ // If V is swallowed, Windows can still react to the original Win release.
128
+ // A tiny Ctrl tap marks the Win key as used while allowing the real Win-up
129
+ // through, so we avoid both Clipboard History and stuck modifier state.
130
+ try {
131
+ keybd_event(VK_CONTROL, 0, 0, 0);
132
+ keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0);
133
+ } catch {}
134
+ }
135
+
136
+ // --- Hook callback ---
137
+ // Fires synchronously on this worker thread each time a keyboard event is
138
+ // delivered to the thread's message queue. Return non-zero to swallow the
139
+ // event (it will not propagate to the rest of the hook chain or any app).
140
+ const hookCallback = koffi.register((nCode, wParam, lParamExt) => {
141
+ try {
142
+ if (nCode >= 0) {
143
+ const kb = koffi.decode(lParamExt, KBDLLHOOKSTRUCT);
144
+ const vk = kb.vkCode;
145
+ const keyDown = wParam === WM_KEYDOWN || wParam === WM_SYSKEYDOWN;
146
+ const keyUp = wParam === WM_KEYUP || wParam === WM_SYSKEYUP;
147
+
148
+ if (!keyDown && !keyUp) {
149
+ return CallNextHookEx(null, nCode, wParam, lParamExt);
150
+ }
151
+
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
+ }
202
+ }
203
+ } catch (err) {
204
+ // Never let JS exceptions escape the hook — Windows will stall the
205
+ // whole keyboard chain. Best-effort report and fall through to
206
+ // CallNextHookEx so the key still reaches its target.
207
+ try { parentPort.postMessage({ type: 'error', error: String(err) }); } catch {}
208
+ }
209
+ return CallNextHookEx(null, nCode, wParam, lParamExt);
210
+ }, koffi.pointer(HookProc));
211
+
212
+ // --- Install hook ---
213
+ // hMod=null + dwThreadId=0 → install a global LL hook owned by the current
214
+ // process. LL hooks are the one case where a null hMod is explicitly allowed.
215
+ const hook = SetWindowsHookExW(WH_KEYBOARD_LL, hookCallback, null, 0);
216
+ if (!hook) {
217
+ parentPort.postMessage({ type: 'error', error: 'SetWindowsHookExW returned null' });
218
+ process.exit(1);
219
+ }
220
+
221
+ parentPort.postMessage({ type: 'ready' });
222
+
223
+ // --- Message pump ---
224
+ // Blocks this worker thread pumping messages so Windows can deliver hook
225
+ // callbacks. Exits on WM_QUIT or worker termination from the main thread.
226
+ const msg = {};
227
+ while (true) {
228
+ const ret = GetMessageW(msg, null, 0, 0);
229
+ if (ret <= 0) break;
230
+ TranslateMessage(msg);
231
+ DispatchMessageW(msg);
232
+ }
233
+
234
+ // --- Cleanup (only reached on clean WM_QUIT; worker.terminate skips this) ---
235
+ try { UnhookWindowsHookEx(hook); } catch {}
236
+ try { koffi.unregister(hookCallback); } catch {}
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+ // Main-thread wrapper around the Win32 keyboard hook worker.
3
+ //
4
+ // On Windows we can't use Electron's `globalShortcut.register('Super+V')`
5
+ // because Windows Clipboard History owns Win+V at the RegisterHotKey layer
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.
9
+ //
10
+ // See lib/windows-hook-worker.js for the hook implementation and the
11
+ // rationale for running it in a worker.
12
+ //
13
+ // Shared state (popup visible + which numpad slots are assigned) is passed
14
+ // via a SharedArrayBuffer because the worker is synchronously blocked in
15
+ // GetMessageW and can't process postMessage events from its JS event loop.
16
+
17
+ const { Worker } = require('worker_threads');
18
+ const path = require('path');
19
+
20
+ let currentHook = null;
21
+
22
+ function install({ onShowPopup, onNumpadPaste }) {
23
+ if (process.platform !== 'win32') {
24
+ // Non-Windows platforms use Electron's globalShortcut; no-op API here.
25
+ return {
26
+ setPopupVisible() {},
27
+ setSlotAssignments() {},
28
+ uninstall() {},
29
+ };
30
+ }
31
+ if (currentHook) return currentHook;
32
+
33
+ // Layout: [popupVisible, slot1, slot2, ..., slot9, reserved...]
34
+ const sharedStateBuffer = new SharedArrayBuffer(16);
35
+ const sharedState = new Uint8Array(sharedStateBuffer);
36
+
37
+ const worker = new Worker(path.join(__dirname, 'windows-hook-worker.js'), {
38
+ workerData: { sharedStateBuffer },
39
+ });
40
+
41
+ worker.on('message', (msg) => {
42
+ switch (msg.type) {
43
+ case 'show':
44
+ try { onShowPopup(); } catch (e) { console.error('[windows-hook] onShowPopup:', e); }
45
+ break;
46
+ case 'numpad':
47
+ try { onNumpadPaste(msg.slot); } catch (e) { console.error('[windows-hook] onNumpadPaste:', e); }
48
+ break;
49
+ case 'ready':
50
+ // Hook installed successfully — no action needed.
51
+ break;
52
+ case 'error':
53
+ console.error('[windows-hook]', msg.error);
54
+ break;
55
+ }
56
+ });
57
+
58
+ worker.on('error', (err) => {
59
+ console.error('[windows-hook] worker crashed:', err);
60
+ currentHook = null;
61
+ });
62
+
63
+ worker.on('exit', (code) => {
64
+ if (code !== 0) console.error(`[windows-hook] worker exited with code ${code}`);
65
+ currentHook = null;
66
+ });
67
+
68
+ currentHook = {
69
+ setPopupVisible(visible) {
70
+ sharedState[0] = visible ? 1 : 0;
71
+ },
72
+ setSlotAssignments(assignedSet) {
73
+ // assignedSet: Set<number> of assigned numpad slot numbers (1-9)
74
+ for (let n = 1; n <= 9; n++) {
75
+ sharedState[n] = assignedSet && assignedSet.has(n) ? 1 : 0;
76
+ }
77
+ },
78
+ uninstall() {
79
+ if (!currentHook) return;
80
+ currentHook = null;
81
+ // worker.terminate() kills the thread; Windows reclaims the hook on
82
+ // thread exit. A cleaner PostThreadMessageW(WM_QUIT) would require
83
+ // exposing the worker thread ID — not worth the extra FFI surface.
84
+ worker.terminate().catch(() => {});
85
+ },
86
+ };
87
+
88
+ return currentHook;
89
+ }
90
+
91
+ module.exports = { install };
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+ // Fast Windows paste primitives via koffi FFI.
3
+ //
4
+ // Replaces the previous `cscript` + VBScript SendKeys approach which had two
5
+ // problems:
6
+ // 1. Spawning cscript per paste costs 200-500ms (cold start). That was the
7
+ // "delay" users noticed vs. the pre-Electron Python version which used
8
+ // direct Win32 calls via ctypes.
9
+ // 2. VBScript `WshShell.SendKeys "^v"` has a documented Win32 quirk where
10
+ // it can flip NumLock state on some systems, triggering OEM keyboard
11
+ // software notifications (e.g. Logitech "NumLock ON" overlay).
12
+ //
13
+ // Uses SendInput (the modern Win32 API recommended for new code — see
14
+ // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendinput).
15
+ // Benefits over the older keybd_event:
16
+ // - Events are atomic: other input from the user or from other processes
17
+ // cannot be interleaved between our Ctrl down / V down / V up / Ctrl up.
18
+ // - Microsoft explicitly flags keybd_event as superseded for new dev.
19
+ //
20
+ // The INPUT struct uses a tagged union in C. We target x64 (the only arch
21
+ // Electron ships by default on Windows) and use a flat struct with explicit
22
+ // padding that matches the x64 memory layout (40 bytes total).
23
+
24
+ if (process.platform !== 'win32') {
25
+ module.exports = {
26
+ sendCtrlV() {},
27
+ typeText() { return { ok: false, chars: 0, events: 0 }; },
28
+ getForegroundWindow() { return null; },
29
+ getForegroundWindowAddress() { return '0'; },
30
+ setForegroundWindow() { return false; },
31
+ isMouseButtonDown() { return false; },
32
+ };
33
+ return;
34
+ }
35
+
36
+ const koffi = require('koffi');
37
+ const user32 = koffi.load('user32.dll');
38
+
39
+ // x64 INPUT struct layout (40 bytes):
40
+ // offset 0: type (DWORD, 4)
41
+ // offset 4: padding (4) — union aligns to 8 (ULONG_PTR alignment)
42
+ // offset 8: wVk (WORD, 2)
43
+ // offset 10: wScan (WORD, 2)
44
+ // offset 12: dwFlags (DWORD, 4)
45
+ // offset 16: time (DWORD, 4)
46
+ // offset 20: padding (4) — align dwExtraInfo to 8
47
+ // offset 24: dwExtraInfo (ULONG_PTR, 8)
48
+ // offset 32: padding (8) — pad to 40, matching MOUSEINPUT's size
49
+ const INPUT = koffi.struct('INPUT', {
50
+ type: 'uint32',
51
+ _pad0: 'uint32',
52
+ wVk: 'uint16',
53
+ wScan: 'uint16',
54
+ dwFlags: 'uint32',
55
+ time: 'uint32',
56
+ _pad1: 'uint32',
57
+ dwExtraInfo: 'uintptr_t',
58
+ _pad2: koffi.array('uint8', 8),
59
+ });
60
+
61
+ const INPUT_KEYBOARD = 1;
62
+ const VK_CONTROL = 0x11;
63
+ const VK_V = 0x56;
64
+ const VK_RETURN = 0x0D;
65
+ const VK_TAB = 0x09;
66
+ const VK_LBUTTON = 0x01;
67
+ const VK_RBUTTON = 0x02;
68
+ const KEYEVENTF_KEYUP = 0x0002;
69
+ const KEYEVENTF_UNICODE = 0x0004;
70
+
71
+ const SendInput = user32.func(
72
+ 'uint32 __stdcall SendInput(uint32 cInputs, INPUT *pInputs, int cbSize)'
73
+ );
74
+ const GetForegroundWindow = user32.func(
75
+ 'void * __stdcall GetForegroundWindow()'
76
+ );
77
+ const SetForegroundWindow = user32.func(
78
+ 'int __stdcall SetForegroundWindow(void *hWnd)'
79
+ );
80
+ const GetAsyncKeyState = user32.func(
81
+ 'int16 __stdcall GetAsyncKeyState(int vKey)'
82
+ );
83
+
84
+ const INPUT_SIZE = koffi.sizeof(INPUT);
85
+
86
+ function keyInput(vk, keyup) {
87
+ return {
88
+ type: INPUT_KEYBOARD,
89
+ _pad0: 0,
90
+ wVk: vk,
91
+ wScan: 0,
92
+ dwFlags: keyup ? KEYEVENTF_KEYUP : 0,
93
+ time: 0,
94
+ _pad1: 0,
95
+ dwExtraInfo: 0,
96
+ _pad2: [0, 0, 0, 0, 0, 0, 0, 0],
97
+ };
98
+ }
99
+
100
+ function sendCtrlV() {
101
+ // Four events delivered atomically: Ctrl down, V down, V up, Ctrl up.
102
+ SendInput(4, [
103
+ keyInput(VK_CONTROL, false),
104
+ keyInput(VK_V, false),
105
+ keyInput(VK_V, true),
106
+ keyInput(VK_CONTROL, true),
107
+ ], INPUT_SIZE);
108
+ }
109
+
110
+ // A single Unicode character injected via KEYEVENTF_UNICODE — inserts the exact
111
+ // codepoint as text, with no virtual-key mapping, so it is immune to keyboard
112
+ // layout AND to Tab/Enter "action" semantics (a literal tab/space is inserted,
113
+ // not a focus change). wVk MUST be 0 for unicode injection.
114
+ function unicodeInput(codeUnit, keyup) {
115
+ return {
116
+ type: INPUT_KEYBOARD,
117
+ _pad0: 0,
118
+ wVk: 0,
119
+ wScan: codeUnit,
120
+ dwFlags: KEYEVENTF_UNICODE | (keyup ? KEYEVENTF_KEYUP : 0),
121
+ time: 0,
122
+ _pad1: 0,
123
+ dwExtraInfo: 0,
124
+ _pad2: [0, 0, 0, 0, 0, 0, 0, 0],
125
+ };
126
+ }
127
+
128
+ // Type text by synthesizing key events — bypassing the clipboard entirely, so
129
+ // there is no backup/restore race and the user's clipboard is never touched.
130
+ // The whole string is delivered in as few SendInput batches as possible (NOT
131
+ // one key at a time with delays), so it is effectively instant.
132
+ //
133
+ // Newlines are sent as a real VK_RETURN keypress (KEYEVENTF_UNICODE '\n' does
134
+ // not produce a newline in most apps). Everything else — including tab — goes
135
+ // through Unicode injection so a snippet's exact characters are inserted without
136
+ // triggering Tab-key focus behavior. Returns {ok, chars, events}.
137
+ function typeText(text) {
138
+ const str = String(text == null ? '' : text);
139
+ if (!str) return { ok: true, chars: 0, events: 0 };
140
+ const events = [];
141
+ for (let i = 0; i < str.length; i++) {
142
+ const code = str.charCodeAt(i);
143
+ if (code === 0x0d) continue; // swallow CR; LF drives the newline
144
+ if (code === 0x0a) { // LF -> real Enter keypress
145
+ events.push(keyInput(VK_RETURN, false));
146
+ events.push(keyInput(VK_RETURN, true));
147
+ continue;
148
+ }
149
+ events.push(unicodeInput(code, false));
150
+ events.push(unicodeInput(code, true));
151
+ }
152
+ if (!events.length) return { ok: true, chars: str.length, events: 0 };
153
+ // Chunk so a single very large array can't overflow the input pipeline; each
154
+ // SendInput call is atomic and there is no inter-key delay, so this stays fast.
155
+ const CHUNK = 400;
156
+ let sent = 0;
157
+ for (let i = 0; i < events.length; i += CHUNK) {
158
+ const batch = events.slice(i, i + CHUNK);
159
+ sent += SendInput(batch.length, batch, INPUT_SIZE);
160
+ }
161
+ return { ok: sent === events.length, chars: str.length, events: events.length, sent };
162
+ }
163
+
164
+ function getForegroundWindow() {
165
+ return GetForegroundWindow();
166
+ }
167
+
168
+ // Numeric address of the current foreground window, as a string (HWNDs can
169
+ // exceed Number range on x64). Used by tests to confirm the intended window is
170
+ // focused before injecting keystrokes, so injection can't leak into another app.
171
+ function getForegroundWindowAddress() {
172
+ try { return String(koffi.address(GetForegroundWindow())); } catch { return '0'; }
173
+ }
174
+
175
+ // -> true when Windows accepted the switch.
176
+ function setForegroundWindow(hwnd) {
177
+ if (!hwnd) return false;
178
+ return !!SetForegroundWindow(hwnd);
179
+ }
180
+
181
+ function isMouseButtonDown() {
182
+ return !!((GetAsyncKeyState(VK_LBUTTON) | GetAsyncKeyState(VK_RBUTTON)) & 0x8000);
183
+ }
184
+
185
+ module.exports = { sendCtrlV, typeText, getForegroundWindow, getForegroundWindowAddress, setForegroundWindow, isMouseButtonDown };