node-mac-recorder 2.22.10 → 2.22.12
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/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -814,6 +814,10 @@ static bool g_leftMouseDown = false;
|
|
|
814
814
|
static bool g_rightMouseDown = false;
|
|
815
815
|
static NSString *g_lastEventType = @"move";
|
|
816
816
|
|
|
817
|
+
// Text input (keyboard) tracking state
|
|
818
|
+
static NSTimeInterval g_lastTextInputEmitTime = 0; // Throttle: son textInput event zamanı
|
|
819
|
+
static const NSTimeInterval TEXT_INPUT_THROTTLE_MS = 50; // Min 50ms aralık (20 FPS)
|
|
820
|
+
|
|
817
821
|
// Accessibility tabanlı cursor tip tespiti
|
|
818
822
|
static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
|
|
819
823
|
@autoreleasepool {
|
|
@@ -1942,6 +1946,125 @@ void writeToFile(NSDictionary *cursorData) {
|
|
|
1942
1946
|
}
|
|
1943
1947
|
}
|
|
1944
1948
|
|
|
1949
|
+
// Text input event: Klavye basıldığında focused text field'in caret pozisyonunu yakala
|
|
1950
|
+
static void emitTextInputEvent(NSTimeInterval timestamp, NSTimeInterval unixTimeMs, CGPoint mouseLocation) {
|
|
1951
|
+
// Throttle: Çok sık emit etme (performans için)
|
|
1952
|
+
if (unixTimeMs - g_lastTextInputEmitTime < TEXT_INPUT_THROTTLE_MS) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
@autoreleasepool {
|
|
1957
|
+
@try {
|
|
1958
|
+
AXUIElementRef systemWide = AXUIElementCreateSystemWide();
|
|
1959
|
+
if (!systemWide) return;
|
|
1960
|
+
|
|
1961
|
+
AXUIElementRef focusedElement = NULL;
|
|
1962
|
+
AXError focusErr = AXUIElementCopyAttributeValue(
|
|
1963
|
+
systemWide, kAXFocusedUIElementAttribute, (CFTypeRef *)&focusedElement);
|
|
1964
|
+
|
|
1965
|
+
if (focusErr != kAXErrorSuccess || !focusedElement) {
|
|
1966
|
+
CFRelease(systemWide);
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Focused element text field mi kontrol et
|
|
1971
|
+
NSString *role = CopyAttributeString(focusedElement, kAXRoleAttribute);
|
|
1972
|
+
BOOL isEditable = NO;
|
|
1973
|
+
CopyAttributeBoolean(focusedElement, CFSTR("AXEditable"), &isEditable);
|
|
1974
|
+
|
|
1975
|
+
BOOL isTextField = StringEqualsAny(role, @[
|
|
1976
|
+
@"AXTextField", @"AXTextArea", @"AXTextView",
|
|
1977
|
+
@"AXTextEditor", @"AXSearchField",
|
|
1978
|
+
@"AXComboBox"
|
|
1979
|
+
]) || isEditable;
|
|
1980
|
+
|
|
1981
|
+
if (!isTextField) {
|
|
1982
|
+
CFRelease(focusedElement);
|
|
1983
|
+
CFRelease(systemWide);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// Input frame bilgisini al (AXPosition + AXSize)
|
|
1988
|
+
CGPoint inputOrigin = CGPointZero;
|
|
1989
|
+
CGSize inputSize = CGSizeZero;
|
|
1990
|
+
AXValueRef positionValue = NULL;
|
|
1991
|
+
AXValueRef sizeValue = NULL;
|
|
1992
|
+
|
|
1993
|
+
AXUIElementCopyAttributeValue(focusedElement, kAXPositionAttribute, (CFTypeRef *)&positionValue);
|
|
1994
|
+
AXUIElementCopyAttributeValue(focusedElement, kAXSizeAttribute, (CFTypeRef *)&sizeValue);
|
|
1995
|
+
|
|
1996
|
+
if (positionValue) {
|
|
1997
|
+
AXValueGetValue(positionValue, kAXValueTypeCGPoint, &inputOrigin);
|
|
1998
|
+
CFRelease(positionValue);
|
|
1999
|
+
}
|
|
2000
|
+
if (sizeValue) {
|
|
2001
|
+
AXValueGetValue(sizeValue, kAXValueTypeCGSize, &inputSize);
|
|
2002
|
+
CFRelease(sizeValue);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// Caret pozisyonunu al (AXSelectedTextRange → AXBoundsForRange)
|
|
2006
|
+
CGPoint caretPos = CGPointMake(inputOrigin.x, inputOrigin.y);
|
|
2007
|
+
BOOL hasCaretPos = NO;
|
|
2008
|
+
|
|
2009
|
+
CFTypeRef selectedRangeValue = NULL;
|
|
2010
|
+
AXError rangeErr = AXUIElementCopyAttributeValue(
|
|
2011
|
+
focusedElement, CFSTR("AXSelectedTextRange"), &selectedRangeValue);
|
|
2012
|
+
|
|
2013
|
+
if (rangeErr == kAXErrorSuccess && selectedRangeValue) {
|
|
2014
|
+
CFTypeRef boundsValue = NULL;
|
|
2015
|
+
AXError boundsErr = AXUIElementCopyParameterizedAttributeValue(
|
|
2016
|
+
focusedElement, CFSTR("AXBoundsForRange"),
|
|
2017
|
+
selectedRangeValue, &boundsValue);
|
|
2018
|
+
|
|
2019
|
+
if (boundsErr == kAXErrorSuccess && boundsValue) {
|
|
2020
|
+
CGRect caretBounds = CGRectZero;
|
|
2021
|
+
if (AXValueGetValue((AXValueRef)boundsValue, kAXValueTypeCGRect, &caretBounds)) {
|
|
2022
|
+
// Caret'in dikey ortası
|
|
2023
|
+
caretPos = CGPointMake(
|
|
2024
|
+
caretBounds.origin.x,
|
|
2025
|
+
caretBounds.origin.y + caretBounds.size.height / 2.0
|
|
2026
|
+
);
|
|
2027
|
+
hasCaretPos = YES;
|
|
2028
|
+
}
|
|
2029
|
+
CFRelease(boundsValue);
|
|
2030
|
+
}
|
|
2031
|
+
CFRelease(selectedRangeValue);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Caret alınamazsa input frame'in sol ortasını kullan
|
|
2035
|
+
if (!hasCaretPos) {
|
|
2036
|
+
caretPos = CGPointMake(inputOrigin.x + 4, inputOrigin.y + inputSize.height / 2.0);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// textInput event'i oluştur ve dosyaya yaz
|
|
2040
|
+
NSDictionary *textInputInfo = @{
|
|
2041
|
+
@"x": @((int)mouseLocation.x),
|
|
2042
|
+
@"y": @((int)mouseLocation.y),
|
|
2043
|
+
@"timestamp": @(timestamp),
|
|
2044
|
+
@"unixTimeMs": @(unixTimeMs),
|
|
2045
|
+
@"cursorType": @"text",
|
|
2046
|
+
@"type": @"textInput",
|
|
2047
|
+
@"caretX": @((int)caretPos.x),
|
|
2048
|
+
@"caretY": @((int)caretPos.y),
|
|
2049
|
+
@"inputFrame": @{
|
|
2050
|
+
@"x": @((int)inputOrigin.x),
|
|
2051
|
+
@"y": @((int)inputOrigin.y),
|
|
2052
|
+
@"width": @((int)inputSize.width),
|
|
2053
|
+
@"height": @((int)inputSize.height)
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
|
|
2057
|
+
writeToFile(textInputInfo);
|
|
2058
|
+
g_lastTextInputEmitTime = unixTimeMs;
|
|
2059
|
+
|
|
2060
|
+
CFRelease(focusedElement);
|
|
2061
|
+
CFRelease(systemWide);
|
|
2062
|
+
} @catch (NSException *exception) {
|
|
2063
|
+
// Accessibility hata verirse sessizce devam et
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1945
2068
|
// Event callback for mouse events
|
|
1946
2069
|
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
|
1947
2070
|
@autoreleasepool {
|
|
@@ -1982,6 +2105,10 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
1982
2105
|
case kCGEventOtherMouseDragged:
|
|
1983
2106
|
eventType = @"drag";
|
|
1984
2107
|
break;
|
|
2108
|
+
case kCGEventKeyDown:
|
|
2109
|
+
// Klavye event'i — text caret tracking için
|
|
2110
|
+
emitTextInputEvent(timestamp, unixTimeMs, location);
|
|
2111
|
+
return event; // Mouse event olarak işleme, ayrı handle edildi
|
|
1985
2112
|
case kCGEventMouseMoved:
|
|
1986
2113
|
default:
|
|
1987
2114
|
eventType = @"move";
|
|
@@ -1991,7 +2118,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
1991
2118
|
if (!ShouldEmitCursorEvent(location, cursorType, eventType)) {
|
|
1992
2119
|
return event;
|
|
1993
2120
|
}
|
|
1994
|
-
|
|
2121
|
+
|
|
1995
2122
|
// Cursor data oluştur
|
|
1996
2123
|
NSDictionary *cursorInfo = @{
|
|
1997
2124
|
@"x": @((int)location.x),
|
|
@@ -2001,7 +2128,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
2001
2128
|
@"cursorType": cursorType,
|
|
2002
2129
|
@"type": eventType
|
|
2003
2130
|
};
|
|
2004
|
-
|
|
2131
|
+
|
|
2005
2132
|
// Direkt dosyaya yaz
|
|
2006
2133
|
writeToFile(cursorInfo);
|
|
2007
2134
|
RememberCursorEvent(location, cursorType, eventType);
|
|
@@ -2140,6 +2267,7 @@ void cleanupCursorTracking() {
|
|
|
2140
2267
|
g_lastDetectedCursorType = nil;
|
|
2141
2268
|
g_cursorTypeCounter = 0;
|
|
2142
2269
|
g_isFirstWrite = true;
|
|
2270
|
+
g_lastTextInputEmitTime = 0;
|
|
2143
2271
|
ResetCursorEventHistory();
|
|
2144
2272
|
}
|
|
2145
2273
|
|
|
@@ -2180,7 +2308,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
2180
2308
|
g_trackingStartTime = [NSDate date];
|
|
2181
2309
|
ResetCursorEventHistory();
|
|
2182
2310
|
|
|
2183
|
-
// Create event tap for mouse events
|
|
2311
|
+
// Create event tap for mouse + keyboard events
|
|
2184
2312
|
CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
|
|
2185
2313
|
CGEventMaskBit(kCGEventLeftMouseUp) |
|
|
2186
2314
|
CGEventMaskBit(kCGEventRightMouseDown) |
|
|
@@ -2190,7 +2318,8 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
2190
2318
|
CGEventMaskBit(kCGEventMouseMoved) |
|
|
2191
2319
|
CGEventMaskBit(kCGEventLeftMouseDragged) |
|
|
2192
2320
|
CGEventMaskBit(kCGEventRightMouseDragged) |
|
|
2193
|
-
CGEventMaskBit(kCGEventOtherMouseDragged)
|
|
2321
|
+
CGEventMaskBit(kCGEventOtherMouseDragged) |
|
|
2322
|
+
CGEventMaskBit(kCGEventKeyDown));
|
|
2194
2323
|
|
|
2195
2324
|
bool eventTapActive = false;
|
|
2196
2325
|
g_eventTap = CGEventTapCreate(kCGSessionEventTap,
|
|
@@ -50,6 +50,30 @@ static void initializeSafeQueue() {
|
|
|
50
50
|
initializeSafeQueue();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
+ (BOOL)shouldAllowElectronWindows {
|
|
54
|
+
NSString *flag = [[[NSProcessInfo processInfo] environment] objectForKey:@"CREAVIT_ALLOW_ELECTRON_WINDOWS"];
|
|
55
|
+
if (!flag) return NO;
|
|
56
|
+
|
|
57
|
+
NSString *normalized = [[flag lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
58
|
+
return [normalized isEqualToString:@"1"] ||
|
|
59
|
+
[normalized isEqualToString:@"true"] ||
|
|
60
|
+
[normalized isEqualToString:@"yes"] ||
|
|
61
|
+
[normalized isEqualToString:@"on"];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
+ (BOOL)shouldSkipWindowOwner:(NSString *)appName {
|
|
65
|
+
if (!appName || appName.length == 0) return YES;
|
|
66
|
+
if ([appName containsString:@"WindowServer"] || [appName containsString:@"Dock"]) return YES;
|
|
67
|
+
|
|
68
|
+
if (![self shouldAllowElectronWindows]) {
|
|
69
|
+
if ([appName containsString:@"Electron"] || [appName containsString:@"node"]) {
|
|
70
|
+
return YES;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return NO;
|
|
75
|
+
}
|
|
76
|
+
|
|
53
77
|
+ (BOOL)startRecordingWithPath:(NSString *)outputPath options:(NSDictionary *)options {
|
|
54
78
|
if (@available(macOS 12.3, *)) {
|
|
55
79
|
return [self startRecordingModern:outputPath options:options];
|
|
@@ -432,8 +456,7 @@ static void initializeSafeQueue() {
|
|
|
432
456
|
|
|
433
457
|
NSString *appName = window.owningApplication.applicationName ?: @"Unknown";
|
|
434
458
|
|
|
435
|
-
|
|
436
|
-
if ([appName containsString:@"Electron"] || [appName containsString:@"node"]) continue;
|
|
459
|
+
if ([self shouldSkipWindowOwner:appName]) continue;
|
|
437
460
|
|
|
438
461
|
NSDictionary *windowInfo = @{
|
|
439
462
|
@"id": @(window.windowID),
|
|
@@ -12,6 +12,30 @@ static void initializeWindowQueue() {
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
static BOOL ShouldAllowElectronWindows(void) {
|
|
16
|
+
NSString *flag = [[[NSProcessInfo processInfo] environment] objectForKey:@"CREAVIT_ALLOW_ELECTRON_WINDOWS"];
|
|
17
|
+
if (!flag) return NO;
|
|
18
|
+
|
|
19
|
+
NSString *normalized = [[flag lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
20
|
+
return [normalized isEqualToString:@"1"] ||
|
|
21
|
+
[normalized isEqualToString:@"true"] ||
|
|
22
|
+
[normalized isEqualToString:@"yes"] ||
|
|
23
|
+
[normalized isEqualToString:@"on"];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static BOOL ShouldSkipWindowOwner(NSString *appName) {
|
|
27
|
+
if (!appName || appName.length == 0) return YES;
|
|
28
|
+
if ([appName containsString:@"WindowServer"] || [appName containsString:@"Dock"]) return YES;
|
|
29
|
+
|
|
30
|
+
if (!ShouldAllowElectronWindows()) {
|
|
31
|
+
if ([appName containsString:@"Electron"] || [appName containsString:@"node"]) {
|
|
32
|
+
return YES;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return NO;
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
// NAPI Function: Get Windows (Electron-safe)
|
|
16
40
|
Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
|
|
17
41
|
Napi::Env env = info.Env();
|
|
@@ -38,11 +62,7 @@ Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
|
|
|
38
62
|
|
|
39
63
|
NSString *appName = window.owningApplication.applicationName ?: @"Unknown";
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
if ([appName containsString:@"Electron"] ||
|
|
43
|
-
[appName containsString:@"node"] ||
|
|
44
|
-
[appName containsString:@"WindowServer"] ||
|
|
45
|
-
[appName containsString:@"Dock"]) continue;
|
|
65
|
+
if (ShouldSkipWindowOwner(appName)) continue;
|
|
46
66
|
|
|
47
67
|
NSDictionary *windowInfo = @{
|
|
48
68
|
@"id": @(window.windowID),
|
|
@@ -94,11 +114,7 @@ Napi::Value GetWindowsElectronSafe(const Napi::CallbackInfo& info) {
|
|
|
94
114
|
NSString *appName = (__bridge NSString*)ownerName;
|
|
95
115
|
NSString *windowTitle = windowName ? (__bridge NSString*)windowName : @"";
|
|
96
116
|
|
|
97
|
-
|
|
98
|
-
if ([appName containsString:@"Electron"] ||
|
|
99
|
-
[appName containsString:@"node"] ||
|
|
100
|
-
[appName containsString:@"WindowServer"] ||
|
|
101
|
-
[appName containsString:@"Dock"]) continue;
|
|
117
|
+
if (ShouldSkipWindowOwner(appName)) continue;
|
|
102
118
|
|
|
103
119
|
// Get window bounds
|
|
104
120
|
CGRect bounds = CGRectZero;
|
package/src/window_selector.mm
CHANGED
|
@@ -56,6 +56,31 @@ static id g_screenKeyEventMonitor = nil;
|
|
|
56
56
|
static NSTimer *g_screenTrackingTimer = nil;
|
|
57
57
|
static NSInteger g_currentActiveScreenIndex = -1;
|
|
58
58
|
|
|
59
|
+
static bool shouldAllowElectronWindows() {
|
|
60
|
+
NSString *flag = [[[NSProcessInfo processInfo] environment] objectForKey:@"CREAVIT_ALLOW_ELECTRON_WINDOWS"];
|
|
61
|
+
if (!flag) return false;
|
|
62
|
+
|
|
63
|
+
NSString *normalized = [[flag lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
64
|
+
return [normalized isEqualToString:@"1"] ||
|
|
65
|
+
[normalized isEqualToString:@"true"] ||
|
|
66
|
+
[normalized isEqualToString:@"yes"] ||
|
|
67
|
+
[normalized isEqualToString:@"on"];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static bool shouldSkipSelectableWindowOwner(NSString *windowOwner) {
|
|
71
|
+
if (!windowOwner || [windowOwner length] == 0) return true;
|
|
72
|
+
if ([windowOwner isEqualToString:@"WindowServer"]) return true;
|
|
73
|
+
if ([windowOwner isEqualToString:@"Dock"]) return true;
|
|
74
|
+
|
|
75
|
+
if (!shouldAllowElectronWindows()) {
|
|
76
|
+
if ([windowOwner containsString:@"Electron"] || [windowOwner containsString:@"node"]) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
59
84
|
// Record icon helpers
|
|
60
85
|
static NSImage *CreateRecordIconImage(CGFloat size) {
|
|
61
86
|
const CGFloat leadingInset = 24.0;
|
|
@@ -757,12 +782,7 @@ NSArray* getAllSelectableWindows() {
|
|
|
757
782
|
|
|
758
783
|
// Skip system windows, dock, menu bar, etc.
|
|
759
784
|
if ([windowLayer intValue] != 0) continue; // Only normal windows
|
|
760
|
-
if (
|
|
761
|
-
if ([windowOwner isEqualToString:@"WindowServer"]) continue;
|
|
762
|
-
if ([windowOwner isEqualToString:@"Dock"]) continue;
|
|
763
|
-
|
|
764
|
-
// Skip Electron windows (our own overlay)
|
|
765
|
-
if ([windowOwner containsString:@"Electron"] || [windowOwner containsString:@"node"]) continue;
|
|
785
|
+
if (shouldSkipSelectableWindowOwner(windowOwner)) continue;
|
|
766
786
|
|
|
767
787
|
// Extract bounds
|
|
768
788
|
int x = [[bounds objectForKey:@"X"] intValue];
|
|
@@ -802,8 +822,7 @@ NSDictionary* getWindowUnderCursor(CGPoint point) {
|
|
|
802
822
|
for (NSDictionary *window in g_allWindows) {
|
|
803
823
|
NSString *appName = [window objectForKey:@"appName"];
|
|
804
824
|
|
|
805
|
-
|
|
806
|
-
if (appName && ([appName containsString:@"Electron"] || [appName containsString:@"node"])) {
|
|
825
|
+
if (shouldSkipSelectableWindowOwner(appName)) {
|
|
807
826
|
continue;
|
|
808
827
|
}
|
|
809
828
|
|