simen-keyboard-listener 1.0.7 → 1.0.9

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.
@@ -1,660 +0,0 @@
1
- /*---------------------------------------------------------------------------------------------
2
- * Copyright (c) Simen. All rights reserved.
3
- * Licensed under the MIT License.
4
- *--------------------------------------------------------------------------------------------*/
5
-
6
- #include <napi.h>
7
-
8
- #include <array>
9
- #include <atomic>
10
- #include <chrono>
11
- #include <condition_variable>
12
- #include <mutex>
13
- #include <optional>
14
- #include <string>
15
- #include <thread>
16
-
17
- namespace {
18
-
19
- struct KeyEvent {
20
- std::string name;
21
- std::string state; // "DOWN" | "UP"
22
- };
23
-
24
- std::atomic<bool> g_running{ false };
25
- std::mutex g_stateMutex;
26
-
27
- std::mutex g_startMutex;
28
- std::condition_variable g_startCv;
29
- int g_startResult = 0; // 0 = pending, 1 = started, -1 = failed
30
-
31
- Napi::ThreadSafeFunction g_tsfn;
32
- std::thread g_thread;
33
-
34
- void setStartResult(int value) {
35
- {
36
- std::lock_guard<std::mutex> lock(g_startMutex);
37
- g_startResult = value;
38
- }
39
- g_startCv.notify_one();
40
- }
41
-
42
- void emitKeyEvent(const std::string& name, const std::string& state) {
43
- if (!g_running.load()) {
44
- return;
45
- }
46
- if (!g_tsfn) {
47
- return;
48
- }
49
-
50
- KeyEvent* payload = new KeyEvent{ name, state };
51
- napi_status status = g_tsfn.NonBlockingCall(payload, [](Napi::Env env, Napi::Function jsCallback, KeyEvent* data) {
52
- Napi::HandleScope scope(env);
53
- Napi::Object evt = Napi::Object::New(env);
54
- evt.Set("name", Napi::String::New(env, data->name));
55
- evt.Set("state", Napi::String::New(env, data->state));
56
- jsCallback.Call({ evt });
57
- delete data;
58
- });
59
-
60
- if (status != napi_ok) {
61
- delete payload;
62
- }
63
- }
64
-
65
- } // namespace
66
-
67
- #if defined(__APPLE__)
68
-
69
- #include <ApplicationServices/ApplicationServices.h>
70
-
71
- namespace {
72
-
73
- // Check if the app has accessibility permission (required for CGEventTap)
74
- static bool macHasAccessibilityPermission() {
75
- // AXIsProcessTrusted returns true if the app has accessibility permission
76
- return AXIsProcessTrusted();
77
- }
78
-
79
- } // namespace
80
-
81
- namespace {
82
-
83
- CFMachPortRef g_eventTap = nullptr;
84
- CFRunLoopSourceRef g_runLoopSource = nullptr;
85
- CFRunLoopRef g_runLoop = nullptr;
86
-
87
- static std::atomic<bool> g_fnDown{ false };
88
- static std::atomic<bool> g_shiftDown{ false };
89
- static std::atomic<bool> g_ctrlDown{ false };
90
- static std::atomic<bool> g_altDown{ false };
91
- static std::atomic<bool> g_cmdDown{ false };
92
-
93
- static bool macHandleFlagsChanged(CGEventRef event) {
94
- const CGEventFlags flags = CGEventGetFlags(event);
95
- bool shouldBlock = false;
96
-
97
- const bool fnIsDown = (flags & kCGEventFlagMaskSecondaryFn) != 0;
98
- const bool fnPrev = g_fnDown.exchange(fnIsDown);
99
- if (fnIsDown != fnPrev) {
100
- emitKeyEvent("FN", fnIsDown ? "DOWN" : "UP");
101
- // Block FN key DOWN from triggering system emoji panel
102
- // Only block on key down, not key up, to avoid interfering with other shortcuts
103
- if (fnIsDown) {
104
- // Check if FN is pressed alone (no other modifiers)
105
- const bool hasOtherModifiers = (flags & (kCGEventFlagMaskShift | kCGEventFlagMaskControl |
106
- kCGEventFlagMaskAlternate | kCGEventFlagMaskCommand)) != 0;
107
- if (!hasOtherModifiers) {
108
- shouldBlock = true;
109
- }
110
- }
111
- }
112
-
113
- const bool shiftIsDown = (flags & kCGEventFlagMaskShift) != 0;
114
- const bool shiftPrev = g_shiftDown.exchange(shiftIsDown);
115
- if (shiftIsDown != shiftPrev) {
116
- // Emit a stable name regardless of left/right shift.
117
- emitKeyEvent("LEFT SHIFT", shiftIsDown ? "DOWN" : "UP");
118
- }
119
-
120
- const bool ctrlIsDown = (flags & kCGEventFlagMaskControl) != 0;
121
- const bool ctrlPrev = g_ctrlDown.exchange(ctrlIsDown);
122
- if (ctrlIsDown != ctrlPrev) {
123
- emitKeyEvent("LEFT CTRL", ctrlIsDown ? "DOWN" : "UP");
124
- }
125
-
126
- const bool altIsDown = (flags & kCGEventFlagMaskAlternate) != 0;
127
- const bool altPrev = g_altDown.exchange(altIsDown);
128
- if (altIsDown != altPrev) {
129
- emitKeyEvent("LEFT ALT", altIsDown ? "DOWN" : "UP");
130
- }
131
-
132
- const bool cmdIsDown = (flags & kCGEventFlagMaskCommand) != 0;
133
- const bool cmdPrev = g_cmdDown.exchange(cmdIsDown);
134
- if (cmdIsDown != cmdPrev) {
135
- emitKeyEvent("LEFT META", cmdIsDown ? "DOWN" : "UP");
136
- }
137
-
138
- return shouldBlock;
139
- }
140
-
141
- static std::optional<std::string> macKeyCodeToName(CGKeyCode keyCode) {
142
- switch (keyCode) {
143
- // Common keys
144
- case 53: return std::string("ESCAPE");
145
- case 49: return std::string("SPACE");
146
- case 36: return std::string("RETURN");
147
- case 48: return std::string("TAB");
148
- case 51: return std::string("BACKSPACE");
149
- case 117: return std::string("DELETE");
150
-
151
- // Arrow keys
152
- case 123: return std::string("LEFT");
153
- case 124: return std::string("RIGHT");
154
- case 125: return std::string("DOWN");
155
- case 126: return std::string("UP");
156
-
157
- // Letters (US keyboard physical mapping)
158
- case 0: return std::string("A");
159
- case 1: return std::string("S");
160
- case 2: return std::string("D");
161
- case 3: return std::string("F");
162
- case 4: return std::string("H");
163
- case 5: return std::string("G");
164
- case 6: return std::string("Z");
165
- case 7: return std::string("X");
166
- case 8: return std::string("C");
167
- case 9: return std::string("V");
168
- case 11: return std::string("B");
169
- case 12: return std::string("Q");
170
- case 13: return std::string("W");
171
- case 14: return std::string("E");
172
- case 15: return std::string("R");
173
- case 16: return std::string("Y");
174
- case 17: return std::string("T");
175
- case 31: return std::string("O");
176
- case 32: return std::string("U");
177
- case 34: return std::string("I");
178
- case 35: return std::string("P");
179
- case 37: return std::string("L");
180
- case 38: return std::string("J");
181
- case 40: return std::string("K");
182
- case 45: return std::string("N");
183
- case 46: return std::string("M");
184
-
185
- // Numbers row (US keyboard physical mapping)
186
- case 18: return std::string("1");
187
- case 19: return std::string("2");
188
- case 20: return std::string("3");
189
- case 21: return std::string("4");
190
- case 23: return std::string("5");
191
- case 22: return std::string("6");
192
- case 26: return std::string("7");
193
- case 28: return std::string("8");
194
- case 25: return std::string("9");
195
- case 29: return std::string("0");
196
- default: return std::nullopt;
197
- }
198
- }
199
-
200
- static CGEventRef macEventTapCallback(CGEventTapProxy /*proxy*/, CGEventType type, CGEventRef event, void* /*userInfo*/) {
201
- if (!g_running.load()) {
202
- return event;
203
- }
204
-
205
- if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) {
206
- if (g_eventTap) {
207
- CGEventTapEnable(g_eventTap, true);
208
- }
209
- g_fnDown.store(false);
210
- g_shiftDown.store(false);
211
- g_ctrlDown.store(false);
212
- g_altDown.store(false);
213
- g_cmdDown.store(false);
214
- return event;
215
- }
216
-
217
- if (type == kCGEventFlagsChanged) {
218
- bool shouldBlock = macHandleFlagsChanged(event);
219
- // Block FN key event from reaching the system
220
- if (shouldBlock) {
221
- return NULL;
222
- }
223
- return event;
224
- }
225
-
226
- if (type == kCGEventKeyDown || type == kCGEventKeyUp) {
227
- const CGKeyCode keyCode = static_cast<CGKeyCode>(CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
228
- const auto name = macKeyCodeToName(keyCode);
229
- if (name.has_value()) {
230
- emitKeyEvent(*name, type == kCGEventKeyDown ? "DOWN" : "UP");
231
- }
232
- return event;
233
- }
234
-
235
- return event;
236
- }
237
-
238
- static void macThreadMain() {
239
- // Pre-check accessibility permission before attempting to create event tap
240
- if (!macHasAccessibilityPermission()) {
241
- g_running.store(false);
242
- setStartResult(-1);
243
- return;
244
- }
245
-
246
- CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) |
247
- CGEventMaskBit(kCGEventKeyUp) |
248
- CGEventMaskBit(kCGEventFlagsChanged);
249
-
250
- g_eventTap = CGEventTapCreate(
251
- kCGSessionEventTap,
252
- kCGHeadInsertEventTap,
253
- kCGEventTapOptionDefault,
254
- mask,
255
- macEventTapCallback,
256
- nullptr
257
- );
258
-
259
- if (!g_eventTap) {
260
- g_running.store(false);
261
- setStartResult(-1);
262
- return;
263
- }
264
-
265
- g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
266
- if (!g_runLoopSource) {
267
- CFRelease(g_eventTap);
268
- g_eventTap = nullptr;
269
- g_running.store(false);
270
- setStartResult(-1);
271
- return;
272
- }
273
-
274
- g_runLoop = CFRunLoopGetCurrent();
275
- CFRunLoopAddSource(g_runLoop, g_runLoopSource, kCFRunLoopCommonModes);
276
- CGEventTapEnable(g_eventTap, true);
277
-
278
- setStartResult(1);
279
-
280
- while (g_running.load()) {
281
- CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, true);
282
- }
283
-
284
- if (g_eventTap) {
285
- CGEventTapEnable(g_eventTap, false);
286
- }
287
- if (g_runLoopSource) {
288
- CFRunLoopRemoveSource(g_runLoop, g_runLoopSource, kCFRunLoopCommonModes);
289
- CFRelease(g_runLoopSource);
290
- g_runLoopSource = nullptr;
291
- }
292
- if (g_eventTap) {
293
- CFRelease(g_eventTap);
294
- g_eventTap = nullptr;
295
- }
296
- g_runLoop = nullptr;
297
-
298
- g_fnDown.store(false);
299
- g_shiftDown.store(false);
300
- g_ctrlDown.store(false);
301
- g_altDown.store(false);
302
- g_cmdDown.store(false);
303
- }
304
-
305
- } // namespace
306
-
307
- #elif defined(_WIN32)
308
-
309
- #define NOMINMAX
310
- #include <windows.h>
311
-
312
- namespace {
313
-
314
- HHOOK g_hook = nullptr;
315
- DWORD g_threadId = 0;
316
- UINT_PTR g_timerId = 0;
317
- // Custom message to wake up the message loop for polling
318
- constexpr UINT WM_POLL_KEYS = WM_USER + 100;
319
-
320
- std::array<bool, 256> g_down{};
321
- std::array<DWORD, 256> g_lastFlags{};
322
-
323
- static std::optional<std::string> winVkToName(DWORD vkCode, DWORD flags);
324
-
325
- static bool winIsPhysicallyDown(DWORD vkCode) {
326
- return (GetAsyncKeyState(static_cast<int>(vkCode)) & 0x8000) != 0;
327
- }
328
-
329
- static void winPollMissingKeyUps() {
330
- for (DWORD vkCode = 0; vkCode < g_down.size(); vkCode++) {
331
- if (!g_down[vkCode]) {
332
- continue;
333
- }
334
- if (winIsPhysicallyDown(vkCode)) {
335
- continue;
336
- }
337
-
338
- const auto name = winVkToName(vkCode, g_lastFlags[vkCode]);
339
- if (name.has_value()) {
340
- emitKeyEvent(*name, "UP");
341
- }
342
- g_down[vkCode] = false;
343
- g_lastFlags[vkCode] = 0;
344
- }
345
- }
346
-
347
- static std::optional<std::string> winVkToName(DWORD vkCode, DWORD flags) {
348
- if (vkCode == VK_ESCAPE) {
349
- return std::string("ESCAPE");
350
- }
351
- if (vkCode == VK_LSHIFT) { return std::string("LEFT SHIFT"); }
352
- if (vkCode == VK_RSHIFT) { return std::string("RIGHT SHIFT"); }
353
-
354
- // Right Alt: VK_RMENU or VK_MENU with extended flag.
355
- if (vkCode == VK_RMENU || (vkCode == VK_MENU && (flags & LLKHF_EXTENDED))) {
356
- return std::string("RIGHT ALT");
357
- }
358
-
359
- // Left Alt (including VK_MENU without extended flag)
360
- if (vkCode == VK_LMENU || vkCode == VK_MENU) { return std::string("LEFT ALT"); }
361
-
362
- // Ctrl
363
- if (vkCode == VK_LCONTROL) { return std::string("LEFT CTRL"); }
364
- if (vkCode == VK_RCONTROL) { return std::string("RIGHT CTRL"); }
365
-
366
- // Meta (Windows key)
367
- if (vkCode == VK_LWIN) { return std::string("LEFT META"); }
368
- if (vkCode == VK_RWIN) { return std::string("RIGHT META"); }
369
-
370
- // Common keys
371
- if (vkCode == VK_SPACE) { return std::string("SPACE"); }
372
- if (vkCode == VK_RETURN) { return std::string("RETURN"); }
373
- if (vkCode == VK_TAB) { return std::string("TAB"); }
374
- if (vkCode == VK_BACK) { return std::string("BACKSPACE"); }
375
- if (vkCode == VK_DELETE) { return std::string("DELETE"); }
376
-
377
- // Arrow keys
378
- if (vkCode == VK_LEFT) { return std::string("LEFT"); }
379
- if (vkCode == VK_RIGHT) { return std::string("RIGHT"); }
380
- if (vkCode == VK_UP) { return std::string("UP"); }
381
- if (vkCode == VK_DOWN) { return std::string("DOWN"); }
382
-
383
- // A-Z
384
- if (vkCode >= 0x41 && vkCode <= 0x5A) {
385
- char c = static_cast<char>(vkCode);
386
- return std::string(1, c);
387
- }
388
-
389
- // 0-9
390
- if (vkCode >= 0x30 && vkCode <= 0x39) {
391
- char c = static_cast<char>(vkCode);
392
- return std::string(1, c);
393
- }
394
-
395
- // Function keys F1-F12
396
- if (vkCode >= VK_F1 && vkCode <= VK_F12) {
397
- int n = static_cast<int>(vkCode - VK_F1) + 1;
398
- return std::string("F") + std::to_string(n);
399
- }
400
-
401
- return std::nullopt;
402
- }
403
-
404
- static DWORD winResolveVkCode(const KBDLLHOOKSTRUCT* k) {
405
- DWORD vkCode = k->vkCode;
406
-
407
- // For modifier keys, the low-level hook can report generic VK_* values.
408
- // Resolve them to stable left/right variants so key up can be matched reliably.
409
- if (vkCode == VK_SHIFT) {
410
- // Map scan code to extended virtual key to distinguish left/right shift.
411
- const UINT mapped = MapVirtualKeyW(k->scanCode, MAPVK_VSC_TO_VK_EX);
412
- if (mapped != 0) {
413
- vkCode = mapped;
414
- }
415
- } else if (vkCode == VK_CONTROL) {
416
- vkCode = (k->flags & LLKHF_EXTENDED) ? VK_RCONTROL : VK_LCONTROL;
417
- } else if (vkCode == VK_MENU) {
418
- vkCode = (k->flags & LLKHF_EXTENDED) ? VK_RMENU : VK_LMENU;
419
- }
420
-
421
- return vkCode;
422
- }
423
-
424
- static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
425
- if (nCode == HC_ACTION && g_running.load()) {
426
- const KBDLLHOOKSTRUCT* k = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
427
- const DWORD vkCode = winResolveVkCode(k);
428
- const DWORD flags = k->flags;
429
-
430
- const bool isKeyMessage = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN || wParam == WM_KEYUP || wParam == WM_SYSKEYUP);
431
- if (isKeyMessage) {
432
- // For WH_KEYBOARD_LL, LLKHF_UP is the authoritative up/down indicator.
433
- const bool isUp = (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) || ((flags & LLKHF_UP) != 0);
434
- const bool isDown = !isUp;
435
- const auto name = winVkToName(vkCode, flags);
436
- if (name.has_value()) {
437
- emitKeyEvent(*name, isDown ? "DOWN" : "UP");
438
- if (vkCode < g_down.size()) {
439
- if (isDown) {
440
- g_down[vkCode] = true;
441
- g_lastFlags[vkCode] = flags;
442
- } else {
443
- g_down[vkCode] = false;
444
- g_lastFlags[vkCode] = 0;
445
- }
446
- }
447
- }
448
- }
449
- }
450
-
451
- return CallNextHookEx(g_hook, nCode, wParam, lParam);
452
- }
453
-
454
- // Timer callback that posts a message to wake up the message loop
455
- static VOID CALLBACK winTimerProc(HWND /*hwnd*/, UINT /*uMsg*/, UINT_PTR /*idEvent*/, DWORD /*dwTime*/) {
456
- // Post a custom message to ensure the message loop wakes up and processes polling
457
- if (g_threadId != 0) {
458
- PostThreadMessageW(g_threadId, WM_POLL_KEYS, 0, 0);
459
- }
460
- }
461
-
462
- static void winThreadMain() {
463
- g_threadId = GetCurrentThreadId();
464
-
465
- g_hook = SetWindowsHookExW(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandleW(nullptr), 0);
466
- if (!g_hook) {
467
- g_running.store(false);
468
- setStartResult(-1);
469
- return;
470
- }
471
-
472
- // Use SetTimer with a callback to ensure reliable polling even when message queue is idle.
473
- // The callback posts WM_POLL_KEYS to wake up GetMessageW.
474
- // Interval of 50ms is sufficient for detecting missed key-up events while being efficient.
475
- g_timerId = SetTimer(nullptr, 1, 50, winTimerProc);
476
-
477
- setStartResult(1);
478
-
479
- MSG msg;
480
- // Use PeekMessage with PM_REMOVE in a loop with MsgWaitForMultipleObjects for better responsiveness.
481
- // This ensures we can exit promptly when g_running becomes false.
482
- while (g_running.load()) {
483
- // Wait for messages with a timeout to allow periodic checks of g_running
484
- DWORD waitResult = MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
485
-
486
- if (waitResult == WAIT_TIMEOUT) {
487
- // Timeout - check g_running and poll for missed key-ups
488
- winPollMissingKeyUps();
489
- continue;
490
- }
491
-
492
- // Process all pending messages
493
- while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) {
494
- if (msg.message == WM_QUIT) {
495
- g_running.store(false);
496
- break;
497
- }
498
-
499
- if (msg.message == WM_POLL_KEYS) {
500
- winPollMissingKeyUps();
501
- continue;
502
- }
503
-
504
- // WM_TIMER messages from SetTimer with callback are handled by DispatchMessage
505
- TranslateMessage(&msg);
506
- DispatchMessageW(&msg);
507
- }
508
- }
509
-
510
- if (g_timerId != 0) {
511
- KillTimer(nullptr, g_timerId);
512
- g_timerId = 0;
513
- }
514
-
515
- if (g_hook) {
516
- UnhookWindowsHookEx(g_hook);
517
- g_hook = nullptr;
518
- }
519
-
520
- g_down.fill(false);
521
- g_lastFlags.fill(0);
522
-
523
- g_threadId = 0;
524
- }
525
-
526
- } // namespace
527
-
528
- #else
529
-
530
- namespace {
531
- static void unsupportedThreadMain() {
532
- g_running.store(false);
533
- setStartResult(-1);
534
- }
535
- } // namespace
536
-
537
- #endif
538
-
539
- static Napi::Value Start(const Napi::CallbackInfo& info) {
540
- Napi::Env env = info.Env();
541
-
542
- if (info.Length() < 1 || !info[0].IsFunction()) {
543
- Napi::TypeError::New(env, "Expected a callback function").ThrowAsJavaScriptException();
544
- return Napi::Boolean::New(env, false);
545
- }
546
-
547
- std::lock_guard<std::mutex> lock(g_stateMutex);
548
- if (g_running.load()) {
549
- return Napi::Boolean::New(env, true);
550
- }
551
-
552
- Napi::Function cb = info[0].As<Napi::Function>();
553
- g_tsfn = Napi::ThreadSafeFunction::New(
554
- env,
555
- cb,
556
- "simen_keyboard_listener",
557
- 0,
558
- 1
559
- );
560
-
561
- {
562
- std::lock_guard<std::mutex> startLock(g_startMutex);
563
- g_startResult = 0;
564
- }
565
-
566
- g_running.store(true);
567
-
568
- #if defined(__APPLE__)
569
- g_thread = std::thread(macThreadMain);
570
- #elif defined(_WIN32)
571
- g_thread = std::thread(winThreadMain);
572
- #else
573
- g_thread = std::thread(unsupportedThreadMain);
574
- #endif
575
-
576
- {
577
- std::unique_lock<std::mutex> startLock(g_startMutex);
578
- g_startCv.wait_for(startLock, std::chrono::milliseconds(1500), []() { return g_startResult != 0; });
579
- }
580
-
581
- if (!g_running.load() || g_startResult != 1) {
582
- g_running.store(false);
583
-
584
- #if defined(_WIN32)
585
- if (g_threadId) {
586
- PostThreadMessageW(g_threadId, WM_QUIT, 0, 0);
587
- }
588
- #endif
589
-
590
- if (g_thread.joinable()) {
591
- g_thread.join();
592
- }
593
-
594
- if (g_tsfn) {
595
- g_tsfn.Release();
596
- g_tsfn = Napi::ThreadSafeFunction();
597
- }
598
-
599
- return Napi::Boolean::New(env, false);
600
- }
601
-
602
- return Napi::Boolean::New(env, true);
603
- }
604
-
605
- static Napi::Value Stop(const Napi::CallbackInfo& info) {
606
- Napi::Env env = info.Env();
607
-
608
- std::lock_guard<std::mutex> lock(g_stateMutex);
609
- if (!g_running.load()) {
610
- return env.Undefined();
611
- }
612
-
613
- g_running.store(false);
614
-
615
- #if defined(_WIN32)
616
- if (g_threadId) {
617
- PostThreadMessageW(g_threadId, WM_QUIT, 0, 0);
618
- }
619
- #endif
620
-
621
- if (g_thread.joinable()) {
622
- g_thread.join();
623
- }
624
-
625
- if (g_tsfn) {
626
- g_tsfn.Release();
627
- g_tsfn = Napi::ThreadSafeFunction();
628
- }
629
-
630
- return env.Undefined();
631
- }
632
-
633
- static Napi::Value IsRunning(const Napi::CallbackInfo& info) {
634
- return Napi::Boolean::New(info.Env(), g_running.load());
635
- }
636
-
637
- // Check if the app has permission to listen to global keyboard events
638
- // Returns: true if permission granted, false otherwise
639
- // On Windows, always returns true (no special permission needed)
640
- // On macOS, checks accessibility permission
641
- static Napi::Value CheckPermission(const Napi::CallbackInfo& info) {
642
- #if defined(__APPLE__)
643
- return Napi::Boolean::New(info.Env(), macHasAccessibilityPermission());
644
- #elif defined(_WIN32)
645
- // Windows doesn't require special permission for keyboard hooks
646
- return Napi::Boolean::New(info.Env(), true);
647
- #else
648
- return Napi::Boolean::New(info.Env(), false);
649
- #endif
650
- }
651
-
652
- static Napi::Object Init(Napi::Env env, Napi::Object exports) {
653
- exports.Set("start", Napi::Function::New(env, Start));
654
- exports.Set("stop", Napi::Function::New(env, Stop));
655
- exports.Set("isRunning", Napi::Function::New(env, IsRunning));
656
- exports.Set("checkPermission", Napi::Function::New(env, CheckPermission));
657
- return exports;
658
- }
659
-
660
- NODE_API_MODULE(simen_keyboard_listener, Init)