node-mac-recorder 2.23.3 → 2.24.0
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 +1 -0
- package/index.js +88 -0
- package/package.json +1 -1
- package/src/keyboard_tracker.mm +291 -0
- package/src/mac_recorder.mm +8 -2
package/binding.gyp
CHANGED
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
|
|
@@ -880,6 +884,16 @@ class MacRecorder extends EventEmitter {
|
|
|
880
884
|
console.warn('⚠️ Cursor tracking failed to start:', cursorError.message);
|
|
881
885
|
// Continue with recording even if cursor fails - don't stop native recording
|
|
882
886
|
}
|
|
887
|
+
|
|
888
|
+
// Klavye kısayolu yakalama (cursor ile AYNI zaman referansı)
|
|
889
|
+
try {
|
|
890
|
+
console.log('⌨️ SYNC: Starting keyboard shortcut capture at timestamp:', syncTimestamp);
|
|
891
|
+
await this.startKeyboardCapture(keyboardFilePath, { startTimestamp: syncTimestamp });
|
|
892
|
+
console.log('✅ SYNC: Keyboard shortcut capture started successfully');
|
|
893
|
+
} catch (keyboardError) {
|
|
894
|
+
console.warn('⚠️ Keyboard capture failed to start:', keyboardError.message);
|
|
895
|
+
// Klavye başlamazsa kayıt devam etsin
|
|
896
|
+
}
|
|
883
897
|
}
|
|
884
898
|
|
|
885
899
|
if (success) {
|
|
@@ -983,6 +997,7 @@ class MacRecorder extends EventEmitter {
|
|
|
983
997
|
cameraOutputPath: this.cameraCaptureFile || null,
|
|
984
998
|
audioOutputPath: this.audioCaptureFile || null,
|
|
985
999
|
cursorOutputPath: cursorFilePath,
|
|
1000
|
+
keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
|
|
986
1001
|
sessionTimestamp: fileTimestampPayload,
|
|
987
1002
|
syncTimestamp: startTimestampPayload,
|
|
988
1003
|
fileTimestamp: fileTimestampPayload,
|
|
@@ -1003,6 +1018,7 @@ class MacRecorder extends EventEmitter {
|
|
|
1003
1018
|
cameraOutputPath: this.cameraCaptureFile || null,
|
|
1004
1019
|
audioOutputPath: this.audioCaptureFile || null,
|
|
1005
1020
|
cursorOutputPath: cursorFilePath,
|
|
1021
|
+
keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
|
|
1006
1022
|
sessionTimestamp: fileTimestampPayload,
|
|
1007
1023
|
syncTimestamp: startTimestampPayload,
|
|
1008
1024
|
fileTimestamp: fileTimestampPayload,
|
|
@@ -1026,6 +1042,7 @@ class MacRecorder extends EventEmitter {
|
|
|
1026
1042
|
cameraOutputPath: this.cameraCaptureFile || null,
|
|
1027
1043
|
audioOutputPath: this.audioCaptureFile || null,
|
|
1028
1044
|
cursorOutputPath: cursorFilePath,
|
|
1045
|
+
keyboardOutputPath: this.keyboardCaptureFile || keyboardFilePath,
|
|
1029
1046
|
sessionTimestamp: fileTimestampPayload,
|
|
1030
1047
|
syncTimestamp: startTimestampPayload,
|
|
1031
1048
|
fileTimestamp: fileTimestampPayload,
|
|
@@ -1107,6 +1124,17 @@ class MacRecorder extends EventEmitter {
|
|
|
1107
1124
|
}
|
|
1108
1125
|
}
|
|
1109
1126
|
|
|
1127
|
+
// Klavye kısayolu yakalamayı durdur (instant)
|
|
1128
|
+
if (this.keyboardCaptureActive) {
|
|
1129
|
+
try {
|
|
1130
|
+
console.log('🛑 SYNC: Stopping keyboard shortcut capture');
|
|
1131
|
+
await this.stopKeyboardCapture();
|
|
1132
|
+
console.log('✅ SYNC: Keyboard shortcut capture stopped');
|
|
1133
|
+
} catch (keyboardError) {
|
|
1134
|
+
console.warn('⚠️ Keyboard capture failed to stop:', keyboardError.message);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1110
1138
|
let success = false;
|
|
1111
1139
|
|
|
1112
1140
|
// 2. Stop native screen recording
|
|
@@ -1701,6 +1729,66 @@ class MacRecorder extends EventEmitter {
|
|
|
1701
1729
|
});
|
|
1702
1730
|
}
|
|
1703
1731
|
|
|
1732
|
+
/**
|
|
1733
|
+
* Klavye kısayolu (shortcut) yakalamayı başlatır.
|
|
1734
|
+
* Native CGEventTap keyDown olaylarını dinler; yalnızca ⌘/⌃/⌥ modifier'lı
|
|
1735
|
+
* basımları zaman damgalı JSON olarak dosyaya yazar (gizlilik: düz yazım kaydedilmez).
|
|
1736
|
+
* @param {string} filepath - Keyboard data JSON dosya yolu
|
|
1737
|
+
* @param {Object} options
|
|
1738
|
+
* @param {number} options.startTimestamp - Cursor/video ile hizalamak için başlangıç zamanı (ms)
|
|
1739
|
+
*/
|
|
1740
|
+
async startKeyboardCapture(filepath, options = {}) {
|
|
1741
|
+
if (typeof filepath !== "string" || !filepath) {
|
|
1742
|
+
throw new Error("Keyboard capture filepath (string) required");
|
|
1743
|
+
}
|
|
1744
|
+
if (
|
|
1745
|
+
!nativeBinding ||
|
|
1746
|
+
typeof nativeBinding.startKeyboardTracking !== "function"
|
|
1747
|
+
) {
|
|
1748
|
+
// Eski native binary — sessizce atla
|
|
1749
|
+
throw new Error("Native keyboard tracking not available in this build");
|
|
1750
|
+
}
|
|
1751
|
+
if (this.keyboardCaptureActive) {
|
|
1752
|
+
throw new Error("Keyboard capture is already running");
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const startTimestamp = Number(options.startTimestamp) || Date.now();
|
|
1756
|
+
const started = nativeBinding.startKeyboardTracking(filepath, startTimestamp);
|
|
1757
|
+
if (!started) {
|
|
1758
|
+
throw new Error(
|
|
1759
|
+
"Failed to start keyboard tracking (Accessibility permission may be required)"
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
this.keyboardCaptureFile = filepath;
|
|
1764
|
+
this.keyboardCaptureActive = true;
|
|
1765
|
+
this.emit("keyboardCaptureStarted", filepath);
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* Klavye kısayolu yakalamayı durdurur ve JSON dosyasını kapatır.
|
|
1771
|
+
*/
|
|
1772
|
+
async stopKeyboardCapture() {
|
|
1773
|
+
if (
|
|
1774
|
+
!nativeBinding ||
|
|
1775
|
+
typeof nativeBinding.stopKeyboardTracking !== "function"
|
|
1776
|
+
) {
|
|
1777
|
+
this.keyboardCaptureActive = false;
|
|
1778
|
+
return false;
|
|
1779
|
+
}
|
|
1780
|
+
if (!this.keyboardCaptureActive) {
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
try {
|
|
1784
|
+
nativeBinding.stopKeyboardTracking();
|
|
1785
|
+
} finally {
|
|
1786
|
+
this.keyboardCaptureActive = false;
|
|
1787
|
+
this.emit("keyboardCaptureStopped");
|
|
1788
|
+
}
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1704
1792
|
/**
|
|
1705
1793
|
* Anlık cursor pozisyonunu ve tipini döndürür
|
|
1706
1794
|
* Display-relative koordinatlar döner (her zaman pozitif)
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/mac_recorder.mm
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|