@tobq/boardclip 2.1.113 → 2.1.114
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 +5 -2
- package/editor-preload.js +2 -0
- package/editor.html +12 -0
- package/index.html +94 -48
- package/lib/clipboard-model.js +588 -23
- package/lib/mcp-core.js +240 -69
- package/lib/sync-delta.js +5 -2
- package/lib/windows-hook-policy.js +127 -0
- package/lib/windows-hook-worker.js +18 -77
- package/lib/windows-hook.js +20 -9
- package/main.js +812 -188
- package/mcp/boardclip-mcp.js +68 -39
- package/mcp-approval.html +4 -2
- package/package.json +1 -1
- package/preload.js +4 -2
- package/site/index.html +108 -37
- package/site/shared/clip-search.js +291 -134
- package/site/shared/clipboard-popup.css +8 -20
- package/site/shared/clipboard-ui-core.js +405 -329
- package/viewer.html +1 -1
|
@@ -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(
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
// Win+V, Win+N and Numpad1-9: each claimed only with exactly its modifiers,
|
|
107
|
+
// 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
|
|
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(
|
|
132
|
-
keybd_event(
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (
|
|
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
|
package/lib/windows-hook.js
CHANGED
|
@@ -4,34 +4,37 @@
|
|
|
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*
|
|
7
|
+
// WH_KEYBOARD_LL hook on a dedicated worker thread which intercepts Win+V,
|
|
8
|
+
// Win+N (new note, while that is the shortcut) and numpad keys *before*
|
|
9
|
+
// Windows Clipboard History / the notification panel see them.
|
|
9
10
|
//
|
|
10
11
|
// See lib/windows-hook-worker.js for the hook implementation and the
|
|
11
12
|
// rationale for running it in a worker.
|
|
12
13
|
//
|
|
13
|
-
// Shared state (popup visible
|
|
14
|
-
//
|
|
14
|
+
// Shared state (popup visible, which numpad slots are assigned, Win+N on;
|
|
15
|
+
// layout = STATE in lib/windows-hook-policy.js) is passed via a
|
|
16
|
+
// SharedArrayBuffer because the worker is synchronously blocked in
|
|
15
17
|
// GetMessageW and can't process postMessage events from its JS event loop.
|
|
16
18
|
|
|
17
19
|
const { Worker } = require('worker_threads');
|
|
18
20
|
const path = require('path');
|
|
21
|
+
const { STATE } = require('./windows-hook-policy');
|
|
19
22
|
|
|
20
23
|
let currentHook = null;
|
|
21
24
|
|
|
22
|
-
function install({ onShowPopup, onNumpadPaste }) {
|
|
25
|
+
function install({ onShowPopup, onNumpadPaste, onNewNote }) {
|
|
23
26
|
if (process.platform !== 'win32') {
|
|
24
27
|
// Non-Windows platforms use Electron's globalShortcut; no-op API here.
|
|
25
28
|
return {
|
|
26
29
|
setPopupVisible() {},
|
|
27
30
|
setSlotAssignments() {},
|
|
31
|
+
setNewNoteHotkey() {},
|
|
28
32
|
uninstall() {},
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
if (currentHook) return currentHook;
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
const sharedStateBuffer = new SharedArrayBuffer(16);
|
|
37
|
+
const sharedStateBuffer = new SharedArrayBuffer(STATE.SIZE);
|
|
35
38
|
const sharedState = new Uint8Array(sharedStateBuffer);
|
|
36
39
|
|
|
37
40
|
const worker = new Worker(path.join(__dirname, 'windows-hook-worker.js'), {
|
|
@@ -46,6 +49,9 @@ function install({ onShowPopup, onNumpadPaste }) {
|
|
|
46
49
|
case 'numpad':
|
|
47
50
|
try { onNumpadPaste(msg.slot); } catch (e) { console.error('[windows-hook] onNumpadPaste:', e); }
|
|
48
51
|
break;
|
|
52
|
+
case 'new-note':
|
|
53
|
+
try { if (onNewNote) onNewNote(); } catch (e) { console.error('[windows-hook] onNewNote:', e); }
|
|
54
|
+
break;
|
|
49
55
|
case 'ready':
|
|
50
56
|
// Hook installed successfully — no action needed.
|
|
51
57
|
break;
|
|
@@ -67,14 +73,19 @@ function install({ onShowPopup, onNumpadPaste }) {
|
|
|
67
73
|
|
|
68
74
|
currentHook = {
|
|
69
75
|
setPopupVisible(visible) {
|
|
70
|
-
sharedState[
|
|
76
|
+
sharedState[STATE.POPUP_VISIBLE] = visible ? 1 : 0;
|
|
71
77
|
},
|
|
72
78
|
setSlotAssignments(assignedSet) {
|
|
73
79
|
// assignedSet: Set<number> of assigned numpad slot numbers (1-9)
|
|
74
80
|
for (let n = 1; n <= 9; n++) {
|
|
75
|
-
sharedState[n] = assignedSet && assignedSet.has(n) ? 1 : 0;
|
|
81
|
+
sharedState[STATE.SLOT_FIRST + n - 1] = assignedSet && assignedSet.has(n) ? 1 : 0;
|
|
76
82
|
}
|
|
77
83
|
},
|
|
84
|
+
// Win+N opens a new note only while it is the new-note shortcut (not
|
|
85
|
+
// while it is off or another chord is recorded, nor during a recording).
|
|
86
|
+
setNewNoteHotkey(enabled) {
|
|
87
|
+
sharedState[STATE.NEW_NOTE_HOTKEY] = enabled ? 1 : 0;
|
|
88
|
+
},
|
|
78
89
|
uninstall() {
|
|
79
90
|
if (!currentHook) return;
|
|
80
91
|
currentHook = null;
|