node-mac-recorder 2.22.11 → 2.22.13
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 +1 -1
- package/src/cursor_tracker.mm +140 -4
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);
|
|
@@ -2037,6 +2164,13 @@ void cursorTimerCallback() {
|
|
|
2037
2164
|
cursorType = @"default";
|
|
2038
2165
|
}
|
|
2039
2166
|
|
|
2167
|
+
// Timer-only mod: CGEventTap yokken kCGEventKeyDown gelmez; caret satırları hiç yazılmazdı.
|
|
2168
|
+
// I-beam görünürken AX ile periyodik textInput üret (emitTextInputEvent içinde throttle var).
|
|
2169
|
+
if ([cursorType isEqualToString:@"text"] ||
|
|
2170
|
+
[cursorType isEqualToString:@"vertical-text"]) {
|
|
2171
|
+
emitTextInputEvent(timestamp, unixTimeMs, location);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2040
2174
|
// Mouse button state polling — event tap olmadığında click/drag tespiti
|
|
2041
2175
|
bool currentLeftMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonLeft);
|
|
2042
2176
|
bool currentRightMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonRight);
|
|
@@ -2140,6 +2274,7 @@ void cleanupCursorTracking() {
|
|
|
2140
2274
|
g_lastDetectedCursorType = nil;
|
|
2141
2275
|
g_cursorTypeCounter = 0;
|
|
2142
2276
|
g_isFirstWrite = true;
|
|
2277
|
+
g_lastTextInputEmitTime = 0;
|
|
2143
2278
|
ResetCursorEventHistory();
|
|
2144
2279
|
}
|
|
2145
2280
|
|
|
@@ -2180,7 +2315,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
2180
2315
|
g_trackingStartTime = [NSDate date];
|
|
2181
2316
|
ResetCursorEventHistory();
|
|
2182
2317
|
|
|
2183
|
-
// Create event tap for mouse events
|
|
2318
|
+
// Create event tap for mouse + keyboard events
|
|
2184
2319
|
CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
|
|
2185
2320
|
CGEventMaskBit(kCGEventLeftMouseUp) |
|
|
2186
2321
|
CGEventMaskBit(kCGEventRightMouseDown) |
|
|
@@ -2190,7 +2325,8 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
2190
2325
|
CGEventMaskBit(kCGEventMouseMoved) |
|
|
2191
2326
|
CGEventMaskBit(kCGEventLeftMouseDragged) |
|
|
2192
2327
|
CGEventMaskBit(kCGEventRightMouseDragged) |
|
|
2193
|
-
CGEventMaskBit(kCGEventOtherMouseDragged)
|
|
2328
|
+
CGEventMaskBit(kCGEventOtherMouseDragged) |
|
|
2329
|
+
CGEventMaskBit(kCGEventKeyDown));
|
|
2194
2330
|
|
|
2195
2331
|
bool eventTapActive = false;
|
|
2196
2332
|
g_eventTap = CGEventTapCreate(kCGSessionEventTap,
|