node-mac-recorder 2.23.3 → 2.24.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/binding.gyp CHANGED
@@ -11,6 +11,7 @@
11
11
  "src/audio_recorder.mm",
12
12
  "src/audio_mixer.mm",
13
13
  "src/cursor_tracker.mm",
14
+ "src/keyboard_tracker.mm",
14
15
  "src/window_selector.mm"
15
16
  ],
16
17
  "include_dirs": [
package/index.js CHANGED
@@ -65,6 +65,9 @@ class MacRecorder extends EventEmitter {
65
65
  this.syncTimestamp = null;
66
66
  this.audioCaptureFile = null;
67
67
  this.audioCaptureActive = false;
68
+ // Keyboard (shortcut) capture variables
69
+ this.keyboardCaptureFile = null;
70
+ this.keyboardCaptureActive = false;
68
71
 
69
72
  this.options = {
70
73
  includeMicrophone: false, // Default olarak mikrofon kapalı
@@ -747,6 +750,7 @@ class MacRecorder extends EventEmitter {
747
750
  this.outputPath = outputPath;
748
751
 
749
752
  const cursorFilePath = path.join(outputDir, `temp_cursor_${sessionTimestamp}.json`);
753
+ const keyboardFilePath = path.join(outputDir, `temp_keyboard_${sessionTimestamp}.json`);
750
754
  // CRITICAL FIX: Use .mov extension for camera (native recorder uses .mov, not .webm)
751
755
  let cameraFilePath =
752
756
  this.options.captureCamera === true
@@ -862,6 +866,31 @@ class MacRecorder extends EventEmitter {
862
866
  this.recordingStartTime = syncTimestamp;
863
867
  console.log(`🎯 CURSOR SYNC: Cursor tracking will use timestamp: ${syncTimestamp}`);
864
868
 
869
+ // CURSOR/VIDEO TIME ALIGNMENT:
870
+ // Cursor timeline'in t=0'i bu andir (syncTimestamp), ama videonun
871
+ // ILK KARESI daha once yakalanmis olabilir (ScreenCaptureKit
872
+ // baslatma gecikmesi + yukaridaki hazir-olma beklemesi). Editor
873
+ // "cursor'un ilk ornegi = video t=0" varsayarsa aradaki fark sabit
874
+ // bir zaman kaymasi olarak kalir. Native gercek video baslangicini
875
+ // biliyor; okuyup sakla ki stop'ta cursor JSON'una yazabilelim.
876
+ this.videoStartTimestamp = 0;
877
+ try {
878
+ const nativeVideoStart =
879
+ typeof nativeBinding.getVideoStartTimestamp === 'function'
880
+ ? Number(nativeBinding.getVideoStartTimestamp())
881
+ : 0;
882
+ if (Number.isFinite(nativeVideoStart) && nativeVideoStart > 0) {
883
+ this.videoStartTimestamp = nativeVideoStart;
884
+ console.log(
885
+ `🎯 SYNC: Video first-frame timestamp: ${nativeVideoStart} (cursor starts ${(syncTimestamp - nativeVideoStart).toFixed(0)}ms later)`
886
+ );
887
+ } else {
888
+ console.warn('⚠️ SYNC: Video start timestamp unavailable — cursor sync metadata will be skipped');
889
+ }
890
+ } catch (videoStartError) {
891
+ console.warn('⚠️ SYNC: Video start timestamp read failed:', videoStartError.message);
892
+ }
893
+
865
894
  const standardCursorOptions = {
866
895
  videoRelative: true,
867
896
  displayInfo: this.recordingDisplayInfo,
@@ -880,6 +909,16 @@ class MacRecorder extends EventEmitter {
880
909
  console.warn('⚠️ Cursor tracking failed to start:', cursorError.message);
881
910
  // Continue with recording even if cursor fails - don't stop native recording
882
911
  }
912
+
913
+ // Klavye kısayolu yakalama (cursor ile AYNI zaman referansı)
914
+ try {
915
+ console.log('⌨️ SYNC: Starting keyboard shortcut capture at timestamp:', syncTimestamp);
916
+ await this.startKeyboardCapture(keyboardFilePath, { startTimestamp: syncTimestamp });
917
+ console.log('✅ SYNC: Keyboard shortcut capture started successfully');
918
+ } catch (keyboardError) {
919
+ console.warn('⚠️ Keyboard capture failed to start:', keyboardError.message);
920
+ // Klavye başlamazsa kayıt devam etsin
921
+ }
883
922
  }
884
923
 
885
924
  if (success) {
@@ -983,6 +1022,7 @@ class MacRecorder extends EventEmitter {
983
1022
  cameraOutputPath: this.cameraCaptureFile || null,
984
1023
  audioOutputPath: this.audioCaptureFile || null,
985
1024
  cursorOutputPath: cursorFilePath,
1025
+ keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
986
1026
  sessionTimestamp: fileTimestampPayload,
987
1027
  syncTimestamp: startTimestampPayload,
988
1028
  fileTimestamp: fileTimestampPayload,
@@ -1003,6 +1043,7 @@ class MacRecorder extends EventEmitter {
1003
1043
  cameraOutputPath: this.cameraCaptureFile || null,
1004
1044
  audioOutputPath: this.audioCaptureFile || null,
1005
1045
  cursorOutputPath: cursorFilePath,
1046
+ keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
1006
1047
  sessionTimestamp: fileTimestampPayload,
1007
1048
  syncTimestamp: startTimestampPayload,
1008
1049
  fileTimestamp: fileTimestampPayload,
@@ -1026,6 +1067,7 @@ class MacRecorder extends EventEmitter {
1026
1067
  cameraOutputPath: this.cameraCaptureFile || null,
1027
1068
  audioOutputPath: this.audioCaptureFile || null,
1028
1069
  cursorOutputPath: cursorFilePath,
1070
+ keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
1029
1071
  sessionTimestamp: fileTimestampPayload,
1030
1072
  syncTimestamp: startTimestampPayload,
1031
1073
  fileTimestamp: fileTimestampPayload,
@@ -1107,6 +1149,17 @@ class MacRecorder extends EventEmitter {
1107
1149
  }
1108
1150
  }
1109
1151
 
1152
+ // Klavye kısayolu yakalamayı durdur (instant)
1153
+ if (this.keyboardCaptureActive) {
1154
+ try {
1155
+ console.log('🛑 SYNC: Stopping keyboard shortcut capture');
1156
+ await this.stopKeyboardCapture();
1157
+ console.log('✅ SYNC: Keyboard shortcut capture stopped');
1158
+ } catch (keyboardError) {
1159
+ console.warn('⚠️ Keyboard capture failed to stop:', keyboardError.message);
1160
+ }
1161
+ }
1162
+
1110
1163
  let success = false;
1111
1164
 
1112
1165
  // 2. Stop native screen recording
@@ -1681,12 +1734,20 @@ class MacRecorder extends EventEmitter {
1681
1734
  this.cursorCaptureInterval = null;
1682
1735
 
1683
1736
  // Dosyayı kapat
1737
+ let closedCursorFile = null;
1684
1738
  if (this.cursorCaptureFile) {
1685
1739
  const fs = require("fs");
1686
1740
  fs.appendFileSync(this.cursorCaptureFile, "]");
1741
+ closedCursorFile = this.cursorCaptureFile;
1687
1742
  this.cursorCaptureFile = null;
1688
1743
  }
1689
1744
 
1745
+ // Cursor/video zaman hizalama bilgisini dosyaya yaz (editör bunu
1746
+ // okuyup sabit kaymayı telafi ediyor).
1747
+ if (closedCursorFile) {
1748
+ this._writeCursorSyncMetadata(closedCursorFile);
1749
+ }
1750
+
1690
1751
  // Değişkenleri temizle
1691
1752
  this.lastCapturedData = null;
1692
1753
  this.cursorCaptureStartTime = null;
@@ -1701,6 +1762,118 @@ class MacRecorder extends EventEmitter {
1701
1762
  });
1702
1763
  }
1703
1764
 
1765
+ /**
1766
+ * Cursor JSON'una video/cursor zaman hizalama bilgisini yazar.
1767
+ *
1768
+ * NEDEN: Cursor timeline'inin t=0'i `syncTimestamp` (kayit hazir olduktan
1769
+ * sonraki an), videonun t=0'i ise ILK KARENIN yakalandigi an. ScreenCaptureKit
1770
+ * baslatma gecikmesi yuzunden bu ikisi ayni degil; fark bilinmezse oynatimda
1771
+ * sabit bir zaman kaymasi olusur (cursor video ile "sync tutmuyor").
1772
+ * Editor tarafi `_syncMetadata.videoStartTime` / `cursorStartTime` okuyup
1773
+ * tam telafi ediyor — burada sadece gercek degerleri yaziyoruz.
1774
+ *
1775
+ * Dosyanin ilk noktasina yazilir; okuyucu ilk metadata'yi bulup kullanir.
1776
+ */
1777
+ _writeCursorSyncMetadata(cursorFilePath) {
1778
+ const videoStartTime = Number(this.videoStartTimestamp);
1779
+ const cursorStartTime = Number(this.syncTimestamp);
1780
+ if (
1781
+ !Number.isFinite(videoStartTime) ||
1782
+ videoStartTime <= 0 ||
1783
+ !Number.isFinite(cursorStartTime) ||
1784
+ cursorStartTime <= 0
1785
+ ) {
1786
+ // Native video baslangici okunamadiysa metadata yazma — editor eski
1787
+ // (telafisiz) davranisa duser, yanlis bir offset uygulamaktan iyidir.
1788
+ return;
1789
+ }
1790
+
1791
+ try {
1792
+ const fs = require("fs");
1793
+ const raw = fs.readFileSync(cursorFilePath, "utf8");
1794
+ const positions = JSON.parse(raw);
1795
+ if (!Array.isArray(positions) || positions.length === 0) return;
1796
+
1797
+ positions[0]._syncMetadata = {
1798
+ videoStartTime,
1799
+ cursorStartTime,
1800
+ startDelayMs: cursorStartTime - videoStartTime,
1801
+ recordingType: this.options?.windowId
1802
+ ? "window"
1803
+ : this.options?.captureArea
1804
+ ? "area"
1805
+ : "display",
1806
+ };
1807
+
1808
+ fs.writeFileSync(cursorFilePath, JSON.stringify(positions));
1809
+ console.log(
1810
+ `🎯 SYNC: Cursor sync metadata written (video→cursor delay ${(cursorStartTime - videoStartTime).toFixed(0)}ms)`
1811
+ );
1812
+ } catch (error) {
1813
+ console.warn("⚠️ SYNC: Cursor sync metadata write failed:", error.message);
1814
+ }
1815
+ }
1816
+
1817
+ /**
1818
+ * Klavye kısayolu (shortcut) yakalamayı başlatır.
1819
+ * Native CGEventTap keyDown olaylarını dinler; yalnızca ⌘/⌃/⌥ modifier'lı
1820
+ * basımları zaman damgalı JSON olarak dosyaya yazar (gizlilik: düz yazım kaydedilmez).
1821
+ * @param {string} filepath - Keyboard data JSON dosya yolu
1822
+ * @param {Object} options
1823
+ * @param {number} options.startTimestamp - Cursor/video ile hizalamak için başlangıç zamanı (ms)
1824
+ */
1825
+ async startKeyboardCapture(filepath, options = {}) {
1826
+ if (typeof filepath !== "string" || !filepath) {
1827
+ throw new Error("Keyboard capture filepath (string) required");
1828
+ }
1829
+ if (
1830
+ !nativeBinding ||
1831
+ typeof nativeBinding.startKeyboardTracking !== "function"
1832
+ ) {
1833
+ // Eski native binary — sessizce atla
1834
+ throw new Error("Native keyboard tracking not available in this build");
1835
+ }
1836
+ if (this.keyboardCaptureActive) {
1837
+ throw new Error("Keyboard capture is already running");
1838
+ }
1839
+
1840
+ const startTimestamp = Number(options.startTimestamp) || Date.now();
1841
+ const started = nativeBinding.startKeyboardTracking(filepath, startTimestamp);
1842
+ if (!started) {
1843
+ throw new Error(
1844
+ "Failed to start keyboard tracking (Accessibility permission may be required)"
1845
+ );
1846
+ }
1847
+
1848
+ this.keyboardCaptureFile = filepath;
1849
+ this.keyboardCaptureActive = true;
1850
+ this.emit("keyboardCaptureStarted", filepath);
1851
+ return true;
1852
+ }
1853
+
1854
+ /**
1855
+ * Klavye kısayolu yakalamayı durdurur ve JSON dosyasını kapatır.
1856
+ */
1857
+ async stopKeyboardCapture() {
1858
+ if (
1859
+ !nativeBinding ||
1860
+ typeof nativeBinding.stopKeyboardTracking !== "function"
1861
+ ) {
1862
+ this.keyboardCaptureActive = false;
1863
+ return false;
1864
+ }
1865
+ if (!this.keyboardCaptureActive) {
1866
+ return false;
1867
+ }
1868
+ try {
1869
+ nativeBinding.stopKeyboardTracking();
1870
+ } finally {
1871
+ this.keyboardCaptureActive = false;
1872
+ this.emit("keyboardCaptureStopped");
1873
+ }
1874
+ return true;
1875
+ }
1876
+
1704
1877
  /**
1705
1878
  * Anlık cursor pozisyonunu ve tipini döndürür
1706
1879
  * Display-relative koordinatlar döner (her zaman pozitif)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.23.3",
3
+ "version": "2.24.1",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,291 @@
1
+ // keyboard_tracker.mm
2
+ // Kayıt sırasında kullanıcının klavye KISAYOLLARINI (shortcut) yakalar.
3
+ // cursor_tracker.mm'deki CGEventTap altyapısıyla aynı desende çalışır: global bir
4
+ // keyDown event tap kurar, her kısayol basımını zaman damgalı JSON olarak dosyaya
5
+ // yazar. GİZLİLİK: yalnızca bir modifier (⌘ / ⌃ / ⌥) ile birlikte basılan tuşlar
6
+ // yakalanır; düz metin yazımı (parola vb.) KAYDEDİLMEZ.
7
+
8
+ #import <napi.h>
9
+ #import <AppKit/AppKit.h>
10
+ #import <Foundation/Foundation.h>
11
+ #import <CoreGraphics/CoreGraphics.h>
12
+ #import <Carbon/Carbon.h>
13
+ #import "logging.h"
14
+
15
+ // ---- Global durum (tek aktif oturum) --------------------------------------
16
+ static CFMachPortRef g_kbEventTap = NULL;
17
+ static CFRunLoopSourceRef g_kbRunLoopSource = NULL;
18
+ static NSFileHandle *g_kbFileHandle = nil;
19
+ static NSString *g_kbOutputPath = nil;
20
+ static BOOL g_kbIsTracking = NO;
21
+ static BOOL g_kbIsFirstWrite = YES;
22
+ static double g_kbStartUnixMs = 0.0; // Senkron için referans başlangıç (ms)
23
+
24
+ static double NowUnixMs() {
25
+ return [[NSDate date] timeIntervalSince1970] * 1000.0;
26
+ }
27
+
28
+ // Özel (yazılamayan) tuşların okunabilir isimleri. Diğer tuşlar için
29
+ // charactersIgnoringModifiers kullanılır.
30
+ static NSString *SpecialKeyName(unsigned short keyCode) {
31
+ switch (keyCode) {
32
+ case kVK_Return: return @"Enter";
33
+ case kVK_ANSI_KeypadEnter: return @"Enter";
34
+ case kVK_Tab: return @"Tab";
35
+ case kVK_Space: return @"Space";
36
+ case kVK_Delete: return @"Delete"; // Backspace
37
+ case kVK_ForwardDelete: return @"ForwardDelete";
38
+ case kVK_Escape: return @"Esc";
39
+ case kVK_Home: return @"Home";
40
+ case kVK_End: return @"End";
41
+ case kVK_PageUp: return @"PageUp";
42
+ case kVK_PageDown: return @"PageDown";
43
+ case kVK_LeftArrow: return @"Left";
44
+ case kVK_RightArrow: return @"Right";
45
+ case kVK_DownArrow: return @"Down";
46
+ case kVK_UpArrow: return @"Up";
47
+ case kVK_F1: return @"F1";
48
+ case kVK_F2: return @"F2";
49
+ case kVK_F3: return @"F3";
50
+ case kVK_F4: return @"F4";
51
+ case kVK_F5: return @"F5";
52
+ case kVK_F6: return @"F6";
53
+ case kVK_F7: return @"F7";
54
+ case kVK_F8: return @"F8";
55
+ case kVK_F9: return @"F9";
56
+ case kVK_F10: return @"F10";
57
+ case kVK_F11: return @"F11";
58
+ case kVK_F12: return @"F12";
59
+ default: return nil;
60
+ }
61
+ }
62
+
63
+ static NSString *JsonEscape(NSString *input) {
64
+ if (!input) return @"";
65
+ NSMutableString *out = [NSMutableString stringWithCapacity:input.length + 2];
66
+ NSUInteger len = input.length;
67
+ for (NSUInteger i = 0; i < len; i++) {
68
+ unichar c = [input characterAtIndex:i];
69
+ switch (c) {
70
+ case '"': [out appendString:@"\\\""]; break;
71
+ case '\\': [out appendString:@"\\\\"]; break;
72
+ case '\n': [out appendString:@"\\n"]; break;
73
+ case '\r': [out appendString:@"\\r"]; break;
74
+ case '\t': [out appendString:@"\\t"]; break;
75
+ default:
76
+ if (c < 0x20) {
77
+ [out appendFormat:@"\\u%04x", c];
78
+ } else {
79
+ [out appendFormat:@"%C", c];
80
+ }
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ static void WriteKeyboardEvent(NSString *jsonObject) {
87
+ if (!g_kbFileHandle || !jsonObject) return;
88
+ @try {
89
+ NSString *chunk = g_kbIsFirstWrite ? jsonObject : [@"," stringByAppendingString:jsonObject];
90
+ [g_kbFileHandle writeData:[chunk dataUsingEncoding:NSUTF8StringEncoding]];
91
+ g_kbIsFirstWrite = NO;
92
+ } @catch (NSException *e) {
93
+ // Sessizce devam et
94
+ }
95
+ }
96
+
97
+ // keyDown event callback — yalnızca modifier'lı basımları (kısayol) yazar.
98
+ static CGEventRef KeyboardEventCallback(CGEventTapProxy proxy,
99
+ CGEventType type,
100
+ CGEventRef event,
101
+ void *refcon) {
102
+ if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) {
103
+ if (g_kbEventTap) {
104
+ CGEventTapEnable(g_kbEventTap, true);
105
+ }
106
+ return event;
107
+ }
108
+
109
+ if (type != kCGEventKeyDown || !g_kbIsTracking) {
110
+ return event;
111
+ }
112
+
113
+ @autoreleasepool {
114
+ CGEventFlags flags = CGEventGetFlags(event);
115
+ BOOL hasCommand = (flags & kCGEventFlagMaskCommand) != 0;
116
+ BOOL hasControl = (flags & kCGEventFlagMaskControl) != 0;
117
+ BOOL hasOption = (flags & kCGEventFlagMaskAlternate) != 0;
118
+ BOOL hasShift = (flags & kCGEventFlagMaskShift) != 0;
119
+ BOOL hasFn = (flags & kCGEventFlagMaskSecondaryFn) != 0;
120
+
121
+ // GİZLİLİK: Kısayol = ⌘ / ⌃ / ⌥ içermeli. Sadece Shift veya düz tuş → atla.
122
+ if (!(hasCommand || hasControl || hasOption)) {
123
+ return event;
124
+ }
125
+
126
+ unsigned short keyCode = (unsigned short)CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode);
127
+
128
+ NSString *keyName = SpecialKeyName(keyCode);
129
+ NSString *chars = @"";
130
+ NSEvent *nsEvent = nil;
131
+ @try {
132
+ nsEvent = [NSEvent eventWithCGEvent:event];
133
+ } @catch (NSException *e) {
134
+ nsEvent = nil;
135
+ }
136
+ if (!keyName) {
137
+ NSString *raw = nsEvent ? [nsEvent charactersIgnoringModifiers] : nil;
138
+ if (raw.length > 0) {
139
+ chars = raw;
140
+ keyName = [raw uppercaseString];
141
+ } else {
142
+ keyName = [NSString stringWithFormat:@"Key%d", (int)keyCode];
143
+ }
144
+ } else {
145
+ chars = keyName;
146
+ }
147
+
148
+ double unixMs = NowUnixMs();
149
+ double relMs = g_kbStartUnixMs > 0 ? (unixMs - g_kbStartUnixMs) : 0.0;
150
+ if (relMs < 0) relMs = 0;
151
+
152
+ NSString *json = [NSString stringWithFormat:
153
+ @"{\"timestamp\":%.0f,\"unixTimeMs\":%.0f,\"type\":\"keydown\",\"keyCode\":%d,"
154
+ @"\"key\":\"%@\",\"chars\":\"%@\",\"modifiers\":{\"meta\":%@,\"control\":%@,"
155
+ @"\"alt\":%@,\"shift\":%@,\"fn\":%@}}",
156
+ relMs, unixMs, (int)keyCode,
157
+ JsonEscape(keyName), JsonEscape(chars),
158
+ hasCommand ? @"true" : @"false",
159
+ hasControl ? @"true" : @"false",
160
+ hasOption ? @"true" : @"false",
161
+ hasShift ? @"true" : @"false",
162
+ hasFn ? @"true" : @"false"];
163
+
164
+ WriteKeyboardEvent(json);
165
+ }
166
+
167
+ return event;
168
+ }
169
+
170
+ static void CleanupKeyboardTracking() {
171
+ g_kbIsTracking = NO;
172
+
173
+ if (g_kbFileHandle) {
174
+ @try {
175
+ // Açılış "[" başlangıçta yazıldığı için burada her zaman sadece "]" ile kapat.
176
+ // (boş durumda dosya "[]" olur, dolu durumda "[{...},{...}]")
177
+ [g_kbFileHandle writeData:[@"]" dataUsingEncoding:NSUTF8StringEncoding]];
178
+ [g_kbFileHandle synchronizeFile];
179
+ [g_kbFileHandle closeFile];
180
+ } @catch (NSException *e) {
181
+ // yut
182
+ }
183
+ g_kbFileHandle = nil;
184
+ }
185
+
186
+ if (g_kbEventTap) {
187
+ CGEventTapEnable(g_kbEventTap, false);
188
+ // CFRelease yapmıyoruz — cursor_tracker.mm ile aynı yaklaşım (sistem yönetsin)
189
+ g_kbEventTap = NULL;
190
+ }
191
+ if (g_kbRunLoopSource) {
192
+ g_kbRunLoopSource = NULL;
193
+ }
194
+
195
+ g_kbOutputPath = nil;
196
+ g_kbIsFirstWrite = YES;
197
+ g_kbStartUnixMs = 0.0;
198
+ }
199
+
200
+ // ---- NAPI: startKeyboardTracking(outputPath, startTimestampMs?) -----------
201
+ Napi::Value StartKeyboardTracking(const Napi::CallbackInfo& info) {
202
+ Napi::Env env = info.Env();
203
+
204
+ if (info.Length() < 1 || !info[0].IsString()) {
205
+ Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
206
+ return env.Null();
207
+ }
208
+
209
+ if (g_kbIsTracking) {
210
+ return Napi::Boolean::New(env, false);
211
+ }
212
+
213
+ std::string outputPath = info[0].As<Napi::String>().Utf8Value();
214
+ double startTs = 0.0;
215
+ if (info.Length() >= 2 && info[1].IsNumber()) {
216
+ startTs = info[1].As<Napi::Number>().DoubleValue();
217
+ }
218
+
219
+ @try {
220
+ g_kbOutputPath = [NSString stringWithUTF8String:outputPath.c_str()];
221
+
222
+ [[NSFileManager defaultManager] createFileAtPath:g_kbOutputPath contents:nil attributes:nil];
223
+ g_kbFileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_kbOutputPath] retain];
224
+ if (!g_kbFileHandle) {
225
+ return Napi::Boolean::New(env, false);
226
+ }
227
+ [g_kbFileHandle truncateFileAtOffset:0];
228
+ [g_kbFileHandle writeData:[@"[" dataUsingEncoding:NSUTF8StringEncoding]];
229
+
230
+ g_kbIsFirstWrite = YES;
231
+ g_kbStartUnixMs = startTs > 0 ? startTs : NowUnixMs();
232
+
233
+ CGEventMask eventMask = CGEventMaskBit(kCGEventKeyDown);
234
+ g_kbEventTap = CGEventTapCreate(kCGSessionEventTap,
235
+ kCGHeadInsertEventTap,
236
+ kCGEventTapOptionListenOnly,
237
+ eventMask,
238
+ KeyboardEventCallback,
239
+ NULL);
240
+
241
+ if (!g_kbEventTap) {
242
+ NSLog(@"⚠️ Failed to create keyboard event tap (requires Accessibility permission)");
243
+ CleanupKeyboardTracking();
244
+ return Napi::Boolean::New(env, false);
245
+ }
246
+
247
+ g_kbRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_kbEventTap, 0);
248
+ CFRunLoopAddSource(CFRunLoopGetMain(), g_kbRunLoopSource, kCFRunLoopCommonModes);
249
+ CGEventTapEnable(g_kbEventTap, true);
250
+
251
+ g_kbIsTracking = YES;
252
+ NSLog(@"✅ Keyboard shortcut tracking active");
253
+ return Napi::Boolean::New(env, true);
254
+ } @catch (NSException *exception) {
255
+ CleanupKeyboardTracking();
256
+ return Napi::Boolean::New(env, false);
257
+ }
258
+ }
259
+
260
+ // ---- NAPI: stopKeyboardTracking() -----------------------------------------
261
+ Napi::Value StopKeyboardTracking(const Napi::CallbackInfo& info) {
262
+ Napi::Env env = info.Env();
263
+ if (!g_kbIsTracking) {
264
+ return Napi::Boolean::New(env, false);
265
+ }
266
+ @try {
267
+ CleanupKeyboardTracking();
268
+ return Napi::Boolean::New(env, true);
269
+ } @catch (NSException *exception) {
270
+ CleanupKeyboardTracking();
271
+ return Napi::Boolean::New(env, false);
272
+ }
273
+ }
274
+
275
+ // ---- NAPI: getKeyboardTrackingStatus() ------------------------------------
276
+ Napi::Value GetKeyboardTrackingStatus(const Napi::CallbackInfo& info) {
277
+ Napi::Env env = info.Env();
278
+ Napi::Object status = Napi::Object::New(env);
279
+ status.Set("isTracking", Napi::Boolean::New(env, g_kbIsTracking));
280
+ status.Set("outputPath",
281
+ g_kbOutputPath ? Napi::String::New(env, [g_kbOutputPath UTF8String])
282
+ : env.Null());
283
+ return status;
284
+ }
285
+
286
+ Napi::Object InitKeyboardTracker(Napi::Env env, Napi::Object exports) {
287
+ exports.Set("startKeyboardTracking", Napi::Function::New(env, StartKeyboardTracking));
288
+ exports.Set("stopKeyboardTracking", Napi::Function::New(env, StopKeyboardTracking));
289
+ exports.Set("getKeyboardTrackingStatus", Napi::Function::New(env, GetKeyboardTrackingStatus));
290
+ return exports;
291
+ }
@@ -48,7 +48,10 @@ extern "C" {
48
48
  // Cursor tracker function declarations
49
49
  Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
50
50
 
51
- // Window selector function declarations
51
+ // Keyboard (shortcut) tracker function declarations
52
+ Napi::Object InitKeyboardTracker(Napi::Env env, Napi::Object exports);
53
+
54
+ // Window selector function declarations
52
55
  Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports);
53
56
 
54
57
  // Window selector overlay functions (external)
@@ -1717,7 +1720,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
1717
1720
 
1718
1721
  // Initialize cursor tracker
1719
1722
  InitCursorTracker(env, exports);
1720
-
1723
+
1724
+ // Initialize keyboard (shortcut) tracker
1725
+ InitKeyboardTracker(env, exports);
1726
+
1721
1727
  // Initialize window selector
1722
1728
  InitWindowSelector(env, exports);
1723
1729