keyspy 1.0.7 → 1.1.1
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 +110 -3
- package/bin/keyspy +24 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +359 -0
- package/{build → dist}/index.d.ts +10 -10
- package/{build → dist}/index.d.ts.map +1 -1
- package/dist/index.js +156 -0
- package/{build/ts/X11KeyServer.d.ts → dist/platforms/linux/index.d.ts} +5 -5
- package/dist/platforms/linux/index.d.ts.map +1 -0
- package/dist/platforms/linux/index.js +181 -0
- package/dist/platforms/linux/keymap.d.ts +3 -0
- package/dist/platforms/linux/keymap.d.ts.map +1 -0
- package/dist/platforms/linux/keymap.js +144 -0
- package/{build/ts/MacKeyServer.d.ts → dist/platforms/mac/index.d.ts} +5 -5
- package/dist/platforms/mac/index.d.ts.map +1 -0
- package/dist/platforms/mac/index.js +181 -0
- package/dist/platforms/mac/keymap.d.ts +3 -0
- package/dist/platforms/mac/keymap.d.ts.map +1 -0
- package/dist/platforms/mac/keymap.js +155 -0
- package/{build/ts/WinKeyServer.d.ts → dist/platforms/windows/index.d.ts} +5 -5
- package/dist/platforms/windows/index.d.ts.map +1 -0
- package/dist/platforms/windows/index.js +110 -0
- package/{build/ts/_data/WinGlobalKeyLookup.d.ts → dist/platforms/windows/keymap.d.ts} +2 -2
- package/dist/platforms/windows/keymap.d.ts.map +1 -0
- package/{build/ts/_data/WinGlobalKeyLookup.js → dist/platforms/windows/keymap.js} +1 -1
- package/{build/ts/_types → dist/types}/IConfig.d.ts +2 -0
- package/dist/types/IConfig.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IConfig.js +1 -1
- package/{build/ts/_types → dist/types}/IGlobalKey.d.ts +1 -1
- package/dist/types/IGlobalKey.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKey.js +1 -1
- package/dist/types/IGlobalKeyDownMap.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKeyDownMap.js +1 -1
- package/dist/types/IGlobalKeyEvent.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKeyEvent.js +1 -1
- package/dist/types/IGlobalKeyListener.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKeyListener.js +1 -1
- package/dist/types/IGlobalKeyListenerRaw.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKeyListenerRaw.js +1 -1
- package/dist/types/IGlobalKeyLookup.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKeyLookup.js +1 -1
- package/dist/types/IGlobalKeyResult.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKeyResult.js +1 -1
- package/dist/types/IGlobalKeyServer.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IGlobalKeyServer.js +1 -1
- package/{build/ts/_types → dist/types}/IMacConfig.d.ts +2 -0
- package/dist/types/IMacConfig.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IMacConfig.js +1 -1
- package/dist/types/IWindowsConfig.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IWindowsConfig.js +1 -1
- package/{build/ts/_types → dist/types}/IX11Config.d.ts +2 -0
- package/dist/types/IX11Config.d.ts.map +1 -0
- package/{build/ts/_types → dist/types}/IX11Config.js +1 -1
- package/dist/utils/permissions.d.ts +10 -0
- package/dist/utils/permissions.d.ts.map +1 -0
- package/dist/utils/permissions.js +115 -0
- package/dist/utils.d.ts +22 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +39 -0
- package/native/MacKeyServer/main.swift +326 -0
- package/native/WinKeyServer/main.cpp +402 -0
- package/native/X11KeyServer/main.cpp +118 -0
- package/package.json +50 -33
- package/runtime/MacKeyServer +0 -0
- package/scripts/download-binaries.js +7 -7
- package/build/index.js +0 -149
- package/build/ts/MacKeyServer.d.ts.map +0 -1
- package/build/ts/MacKeyServer.js +0 -159
- package/build/ts/WinKeyServer.d.ts.map +0 -1
- package/build/ts/WinKeyServer.js +0 -88
- package/build/ts/X11KeyServer.d.ts.map +0 -1
- package/build/ts/X11KeyServer.js +0 -159
- package/build/ts/_data/MacGlobalKeyLookup.d.ts +0 -3
- package/build/ts/_data/MacGlobalKeyLookup.d.ts.map +0 -1
- package/build/ts/_data/MacGlobalKeyLookup.js +0 -155
- package/build/ts/_data/WinGlobalKeyLookup.d.ts.map +0 -1
- package/build/ts/_data/X11GlobalKeyLookup.d.ts +0 -3
- package/build/ts/_data/X11GlobalKeyLookup.d.ts.map +0 -1
- package/build/ts/_data/X11GlobalKeyLookup.js +0 -144
- package/build/ts/_types/IConfig.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKey.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKeyDownMap.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKeyEvent.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKeyListener.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKeyListenerRaw.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKeyLookup.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKeyResult.d.ts.map +0 -1
- package/build/ts/_types/IGlobalKeyServer.d.ts.map +0 -1
- package/build/ts/_types/IMacConfig.d.ts.map +0 -1
- package/build/ts/_types/IWindowsConfig.d.ts.map +0 -1
- package/build/ts/_types/IX11Config.d.ts.map +0 -1
- package/build/ts/isSpawnEventSupported.d.ts +0 -6
- package/build/ts/isSpawnEventSupported.d.ts.map +0 -1
- package/build/ts/isSpawnEventSupported.js +0 -18
- /package/{build/ts/_types → dist/types}/IGlobalKeyDownMap.d.ts +0 -0
- /package/{build/ts/_types → dist/types}/IGlobalKeyEvent.d.ts +0 -0
- /package/{build/ts/_types → dist/types}/IGlobalKeyListener.d.ts +0 -0
- /package/{build/ts/_types → dist/types}/IGlobalKeyListenerRaw.d.ts +0 -0
- /package/{build/ts/_types → dist/types}/IGlobalKeyLookup.d.ts +0 -0
- /package/{build/ts/_types → dist/types}/IGlobalKeyResult.d.ts +0 -0
- /package/{build/ts/_types → dist/types}/IGlobalKeyServer.d.ts +0 -0
- /package/{build/ts/_types → dist/types}/IWindowsConfig.d.ts +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#include <windows.h>
|
|
2
|
+
#include <stdio.h>
|
|
3
|
+
#include <stdlib.h>
|
|
4
|
+
#include <iostream>
|
|
5
|
+
#include <thread>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <cstddef>
|
|
8
|
+
#include <ctime>
|
|
9
|
+
#include <climits>
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
Description:
|
|
13
|
+
This C++ module uses `SetWindowsHookEx` to create a system-wide Low Level keyboard hook to intercept keyboard
|
|
14
|
+
events before they reach other applications on the operating system. Event handling is offloaded to the
|
|
15
|
+
calling process. The calling process is to decide whether the events should be caught or propogated.
|
|
16
|
+
|
|
17
|
+
The key information sent to the main process is like:
|
|
18
|
+
4,DOWN,0,1
|
|
19
|
+
4,UP,0,2
|
|
20
|
+
14,DOWN,0,3
|
|
21
|
+
14,UP,0,4
|
|
22
|
+
37,DOWN,0,5
|
|
23
|
+
37,UP,0,6
|
|
24
|
+
37,DOWN,0,7
|
|
25
|
+
37,UP,0,8
|
|
26
|
+
31,DOWN,0,9
|
|
27
|
+
31,UP,0,10
|
|
28
|
+
|
|
29
|
+
Note: the format of the information is as follows:
|
|
30
|
+
VirtualKeyCode,UpDownState,ScanCode,EventID
|
|
31
|
+
p.s. ScanCode has been removed (is 0) from the above for documentation purposes only.
|
|
32
|
+
|
|
33
|
+
when `hello` is typed. If `H` is held down for a long period of time, DOWN events are repeated until released.
|
|
34
|
+
4,DOWN,35,1
|
|
35
|
+
4,DOWN,35,2
|
|
36
|
+
4,DOWN,35,3
|
|
37
|
+
4,DOWN,35,4
|
|
38
|
+
4,DOWN,35,5
|
|
39
|
+
4,DOWN,35,6
|
|
40
|
+
4,UP,35,7
|
|
41
|
+
|
|
42
|
+
The calling process should send "1,{id}\n" if it wants the tap to block the keypress and "0,{id}\n"
|
|
43
|
+
if it wants the tap to propogate the keypress to the rest of the system (where {id} is the integer id received).
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
|
|
47
|
+
* Capturing keypresses while the windows key is down behaves differently to what you may expect. By default releasing the windows
|
|
48
|
+
key opens the start menu in windows, unless another key is pressed or released before the windows key is released. When a process
|
|
49
|
+
captures a key-down event while the windows key is down this message will not be sent to the OS, and thus will open the start menu.
|
|
50
|
+
In order to prevent the start menu from opening up, we send an artifical VK_HELP key up event, whenever capturing a key while the
|
|
51
|
+
windows key is pressed down. This also applies to the alt key. When this occurs a message "Sending VK_HELP to prevent win_key_up
|
|
52
|
+
triggering start menu" is also send to stderr.
|
|
53
|
+
* The hook procedure should process a message in less time than the data entry specified in the `LowLevelHooksTimeout` value in the
|
|
54
|
+
registry key `HKEY_CURRENT_USER\Control Panel\Desktop`. Note this is sometimes undefined and thus the timeout is an undocumented
|
|
55
|
+
amount of time. If the hook procedure times out, the hook is silently removed without being called and there is no way for the
|
|
56
|
+
application to know whether the hook is removed or not. In order to accommodate for this timeout, our process requires all
|
|
57
|
+
keystrokes to respond within 30ms of dispatch. If your process does not respond within 30ms the event will be propogated to the
|
|
58
|
+
rest of the system.
|
|
59
|
+
* The specific timeout time is declared in the `timeoutTime` global variable.
|
|
60
|
+
|
|
61
|
+
Compilation:
|
|
62
|
+
|
|
63
|
+
1. Install mingw via VSCode plugin documentation [here](https://code.visualstudio.com/docs/languages/cpp#_example-install-mingwx64).
|
|
64
|
+
2. Run the following command to build C++ application
|
|
65
|
+
|
|
66
|
+
`"C:\MinGW\bin\c++.exe" "src\bin\WinKeyServer\main.cpp" -o "bin\WinKeyServer.exe" -static-libgcc -static-libstdc++`
|
|
67
|
+
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
//How long to wait before timing out a key response
|
|
71
|
+
int timeoutTime = 30;
|
|
72
|
+
|
|
73
|
+
POINT zeroPoint { 0, 0 };
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* HEADERS
|
|
77
|
+
*/
|
|
78
|
+
enum KeyState
|
|
79
|
+
{
|
|
80
|
+
none = 0,
|
|
81
|
+
down = 1,
|
|
82
|
+
up = 2
|
|
83
|
+
};
|
|
84
|
+
void MessageLoop();
|
|
85
|
+
__declspec(dllexport) LRESULT CALLBACK KeyboardEvent(int nCode, WPARAM wParam, LPARAM lParam);
|
|
86
|
+
__declspec(dllexport) LRESULT CALLBACK MouseEvent(int nCode, WPARAM wParam, LPARAM lParam);
|
|
87
|
+
bool haltPropogation(bool isMouse, bool isDown, DWORD vkCode, DWORD scanCode, POINT location);
|
|
88
|
+
KeyState getKeyState(WPARAM wParam);
|
|
89
|
+
DWORD getMouseButtonCode(WPARAM wParam);
|
|
90
|
+
void printErr(const char *str);
|
|
91
|
+
std::string awaitLine();
|
|
92
|
+
std::string getString();
|
|
93
|
+
void fakeKey(DWORD iKeyEventF, WORD vkVirtualKey);
|
|
94
|
+
DWORD WINAPI timeoutLoop(LPVOID lpParam);
|
|
95
|
+
DWORD WINAPI checkInputLoop(LPVOID lpParam);
|
|
96
|
+
|
|
97
|
+
struct data
|
|
98
|
+
{
|
|
99
|
+
char *buffer;
|
|
100
|
+
std::size_t size;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
HHOOK hKeyboardHook;
|
|
104
|
+
HHOOK hMouseHook;
|
|
105
|
+
bool bIsMetaDown = false;
|
|
106
|
+
bool bIsAltDown;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Main process entry point
|
|
110
|
+
* * Create threads used for timeout
|
|
111
|
+
* * Register lowlevel keyboard hook
|
|
112
|
+
* * Begin message loop
|
|
113
|
+
*/
|
|
114
|
+
int main(int argc, char **argv)
|
|
115
|
+
{
|
|
116
|
+
//Get module instance handle
|
|
117
|
+
HINSTANCE hInstance = GetModuleHandle(NULL);
|
|
118
|
+
if (!hInstance)
|
|
119
|
+
return 1;
|
|
120
|
+
|
|
121
|
+
//Create threads to deal with input
|
|
122
|
+
HANDLE timeoutThread = CreateThread(NULL, 0, timeoutLoop, NULL, 0, NULL);
|
|
123
|
+
HANDLE inputThread = CreateThread(NULL, 0, checkInputLoop, NULL, 0, NULL);
|
|
124
|
+
|
|
125
|
+
//Hook to global keyboard events
|
|
126
|
+
hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, (HOOKPROC)KeyboardEvent, hInstance, 0);
|
|
127
|
+
hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, (HOOKPROC)MouseEvent, hInstance, 0);
|
|
128
|
+
|
|
129
|
+
//Wait app is closed
|
|
130
|
+
MessageLoop();
|
|
131
|
+
|
|
132
|
+
//Unhook from global keyboard events
|
|
133
|
+
UnhookWindowsHookEx(hKeyboardHook);
|
|
134
|
+
UnhookWindowsHookEx(hMouseHook);
|
|
135
|
+
|
|
136
|
+
//Dispose the threads
|
|
137
|
+
CloseHandle(timeoutThread);
|
|
138
|
+
CloseHandle(inputThread);
|
|
139
|
+
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Callback which receives and captures low level keyboard events.
|
|
145
|
+
* This callback should return non-zero result if the event is captured, otherwise return
|
|
146
|
+
* result of CallNextHookEx
|
|
147
|
+
*/
|
|
148
|
+
__declspec(dllexport) LRESULT CALLBACK KeyboardEvent(int nCode, WPARAM wParam, LPARAM lParam)
|
|
149
|
+
{
|
|
150
|
+
//If key is up or down
|
|
151
|
+
KeyState ks = getKeyState(wParam);
|
|
152
|
+
if ((nCode == HC_ACTION) && ks)
|
|
153
|
+
{
|
|
154
|
+
KBDLLHOOKSTRUCT key = *((KBDLLHOOKSTRUCT *)lParam);
|
|
155
|
+
|
|
156
|
+
//Stop propogation if needed using stdio messaging. 1st param is casted lPARAM.
|
|
157
|
+
//Returning 1 from this function will halt propogation
|
|
158
|
+
if (haltPropogation(false, ks == down, key.vkCode, key.scanCode, zeroPoint))
|
|
159
|
+
{
|
|
160
|
+
//fixes issue https://github.com/LaunchMenu/node-global-key-listener/issues/3
|
|
161
|
+
//TODO: maybe there is a better fix for this which doesn't involve sending arbitrary key events?
|
|
162
|
+
if (bIsMetaDown || bIsAltDown)
|
|
163
|
+
{
|
|
164
|
+
printErr("Sending VK_HELP to prevent win_key_up triggering start menu");
|
|
165
|
+
fakeKey(KEYEVENTF_KEYUP, VK_HELP);
|
|
166
|
+
}
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
else
|
|
170
|
+
{
|
|
171
|
+
//Work around for Windows key behaviour. See github issue #3
|
|
172
|
+
if (key.vkCode == VK_LWIN || key.vkCode == VK_RWIN)
|
|
173
|
+
bIsMetaDown = ks == down;
|
|
174
|
+
if (key.vkCode == VK_LMENU || key.vkCode == VK_RMENU)
|
|
175
|
+
bIsAltDown = ks == down;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return CallNextHookEx(hKeyboardHook, nCode, wParam, lParam);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Callback which receives and captures low level mouse events.
|
|
183
|
+
* This callback should return non-zero result if the event is captured, otherwise return
|
|
184
|
+
* result of CallNextHookEx
|
|
185
|
+
*/
|
|
186
|
+
__declspec(dllexport) LRESULT CALLBACK MouseEvent(int nCode, WPARAM wParam, LPARAM lParam)
|
|
187
|
+
{
|
|
188
|
+
MOUSEHOOKSTRUCT * pMouseStruct = (MOUSEHOOKSTRUCT *)lParam;
|
|
189
|
+
KeyState ks = getKeyState(wParam);
|
|
190
|
+
DWORD vCode = getMouseButtonCode(wParam);
|
|
191
|
+
|
|
192
|
+
if (nCode >= 0 && pMouseStruct != NULL && ks && vCode)
|
|
193
|
+
{
|
|
194
|
+
//Stop propogation if needed using stdio messaging. 1st param is casted lPARAM.
|
|
195
|
+
//Returning 1 from this function will halt propogation
|
|
196
|
+
if (haltPropogation(true, ks == down, vCode, vCode, pMouseStruct->pt))
|
|
197
|
+
{
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return CallNextHookEx(hKeyboardHook, nCode, wParam, lParam);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Application's core message loop to persist application
|
|
207
|
+
*/
|
|
208
|
+
void MessageLoop()
|
|
209
|
+
{
|
|
210
|
+
MSG message;
|
|
211
|
+
while (GetMessage(&message, NULL, 0, 0))
|
|
212
|
+
{
|
|
213
|
+
TranslateMessage(&message);
|
|
214
|
+
DispatchMessage(&message);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Translates wparam to KeyState enum
|
|
220
|
+
* @returns up if KEYUP or SYSKEYUP, down if KEYDOWN or SYSKEYDOWN else none
|
|
221
|
+
*/
|
|
222
|
+
KeyState getKeyState(WPARAM wParam)
|
|
223
|
+
{
|
|
224
|
+
switch (wParam)
|
|
225
|
+
{
|
|
226
|
+
case WM_KEYDOWN:
|
|
227
|
+
return down;
|
|
228
|
+
case WM_KEYUP:
|
|
229
|
+
return up;
|
|
230
|
+
|
|
231
|
+
case WM_SYSKEYDOWN:
|
|
232
|
+
return down;
|
|
233
|
+
case WM_SYSKEYUP:
|
|
234
|
+
return up;
|
|
235
|
+
|
|
236
|
+
case WM_LBUTTONDOWN:
|
|
237
|
+
return down;
|
|
238
|
+
case WM_LBUTTONUP:
|
|
239
|
+
return up;
|
|
240
|
+
|
|
241
|
+
case WM_RBUTTONDOWN:
|
|
242
|
+
return down;
|
|
243
|
+
case WM_RBUTTONUP:
|
|
244
|
+
return up;
|
|
245
|
+
|
|
246
|
+
case WM_MBUTTONDOWN:
|
|
247
|
+
return down;
|
|
248
|
+
case WM_MBUTTONUP:
|
|
249
|
+
return up;
|
|
250
|
+
|
|
251
|
+
default:
|
|
252
|
+
return none;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Translates wparam to VK_* key code
|
|
258
|
+
* @returns VK_LBUTTON, VK_RBUTTON, VK_MBUTTON or 0
|
|
259
|
+
*/
|
|
260
|
+
DWORD getMouseButtonCode(WPARAM wParam)
|
|
261
|
+
{
|
|
262
|
+
switch (wParam)
|
|
263
|
+
{
|
|
264
|
+
case WM_LBUTTONDOWN:
|
|
265
|
+
case WM_LBUTTONUP:
|
|
266
|
+
return VK_LBUTTON;
|
|
267
|
+
|
|
268
|
+
case WM_RBUTTONDOWN:
|
|
269
|
+
case WM_RBUTTONUP:
|
|
270
|
+
return VK_RBUTTON;
|
|
271
|
+
|
|
272
|
+
case WM_MBUTTONDOWN:
|
|
273
|
+
case WM_MBUTTONUP:
|
|
274
|
+
return VK_MBUTTON;
|
|
275
|
+
|
|
276
|
+
default:
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Used to send fake virtual key to system.
|
|
283
|
+
* @param iKeyEventF - 0 for key down event, KEYEVENTF_KEYUP for key up events and KEYEVENTF_EXTENDEDKEY for unicode(?)
|
|
284
|
+
* @param vkVirtualKey - Virtual key to send keypress for
|
|
285
|
+
*/
|
|
286
|
+
void fakeKey(DWORD iKeyEventF, WORD vkVirtualKey)
|
|
287
|
+
{
|
|
288
|
+
INPUT inputFix;
|
|
289
|
+
inputFix.type = INPUT_KEYBOARD;
|
|
290
|
+
inputFix.ki.wVk = vkVirtualKey;
|
|
291
|
+
inputFix.ki.wScan = 0;
|
|
292
|
+
inputFix.ki.dwFlags = iKeyEventF;
|
|
293
|
+
inputFix.ki.time = 0;
|
|
294
|
+
inputFix.ki.dwExtraInfo = 0;
|
|
295
|
+
SendInput(1, &inputFix, sizeof(inputFix));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Log error message to stderr
|
|
300
|
+
*/
|
|
301
|
+
void printErr(const char str[])
|
|
302
|
+
{
|
|
303
|
+
fprintf(stderr, str);
|
|
304
|
+
fflush(stderr);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Key propagation communication with timeout
|
|
308
|
+
HANDLE signalMutex = CreateMutex(NULL, FALSE, NULL);
|
|
309
|
+
HANDLE requestTimeoutSemaphore = CreateSemaphore(NULL, 0, INT_MAX, NULL);
|
|
310
|
+
HANDLE responseSemaphore = CreateSemaphore(NULL, 0, INT_MAX, NULL);
|
|
311
|
+
long requestTime = 0;
|
|
312
|
+
long responseId = 0;
|
|
313
|
+
long timeoutId = 0;
|
|
314
|
+
long curId = 0;
|
|
315
|
+
std::string output = "";
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* haltPropogation
|
|
319
|
+
* Communicates key information with the calling process to identify whether the key event should
|
|
320
|
+
* be propogated to the rest of the OS.
|
|
321
|
+
* @param key - The key pressed.
|
|
322
|
+
* @param isDown - down, if a keydown event occurred, up if a keyup event occurred, none otherwise.
|
|
323
|
+
* @returns Whether the event should be propogated or not.
|
|
324
|
+
* @remark Sends a comma delimited string of the form "keyCode,DOWN,scanCode,eventID" or "keyCode,UP,scanCode,eventID".
|
|
325
|
+
* Expects "1\n" (halt propogation of event) or "0\n" (do not halt propogation of event)
|
|
326
|
+
* @remark This function timeouts after 30ms and returns false in order to propogate the event to the rest of the OS.
|
|
327
|
+
*/
|
|
328
|
+
bool haltPropogation(bool isMouse, bool isDown, DWORD vkCode, DWORD scanCode, POINT location)
|
|
329
|
+
{
|
|
330
|
+
curId = curId + 1;
|
|
331
|
+
printf("%s,%s,%i,%i,%ld,%ld,%i\n", (isMouse ? "MOUSE" : "KEYBOARD"), (isDown ? "DOWN" : "UP"), vkCode, scanCode, location.x, location.y, curId);
|
|
332
|
+
fflush(stdout);
|
|
333
|
+
|
|
334
|
+
// Indicate when the next timeout should occur
|
|
335
|
+
requestTime = time(0) * 1000 + timeoutTime;
|
|
336
|
+
ReleaseSemaphore(requestTimeoutSemaphore, 1, NULL);
|
|
337
|
+
|
|
338
|
+
// Wait for any response
|
|
339
|
+
WaitForSingleObject(responseSemaphore, INFINITE);
|
|
340
|
+
|
|
341
|
+
return output == "1";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Synchronously reads a line from the stdin and tries to report the result to the haltPropogation function (if it hasn't
|
|
346
|
+
* timed out already)
|
|
347
|
+
*/
|
|
348
|
+
DWORD WINAPI checkInputLoop(LPVOID lpParam)
|
|
349
|
+
{
|
|
350
|
+
while (true)
|
|
351
|
+
{
|
|
352
|
+
// Retrieve input and extract the code
|
|
353
|
+
std::string entry;
|
|
354
|
+
std::getline(std::cin, entry);
|
|
355
|
+
|
|
356
|
+
int index = entry.find_first_of(",");
|
|
357
|
+
std::string code = entry.substr(0, index);
|
|
358
|
+
int id = atoi((entry.substr(index + 1)).c_str());
|
|
359
|
+
|
|
360
|
+
// Lock the signalling, making sure the timeout doesn't signal it's response yet
|
|
361
|
+
WaitForSingleObject(signalMutex, INFINITE);
|
|
362
|
+
if (timeoutId < id)
|
|
363
|
+
{
|
|
364
|
+
// Set the output and signal that there is a response
|
|
365
|
+
responseId = id;
|
|
366
|
+
output = code;
|
|
367
|
+
ReleaseSemaphore(responseSemaphore, 1, NULL);
|
|
368
|
+
}
|
|
369
|
+
ReleaseMutex(signalMutex);
|
|
370
|
+
}
|
|
371
|
+
return 0;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Synchronously waits until a timeout occurs and reports this to the haltPropogation function if it hasn't received a response
|
|
376
|
+
* yet.
|
|
377
|
+
*/
|
|
378
|
+
DWORD WINAPI timeoutLoop(LPVOID lpParam)
|
|
379
|
+
{
|
|
380
|
+
while (true)
|
|
381
|
+
{
|
|
382
|
+
// Wait for a timeout to be requested
|
|
383
|
+
WaitForSingleObject(requestTimeoutSemaphore, INFINITE);
|
|
384
|
+
|
|
385
|
+
// Calculate how long to sleep in order to wake up at the requested time and start sleeping
|
|
386
|
+
long sleepDuration = requestTime - time(0) * 1000;
|
|
387
|
+
if (sleepDuration > 0)
|
|
388
|
+
Sleep(sleepDuration);
|
|
389
|
+
|
|
390
|
+
// Lock the signalling, making sure the input signalling can't happen before we finished here
|
|
391
|
+
WaitForSingleObject(signalMutex, INFINITE);
|
|
392
|
+
timeoutId = timeoutId + 1;
|
|
393
|
+
if (responseId < timeoutId)
|
|
394
|
+
{
|
|
395
|
+
// Set the output to 0 and signal that there is a response
|
|
396
|
+
output = "0";
|
|
397
|
+
ReleaseSemaphore(responseSemaphore, 1, NULL);
|
|
398
|
+
}
|
|
399
|
+
ReleaseMutex(signalMutex);
|
|
400
|
+
}
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#include <iostream>
|
|
2
|
+
#include <X11/XKBlib.h>
|
|
3
|
+
#include <X11/extensions/XInput2.h>
|
|
4
|
+
|
|
5
|
+
int main() {
|
|
6
|
+
Display * display = XOpenDisplay(nullptr);
|
|
7
|
+
|
|
8
|
+
if (display == nullptr) {
|
|
9
|
+
std::cerr << "Cannot open X display" << std::endl;
|
|
10
|
+
exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
int xiOpcode, queryEvent, queryError;
|
|
14
|
+
if (!XQueryExtension(display, "XInputExtension", &xiOpcode, &queryEvent, &queryError)) {
|
|
15
|
+
std::cerr << "X Input extension not available" << std::endl;
|
|
16
|
+
exit(2);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
int majorVersion = 2, minorVersion = 0;
|
|
20
|
+
int queryResult = XIQueryVersion(display, &majorVersion, &minorVersion);
|
|
21
|
+
if (queryResult == BadRequest) {
|
|
22
|
+
std::cerr << "Expected X Input version 2.0" << std::endl;
|
|
23
|
+
exit(3);
|
|
24
|
+
} else if (queryResult != Success) {
|
|
25
|
+
std::cerr << "Cannot query X Input version" << std::endl;
|
|
26
|
+
exit(4);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Window rootWindow = DefaultRootWindow(display);
|
|
30
|
+
XIEventMask eventMask;
|
|
31
|
+
eventMask.deviceid = XIAllDevices;
|
|
32
|
+
eventMask.mask_len = XIMaskLen(XI_LASTEVENT);
|
|
33
|
+
|
|
34
|
+
eventMask.mask = (unsigned char *) calloc(eventMask.mask_len, sizeof(unsigned char));
|
|
35
|
+
XISetMask(eventMask.mask, XI_RawKeyPress);
|
|
36
|
+
XISetMask(eventMask.mask, XI_RawKeyRelease);
|
|
37
|
+
XISetMask(eventMask.mask, XI_RawButtonPress);
|
|
38
|
+
XISetMask(eventMask.mask, XI_RawButtonRelease);
|
|
39
|
+
|
|
40
|
+
XISelectEvents(display, rootWindow, &eventMask, 1);
|
|
41
|
+
XSync(display, false);
|
|
42
|
+
free(eventMask.mask);
|
|
43
|
+
|
|
44
|
+
int xkbOpcode, xkbEventCode;
|
|
45
|
+
if (!XkbQueryExtension(display, &xkbOpcode, &xkbEventCode, &queryError, &majorVersion, &minorVersion)) {
|
|
46
|
+
std::cerr << "X keyboard extension not available" << std::endl;
|
|
47
|
+
exit(5);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
long eventId = 0;
|
|
51
|
+
|
|
52
|
+
int lastEvtype = 0;
|
|
53
|
+
KeyCode lastKeyCode = 0;
|
|
54
|
+
|
|
55
|
+
while (true) {
|
|
56
|
+
XEvent xEvent;
|
|
57
|
+
XGenericEventCookie *cookie = (XGenericEventCookie*)&xEvent.xcookie;
|
|
58
|
+
XNextEvent(display, &xEvent);
|
|
59
|
+
|
|
60
|
+
if (XGetEventData(display, cookie)) {
|
|
61
|
+
if (
|
|
62
|
+
cookie->type == GenericEvent
|
|
63
|
+
&& cookie->extension == xiOpcode
|
|
64
|
+
) {
|
|
65
|
+
XIRawEvent *xInputRawEvent = (XIRawEvent *) cookie->data;
|
|
66
|
+
|
|
67
|
+
bool isKeyboard =
|
|
68
|
+
cookie->evtype == XI_RawKeyRelease
|
|
69
|
+
|| cookie->evtype == XI_RawKeyPress;
|
|
70
|
+
|
|
71
|
+
bool isMouse =
|
|
72
|
+
cookie->evtype == XI_RawButtonRelease
|
|
73
|
+
|| cookie->evtype == XI_RawButtonPress;
|
|
74
|
+
|
|
75
|
+
KeyCode keyCode = xInputRawEvent->detail;
|
|
76
|
+
int x = 0;
|
|
77
|
+
int y = 0;
|
|
78
|
+
|
|
79
|
+
if (isMouse) {
|
|
80
|
+
Window root, child;
|
|
81
|
+
int windowX, windowY;
|
|
82
|
+
unsigned int mask;
|
|
83
|
+
|
|
84
|
+
XQueryPointer(display, rootWindow, &root, &child, &x, &y, &windowX, &windowY, &mask);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
bool isDown =
|
|
88
|
+
cookie->evtype == XI_RawKeyPress
|
|
89
|
+
|| cookie->evtype == XI_RawButtonPress;
|
|
90
|
+
|
|
91
|
+
bool isDuplicate = lastEvtype == cookie->evtype
|
|
92
|
+
&& lastKeyCode == keyCode;
|
|
93
|
+
|
|
94
|
+
if ((isKeyboard || isMouse) && !isDuplicate) {
|
|
95
|
+
std::cout
|
|
96
|
+
<< (isKeyboard ? "KEYBOARD" : "MOUSE")
|
|
97
|
+
<< ","
|
|
98
|
+
<< (isDown ? "DOWN" : "UP")
|
|
99
|
+
<< ","
|
|
100
|
+
<< (long) keyCode
|
|
101
|
+
<< ","
|
|
102
|
+
<< x
|
|
103
|
+
<< ","
|
|
104
|
+
<< y
|
|
105
|
+
<< ","
|
|
106
|
+
<< eventId++
|
|
107
|
+
<< std::endl;
|
|
108
|
+
|
|
109
|
+
std::flush(std::cout);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lastEvtype = cookie->evtype;
|
|
113
|
+
lastKeyCode = keyCode;
|
|
114
|
+
}
|
|
115
|
+
XFreeEventData(display, cookie);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keyspy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A cross-platform global keyboard and mouse listener for Node.js",
|
|
5
|
-
"
|
|
6
|
-
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cross-platform",
|
|
7
|
+
"global",
|
|
8
|
+
"hook",
|
|
9
|
+
"key",
|
|
10
|
+
"keyboard",
|
|
11
|
+
"listener",
|
|
12
|
+
"monitor",
|
|
13
|
+
"mouse",
|
|
14
|
+
"node",
|
|
15
|
+
"spy",
|
|
16
|
+
"macos",
|
|
17
|
+
"function-keys"
|
|
18
|
+
],
|
|
19
|
+
"author": "teomyth",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/teomyth/keyspy.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/teomyth/keyspy/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/teomyth/keyspy#readme",
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"bin": {
|
|
32
|
+
"keyspy": "bin/keyspy"
|
|
33
|
+
},
|
|
7
34
|
"files": [
|
|
8
|
-
"
|
|
35
|
+
"dist/**/*",
|
|
36
|
+
"bin/**/*",
|
|
37
|
+
"native/**/*",
|
|
38
|
+
"runtime/**/*",
|
|
9
39
|
"scripts/download-binaries.js",
|
|
10
40
|
"README.md",
|
|
11
41
|
"LICENSE"
|
|
@@ -13,8 +43,16 @@
|
|
|
13
43
|
"scripts": {
|
|
14
44
|
"build": "tsc",
|
|
15
45
|
"build:native": "node scripts/build-native.js",
|
|
16
|
-
"
|
|
46
|
+
"build:dev": "npm run build:native && npm run build",
|
|
47
|
+
"check-permissions": "npm run build && node scripts/check-permissions.js",
|
|
48
|
+
"clean": "rm -rf dist build runtime coverage test-results .turbo junit.xml tsconfig.tsbuildinfo",
|
|
17
49
|
"dev": "tsc --watch",
|
|
50
|
+
"dev:setup": "npm run build:dev",
|
|
51
|
+
"cli": "npm run build && node dist/cli.js",
|
|
52
|
+
"cli:fresh": "npm run build:dev && node dist/cli.js",
|
|
53
|
+
"cli:v": "npm run build && node dist/cli.js -v",
|
|
54
|
+
"cli:vv": "npm run build && node dist/cli.js -vv",
|
|
55
|
+
"cli:vvv": "npm run build && node dist/cli.js -vvv",
|
|
18
56
|
"format": "biome format --write .",
|
|
19
57
|
"format:check": "biome format .",
|
|
20
58
|
"lint": "biome lint .",
|
|
@@ -30,48 +68,27 @@
|
|
|
30
68
|
"test": "jest",
|
|
31
69
|
"test:coverage": "jest --coverage",
|
|
32
70
|
"test:integration": "jest tests/integration",
|
|
33
|
-
"test:
|
|
71
|
+
"test:integration:safe": "npm run check-permissions && jest tests/integration",
|
|
34
72
|
"test:unit": "jest tests/unit",
|
|
35
73
|
"test:watch": "jest --watch"
|
|
36
74
|
},
|
|
37
75
|
"dependencies": {
|
|
38
|
-
"@expo/sudo-prompt": "^9.3.2"
|
|
76
|
+
"@expo/sudo-prompt": "^9.3.2",
|
|
77
|
+
"cli-table3": "^0.6.5"
|
|
39
78
|
},
|
|
40
79
|
"devDependencies": {
|
|
41
80
|
"@biomejs/biome": "^1.9.4",
|
|
42
81
|
"@release-it/conventional-changelog": "^10.0.1",
|
|
43
|
-
"@types/jest": "^
|
|
44
|
-
"@types/node": "^
|
|
45
|
-
"jest": "^
|
|
82
|
+
"@types/jest": "^30.0.0",
|
|
83
|
+
"@types/node": "^24.0.3",
|
|
84
|
+
"jest": "^30.0.0",
|
|
46
85
|
"jest-junit": "^16.0.0",
|
|
47
86
|
"release-it": "^19.0.3",
|
|
48
|
-
"ts-jest": "^29.
|
|
87
|
+
"ts-jest": "^29.4.0",
|
|
49
88
|
"ts-node": "^10.9.2",
|
|
50
89
|
"turbo": "^2.5.4",
|
|
51
90
|
"typescript": "^5.8.3"
|
|
52
91
|
},
|
|
53
|
-
"keywords": [
|
|
54
|
-
"cross-platform",
|
|
55
|
-
"global",
|
|
56
|
-
"hook",
|
|
57
|
-
"key",
|
|
58
|
-
"keyboard",
|
|
59
|
-
"listener",
|
|
60
|
-
"monitor",
|
|
61
|
-
"mouse",
|
|
62
|
-
"node",
|
|
63
|
-
"spy"
|
|
64
|
-
],
|
|
65
|
-
"author": "teomyth",
|
|
66
|
-
"license": "MIT",
|
|
67
|
-
"repository": {
|
|
68
|
-
"type": "git",
|
|
69
|
-
"url": "git+https://github.com/teomyth/keyspy.git"
|
|
70
|
-
},
|
|
71
|
-
"bugs": {
|
|
72
|
-
"url": "https://github.com/teomyth/keyspy/issues"
|
|
73
|
-
},
|
|
74
|
-
"homepage": "https://github.com/teomyth/keyspy#readme",
|
|
75
92
|
"engines": {
|
|
76
93
|
"node": ">=18.0.0"
|
|
77
94
|
},
|
|
Binary file
|
|
@@ -99,13 +99,13 @@ async function downloadAndExtract() {
|
|
|
99
99
|
try {
|
|
100
100
|
const { file: binaryFile, archive } = getPlatformInfo();
|
|
101
101
|
|
|
102
|
-
// Create
|
|
103
|
-
const
|
|
104
|
-
if (!fs.existsSync(
|
|
105
|
-
fs.mkdirSync(
|
|
102
|
+
// Create runtime directory for downloaded binaries
|
|
103
|
+
const runtimeDir = path.join(__dirname, "..", "runtime");
|
|
104
|
+
if (!fs.existsSync(runtimeDir)) {
|
|
105
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const binaryPath = path.join(
|
|
108
|
+
const binaryPath = path.join(runtimeDir, binaryFile);
|
|
109
109
|
|
|
110
110
|
// Check if binary already exists
|
|
111
111
|
if (fs.existsSync(binaryPath)) {
|
|
@@ -115,7 +115,7 @@ async function downloadAndExtract() {
|
|
|
115
115
|
|
|
116
116
|
// Download URL
|
|
117
117
|
const downloadUrl = `https://github.com/teomyth/keyspy/releases/download/v${version}/${archive}`;
|
|
118
|
-
const archivePath = path.join(
|
|
118
|
+
const archivePath = path.join(runtimeDir, archive);
|
|
119
119
|
|
|
120
120
|
log(`Platform: ${platform}-${arch}`);
|
|
121
121
|
log(`Binary: ${binaryFile}`);
|
|
@@ -126,7 +126,7 @@ async function downloadAndExtract() {
|
|
|
126
126
|
log(`Downloaded: ${archive}`);
|
|
127
127
|
|
|
128
128
|
// Extract archive
|
|
129
|
-
if (extractTarGz(archivePath,
|
|
129
|
+
if (extractTarGz(archivePath, runtimeDir)) {
|
|
130
130
|
log(`Extracted: ${archive}`);
|
|
131
131
|
|
|
132
132
|
// Make binary executable (Unix systems)
|