node-mac-recorder 2.22.24 → 2.22.32
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 +0 -1
- package/electron-safe-binding.gyp +0 -1
- package/electron-safe-index.js +104 -196
- package/index.js +355 -8
- package/package.json +1 -1
- package/src/avfoundation_recorder.mm +1 -2
- package/src/cursor_tracker.mm +4 -90
- package/src/electron_safe/cursor_tracker_electron.mm +0 -34
- package/src/electron_safe/window_selector_electron.mm +5 -2
- package/src/screen_capture_kit.mm +11 -17
- package/src/window_selector.mm +5 -2
- package/lib/cursorCapture/displayInfo.js +0 -110
- package/lib/cursorCapture/polling.js +0 -585
- package/src/text_input_ax_snapshot.h +0 -3
- package/src/text_input_ax_snapshot.mm +0 -161
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const { EventEmitter } = require("events");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
-
const cursorCapturePolling = require("./lib/cursorCapture/polling");
|
|
5
4
|
|
|
6
5
|
// Auto-switch to Electron-safe implementation when running under Electron and binary exists
|
|
7
6
|
let USE_ELECTRON_SAFE = false;
|
|
@@ -1341,17 +1340,365 @@ class MacRecorder extends EventEmitter {
|
|
|
1341
1340
|
});
|
|
1342
1341
|
}
|
|
1343
1342
|
|
|
1343
|
+
/**
|
|
1344
|
+
* Event'in kaydedilip kaydedilmeyeceğini belirler
|
|
1345
|
+
*/
|
|
1346
|
+
shouldCaptureEvent(currentData) {
|
|
1347
|
+
if (!this.lastCapturedData) {
|
|
1348
|
+
return true; // İlk event
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const last = this.lastCapturedData;
|
|
1352
|
+
|
|
1353
|
+
// Event type değişmişse
|
|
1354
|
+
if (currentData.type !== last.type) {
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Pozisyon değişmişse (minimum 2 pixel tolerans)
|
|
1359
|
+
if (
|
|
1360
|
+
Math.abs(currentData.x - last.x) >= 2 ||
|
|
1361
|
+
Math.abs(currentData.y - last.y) >= 2
|
|
1362
|
+
) {
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Cursor type değişmişse
|
|
1367
|
+
if (currentData.cursorType !== last.cursorType) {
|
|
1368
|
+
return true;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Hiçbir değişiklik yoksa kaydetme
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Unified cursor capture for all recording types - uses video-relative coordinates
|
|
1377
|
+
* @param {string|number} intervalOrFilepath - Cursor data JSON dosya yolu veya interval
|
|
1378
|
+
* @param {Object} options - Cursor capture seçenekleri
|
|
1379
|
+
* @param {boolean} options.videoRelative - Use video-relative coordinates (recommended)
|
|
1380
|
+
* @param {Object} options.displayInfo - Display information for coordinate transformation
|
|
1381
|
+
* @param {string} options.recordingType - Type of recording: 'display', 'window', 'area'
|
|
1382
|
+
* @param {Object} options.captureArea - Capture area for area recording coordinate transformation
|
|
1383
|
+
* @param {number} options.windowId - Window ID for window recording coordinate transformation
|
|
1384
|
+
* @param {number} options.startTimestamp - Pre-defined start timestamp for synchronization (optional)
|
|
1385
|
+
*/
|
|
1344
1386
|
async startCursorCapture(intervalOrFilepath = 100, options = {}) {
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1387
|
+
let filepath;
|
|
1388
|
+
let interval = 20; // Default 50 FPS
|
|
1389
|
+
|
|
1390
|
+
// Parameter parsing: number = interval, string = filepath
|
|
1391
|
+
if (typeof intervalOrFilepath === "number") {
|
|
1392
|
+
interval = Math.max(10, intervalOrFilepath); // Min 10ms
|
|
1393
|
+
filepath = `cursor-data-${Date.now()}.json`;
|
|
1394
|
+
} else if (typeof intervalOrFilepath === "string") {
|
|
1395
|
+
filepath = intervalOrFilepath;
|
|
1396
|
+
} else {
|
|
1397
|
+
throw new Error(
|
|
1398
|
+
"Parameter must be interval (number) or filepath (string)"
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (this.cursorCaptureInterval) {
|
|
1403
|
+
throw new Error("Cursor capture is already running");
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// SYNC FIX: Use pre-defined timestamp if provided for synchronization
|
|
1407
|
+
const syncStartTime = options.startTimestamp || Date.now();
|
|
1408
|
+
|
|
1409
|
+
// Fetch window bounds for multi-window recording
|
|
1410
|
+
if (options.multiWindowBounds && options.multiWindowBounds.length > 0) {
|
|
1411
|
+
try {
|
|
1412
|
+
const allWindows = await this.getWindows();
|
|
1413
|
+
// Match window IDs and populate bounds
|
|
1414
|
+
for (const windowInfo of options.multiWindowBounds) {
|
|
1415
|
+
const windowData = allWindows.find(w => w.id === windowInfo.windowId);
|
|
1416
|
+
if (windowData) {
|
|
1417
|
+
windowInfo.bounds = {
|
|
1418
|
+
x: windowData.x || 0,
|
|
1419
|
+
y: windowData.y || 0,
|
|
1420
|
+
width: windowData.width || 0,
|
|
1421
|
+
height: windowData.height || 0
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
console.warn('Failed to fetch window bounds for multi-window cursor tracking:', error.message);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Use video-relative coordinate system for all recording types
|
|
1431
|
+
if (options.videoRelative && options.displayInfo) {
|
|
1432
|
+
// Calculate video offset based on recording type
|
|
1433
|
+
let videoOffsetX = 0;
|
|
1434
|
+
let videoOffsetY = 0;
|
|
1435
|
+
let videoWidth = options.displayInfo.width || options.displayInfo.logicalWidth;
|
|
1436
|
+
let videoHeight = options.displayInfo.height || options.displayInfo.logicalHeight;
|
|
1437
|
+
|
|
1438
|
+
if (options.recordingType === 'window' && options.windowId) {
|
|
1439
|
+
// For window recording: offset = window position in display
|
|
1440
|
+
if (options.captureArea) {
|
|
1441
|
+
videoOffsetX = options.captureArea.x;
|
|
1442
|
+
videoOffsetY = options.captureArea.y;
|
|
1443
|
+
videoWidth = options.captureArea.width;
|
|
1444
|
+
videoHeight = options.captureArea.height;
|
|
1445
|
+
}
|
|
1446
|
+
} else if (options.recordingType === 'area' && options.captureArea) {
|
|
1447
|
+
// For area recording: offset = area position in display
|
|
1448
|
+
videoOffsetX = options.captureArea.x;
|
|
1449
|
+
videoOffsetY = options.captureArea.y;
|
|
1450
|
+
videoWidth = options.captureArea.width;
|
|
1451
|
+
videoHeight = options.captureArea.height;
|
|
1452
|
+
}
|
|
1453
|
+
// For display recording: offset remains 0,0
|
|
1454
|
+
|
|
1455
|
+
this.cursorDisplayInfo = {
|
|
1456
|
+
displayId: options.displayInfo.displayId || options.displayInfo.id,
|
|
1457
|
+
displayX: options.displayInfo.x || 0,
|
|
1458
|
+
displayY: options.displayInfo.y || 0,
|
|
1459
|
+
displayWidth: options.displayInfo.width || options.displayInfo.logicalWidth,
|
|
1460
|
+
displayHeight: options.displayInfo.height || options.displayInfo.logicalHeight,
|
|
1461
|
+
videoOffsetX: videoOffsetX,
|
|
1462
|
+
videoOffsetY: videoOffsetY,
|
|
1463
|
+
videoWidth: videoWidth,
|
|
1464
|
+
videoHeight: videoHeight,
|
|
1465
|
+
videoRelative: true,
|
|
1466
|
+
recordingType: options.recordingType || 'display',
|
|
1467
|
+
// Store additional context for debugging
|
|
1468
|
+
captureArea: options.captureArea,
|
|
1469
|
+
windowId: options.windowId,
|
|
1470
|
+
// Multi-window bounds for location detection
|
|
1471
|
+
multiWindowBounds: options.multiWindowBounds || null
|
|
1472
|
+
};
|
|
1473
|
+
} else if (this.recordingDisplayInfo) {
|
|
1474
|
+
// Fallback: Use recording display info if available
|
|
1475
|
+
this.cursorDisplayInfo = {
|
|
1476
|
+
...this.recordingDisplayInfo,
|
|
1477
|
+
displayX: this.recordingDisplayInfo.x || 0,
|
|
1478
|
+
displayY: this.recordingDisplayInfo.y || 0,
|
|
1479
|
+
displayWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
|
|
1480
|
+
displayHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
|
|
1481
|
+
videoOffsetX: 0,
|
|
1482
|
+
videoOffsetY: 0,
|
|
1483
|
+
videoWidth: this.recordingDisplayInfo.width || this.recordingDisplayInfo.logicalWidth,
|
|
1484
|
+
videoHeight: this.recordingDisplayInfo.height || this.recordingDisplayInfo.logicalHeight,
|
|
1485
|
+
videoRelative: true,
|
|
1486
|
+
recordingType: options.recordingType || 'display',
|
|
1487
|
+
multiWindowBounds: options.multiWindowBounds || null
|
|
1488
|
+
};
|
|
1489
|
+
} else {
|
|
1490
|
+
// Final fallback: Main display global coordinates
|
|
1491
|
+
try {
|
|
1492
|
+
const displays = await this.getDisplays();
|
|
1493
|
+
const mainDisplay = displays.find((d) => d.isPrimary) || displays[0];
|
|
1494
|
+
if (mainDisplay) {
|
|
1495
|
+
this.cursorDisplayInfo = {
|
|
1496
|
+
displayId: mainDisplay.id,
|
|
1497
|
+
x: mainDisplay.x,
|
|
1498
|
+
y: mainDisplay.y,
|
|
1499
|
+
width: parseInt(mainDisplay.resolution.split("x")[0]),
|
|
1500
|
+
height: parseInt(mainDisplay.resolution.split("x")[1]),
|
|
1501
|
+
multiWindowBounds: options.multiWindowBounds || null
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
console.warn("Main display bilgisi alınamadı:", error.message);
|
|
1506
|
+
this.cursorDisplayInfo = null; // Fallback: global koordinatlar
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
return new Promise((resolve, reject) => {
|
|
1511
|
+
try {
|
|
1512
|
+
// Dosyayı oluştur ve temizle
|
|
1513
|
+
const fs = require("fs");
|
|
1514
|
+
fs.writeFileSync(filepath, "[");
|
|
1515
|
+
|
|
1516
|
+
this.cursorCaptureFile = filepath;
|
|
1517
|
+
// SYNC FIX: Use synchronized start time for accurate timestamp calculation
|
|
1518
|
+
this.cursorCaptureStartTime = syncStartTime;
|
|
1519
|
+
this.cursorCaptureFirstWrite = true;
|
|
1520
|
+
this.lastCapturedData = null;
|
|
1521
|
+
// Store session timestamp for sync metadata
|
|
1522
|
+
this.cursorCaptureSessionTimestamp = this.sessionTimestamp;
|
|
1523
|
+
|
|
1524
|
+
// JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak için)
|
|
1525
|
+
this.cursorCaptureInterval = setInterval(() => {
|
|
1526
|
+
try {
|
|
1527
|
+
const position = nativeBinding.getCursorPosition();
|
|
1528
|
+
const timestamp = Date.now() - this.cursorCaptureStartTime;
|
|
1529
|
+
|
|
1530
|
+
// Video-relative coordinate transformation for all recording types
|
|
1531
|
+
let x = position.x;
|
|
1532
|
+
let y = position.y;
|
|
1533
|
+
let coordinateSystem = "global";
|
|
1534
|
+
|
|
1535
|
+
// Apply video-relative transformation for all recording types
|
|
1536
|
+
if (this.cursorDisplayInfo && this.cursorDisplayInfo.videoRelative) {
|
|
1537
|
+
// Step 1: Transform global → display-relative coordinates
|
|
1538
|
+
const displayRelativeX = position.x - this.cursorDisplayInfo.displayX;
|
|
1539
|
+
const displayRelativeY = position.y - this.cursorDisplayInfo.displayY;
|
|
1540
|
+
|
|
1541
|
+
// Step 2: Transform display-relative → video-relative coordinates
|
|
1542
|
+
x = displayRelativeX - this.cursorDisplayInfo.videoOffsetX;
|
|
1543
|
+
y = displayRelativeY - this.cursorDisplayInfo.videoOffsetY;
|
|
1544
|
+
coordinateSystem = "video-relative";
|
|
1545
|
+
|
|
1546
|
+
// Bounds check for video area (don't skip, just note if outside)
|
|
1547
|
+
const outsideVideo = x < 0 || y < 0 ||
|
|
1548
|
+
x >= this.cursorDisplayInfo.videoWidth ||
|
|
1549
|
+
y >= this.cursorDisplayInfo.videoHeight;
|
|
1550
|
+
|
|
1551
|
+
// For debugging - add metadata if cursor is outside video area
|
|
1552
|
+
if (outsideVideo) {
|
|
1553
|
+
coordinateSystem = "video-relative-outside";
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const cursorData = {
|
|
1558
|
+
x: x,
|
|
1559
|
+
y: y,
|
|
1560
|
+
timestamp: timestamp,
|
|
1561
|
+
unixTimeMs: Date.now(),
|
|
1562
|
+
cursorType: position.cursorType,
|
|
1563
|
+
type: position.eventType || "move",
|
|
1564
|
+
coordinateSystem: coordinateSystem,
|
|
1565
|
+
// Video-relative metadata for all recording types
|
|
1566
|
+
recordingType: this.cursorDisplayInfo?.recordingType || "display",
|
|
1567
|
+
videoInfo: this.cursorDisplayInfo ? {
|
|
1568
|
+
width: this.cursorDisplayInfo.videoWidth,
|
|
1569
|
+
height: this.cursorDisplayInfo.videoHeight,
|
|
1570
|
+
offsetX: this.cursorDisplayInfo.videoOffsetX,
|
|
1571
|
+
offsetY: this.cursorDisplayInfo.videoOffsetY
|
|
1572
|
+
} : {},
|
|
1573
|
+
displayInfo: this.cursorDisplayInfo ? {
|
|
1574
|
+
displayId: this.cursorDisplayInfo.displayId,
|
|
1575
|
+
width: this.cursorDisplayInfo.displayWidth,
|
|
1576
|
+
height: this.cursorDisplayInfo.displayHeight
|
|
1577
|
+
} : {}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// Multi-window location detection with window-relative coordinates
|
|
1581
|
+
if (this.cursorDisplayInfo?.multiWindowBounds && this.cursorDisplayInfo.multiWindowBounds.length > 0) {
|
|
1582
|
+
const location = { hover: null, click: null };
|
|
1583
|
+
let windowRelativeCoords = null;
|
|
1584
|
+
|
|
1585
|
+
// Detect which window the cursor is over
|
|
1586
|
+
for (const windowInfo of this.cursorDisplayInfo.multiWindowBounds) {
|
|
1587
|
+
if (windowInfo.bounds) {
|
|
1588
|
+
const { x: wx, y: wy, width: ww, height: wh } = windowInfo.bounds;
|
|
1589
|
+
// Check if cursor is inside window bounds (using global coordinates)
|
|
1590
|
+
if (position.x >= wx && position.x <= wx + ww &&
|
|
1591
|
+
position.y >= wy && position.y <= wy + wh) {
|
|
1592
|
+
location.hover = windowInfo.windowId;
|
|
1593
|
+
|
|
1594
|
+
// Calculate window-relative coordinates
|
|
1595
|
+
// These coords are relative to the window's top-left corner (0,0)
|
|
1596
|
+
// This allows the desktop app to position cursor correctly
|
|
1597
|
+
// regardless of where the window is placed on canvas
|
|
1598
|
+
windowRelativeCoords = {
|
|
1599
|
+
windowId: windowInfo.windowId,
|
|
1600
|
+
x: position.x - wx,
|
|
1601
|
+
y: position.y - wy,
|
|
1602
|
+
// Also include window dimensions for reference
|
|
1603
|
+
windowWidth: ww,
|
|
1604
|
+
windowHeight: wh
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
// If this is a click/drag event, mark click location
|
|
1608
|
+
// Native eventType values: 'mousedown', 'mouseup', 'drag', 'rightmousedown', 'rightmouseup', 'rightdrag'
|
|
1609
|
+
const eventType = position.eventType || '';
|
|
1610
|
+
if (eventType === 'mousedown' ||
|
|
1611
|
+
eventType === 'mouseup' ||
|
|
1612
|
+
eventType === 'drag' ||
|
|
1613
|
+
eventType === 'rightmousedown' ||
|
|
1614
|
+
eventType === 'rightmouseup' ||
|
|
1615
|
+
eventType === 'rightdrag') {
|
|
1616
|
+
location.click = windowInfo.windowId;
|
|
1617
|
+
}
|
|
1618
|
+
break; // Found the window, stop searching
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Add location info to cursor data
|
|
1624
|
+
cursorData.location = location;
|
|
1625
|
+
|
|
1626
|
+
// Add window-relative coordinates if cursor is over a window
|
|
1627
|
+
if (windowRelativeCoords) {
|
|
1628
|
+
cursorData.windowRelative = windowRelativeCoords;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Add sync metadata to first event only
|
|
1633
|
+
if (this.cursorCaptureFirstWrite && this.cursorCaptureSessionTimestamp) {
|
|
1634
|
+
cursorData._syncMetadata = {
|
|
1635
|
+
videoStartTime: this.cursorCaptureSessionTimestamp,
|
|
1636
|
+
cursorStartTime: this.cursorCaptureStartTime,
|
|
1637
|
+
offsetMs: this.cursorCaptureStartTime - this.cursorCaptureSessionTimestamp
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
|
|
1642
|
+
if (this.shouldCaptureEvent(cursorData)) {
|
|
1643
|
+
// Dosyaya ekle
|
|
1644
|
+
const jsonString = JSON.stringify(cursorData);
|
|
1645
|
+
|
|
1646
|
+
if (this.cursorCaptureFirstWrite) {
|
|
1647
|
+
fs.appendFileSync(filepath, jsonString);
|
|
1648
|
+
this.cursorCaptureFirstWrite = false;
|
|
1649
|
+
} else {
|
|
1650
|
+
fs.appendFileSync(filepath, "," + jsonString);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Son pozisyonu sakla
|
|
1654
|
+
this.lastCapturedData = { ...cursorData };
|
|
1655
|
+
}
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
console.error("Cursor capture error:", error);
|
|
1658
|
+
}
|
|
1659
|
+
}, interval); // Configurable FPS
|
|
1660
|
+
|
|
1661
|
+
this.emit("cursorCaptureStarted", filepath);
|
|
1662
|
+
resolve(true);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
reject(error);
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1351
1667
|
}
|
|
1352
1668
|
|
|
1669
|
+
/**
|
|
1670
|
+
* Cursor capture durdurur - dosya yazma işlemini sonlandırır
|
|
1671
|
+
*/
|
|
1353
1672
|
async stopCursorCapture() {
|
|
1354
|
-
return
|
|
1673
|
+
return new Promise((resolve, reject) => {
|
|
1674
|
+
try {
|
|
1675
|
+
if (!this.cursorCaptureInterval) {
|
|
1676
|
+
return resolve(false);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// Interval'ı durdur
|
|
1680
|
+
clearInterval(this.cursorCaptureInterval);
|
|
1681
|
+
this.cursorCaptureInterval = null;
|
|
1682
|
+
|
|
1683
|
+
// Dosyayı kapat
|
|
1684
|
+
if (this.cursorCaptureFile) {
|
|
1685
|
+
const fs = require("fs");
|
|
1686
|
+
fs.appendFileSync(this.cursorCaptureFile, "]");
|
|
1687
|
+
this.cursorCaptureFile = null;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// Değişkenleri temizle
|
|
1691
|
+
this.lastCapturedData = null;
|
|
1692
|
+
this.cursorCaptureStartTime = null;
|
|
1693
|
+
this.cursorCaptureFirstWrite = true;
|
|
1694
|
+
this.cursorDisplayInfo = null;
|
|
1695
|
+
|
|
1696
|
+
this.emit("cursorCaptureStopped");
|
|
1697
|
+
resolve(true);
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
reject(error);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1355
1702
|
}
|
|
1356
1703
|
|
|
1357
1704
|
/**
|
package/package.json
CHANGED
|
@@ -502,8 +502,7 @@ extern "C" bool stopAVFoundationRecording() {
|
|
|
502
502
|
|
|
503
503
|
// Finish writing with null checks
|
|
504
504
|
AVAssetWriterInput *writerInput = g_avVideoInput;
|
|
505
|
-
|
|
506
|
-
if (writerInput && writerRef && writerRef.status == AVAssetWriterStatusWriting) {
|
|
505
|
+
if (writerInput) {
|
|
507
506
|
[writerInput markAsFinished];
|
|
508
507
|
}
|
|
509
508
|
|
package/src/cursor_tracker.mm
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
#import <Accessibility/Accessibility.h>
|
|
8
8
|
#import <dispatch/dispatch.h>
|
|
9
9
|
#import "logging.h"
|
|
10
|
-
#import "text_input_ax_snapshot.h"
|
|
11
10
|
#include <vector>
|
|
12
11
|
#include <math.h>
|
|
13
12
|
|
|
@@ -815,10 +814,6 @@ static bool g_leftMouseDown = false;
|
|
|
815
814
|
static bool g_rightMouseDown = false;
|
|
816
815
|
static NSString *g_lastEventType = @"move";
|
|
817
816
|
|
|
818
|
-
// Text input (keyboard) tracking state
|
|
819
|
-
static NSTimeInterval g_lastTextInputEmitTime = 0; // Throttle: son textInput event zamanı
|
|
820
|
-
static const NSTimeInterval TEXT_INPUT_THROTTLE_MS = 50; // Min 50ms aralık (20 FPS)
|
|
821
|
-
|
|
822
817
|
// Accessibility tabanlı cursor tip tespiti
|
|
823
818
|
static NSString* detectCursorTypeUsingAccessibility(CGPoint cursorPos) {
|
|
824
819
|
@autoreleasepool {
|
|
@@ -1947,29 +1942,6 @@ void writeToFile(NSDictionary *cursorData) {
|
|
|
1947
1942
|
}
|
|
1948
1943
|
}
|
|
1949
1944
|
|
|
1950
|
-
// Text input event: Klavye basıldığında focused text field'in caret pozisyonunu yakala
|
|
1951
|
-
static void emitTextInputEvent(NSTimeInterval timestamp, NSTimeInterval unixTimeMs, CGPoint mouseLocation) {
|
|
1952
|
-
if (unixTimeMs - g_lastTextInputEmitTime < TEXT_INPUT_THROTTLE_MS) {
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
NSDictionary *snap = MRTextInputSnapshotDictionary();
|
|
1957
|
-
if (!snap) {
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
NSMutableDictionary *textInputInfo = [NSMutableDictionary dictionaryWithDictionary:snap];
|
|
1962
|
-
textInputInfo[@"x"] = @((int)mouseLocation.x);
|
|
1963
|
-
textInputInfo[@"y"] = @((int)mouseLocation.y);
|
|
1964
|
-
textInputInfo[@"timestamp"] = @(timestamp);
|
|
1965
|
-
textInputInfo[@"unixTimeMs"] = @(unixTimeMs);
|
|
1966
|
-
textInputInfo[@"cursorType"] = @"text";
|
|
1967
|
-
textInputInfo[@"type"] = @"textInput";
|
|
1968
|
-
|
|
1969
|
-
writeToFile(textInputInfo);
|
|
1970
|
-
g_lastTextInputEmitTime = unixTimeMs;
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
1945
|
// Event callback for mouse events
|
|
1974
1946
|
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
|
1975
1947
|
@autoreleasepool {
|
|
@@ -1996,23 +1968,6 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
1996
1968
|
// Event tipini belirle
|
|
1997
1969
|
switch (type) {
|
|
1998
1970
|
case kCGEventLeftMouseDown:
|
|
1999
|
-
eventType = @"mousedown";
|
|
2000
|
-
// Odak/caret çoğu uygulamada tıklamadan hemen sonra oluşur; kısa gecikmeyle AX caret yaz.
|
|
2001
|
-
// (Sadece tuşta emit edilince ilk tıkta timeline'da textInput olmuyordu.)
|
|
2002
|
-
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.028 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
2003
|
-
if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) return;
|
|
2004
|
-
NSDate *now = [NSDate date];
|
|
2005
|
-
NSTimeInterval ts = [now timeIntervalSinceDate:g_trackingStartTime] * 1000.0;
|
|
2006
|
-
NSTimeInterval unixMs = [now timeIntervalSince1970] * 1000.0;
|
|
2007
|
-
CGPoint loc = CGPointZero;
|
|
2008
|
-
CGEventRef posEv = CGEventCreate(NULL);
|
|
2009
|
-
if (posEv) {
|
|
2010
|
-
loc = CGEventGetLocation(posEv);
|
|
2011
|
-
CFRelease(posEv);
|
|
2012
|
-
}
|
|
2013
|
-
emitTextInputEvent(ts, unixMs, loc);
|
|
2014
|
-
});
|
|
2015
|
-
break;
|
|
2016
1971
|
case kCGEventRightMouseDown:
|
|
2017
1972
|
case kCGEventOtherMouseDown:
|
|
2018
1973
|
eventType = @"mousedown";
|
|
@@ -2027,10 +1982,6 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
2027
1982
|
case kCGEventOtherMouseDragged:
|
|
2028
1983
|
eventType = @"drag";
|
|
2029
1984
|
break;
|
|
2030
|
-
case kCGEventKeyDown:
|
|
2031
|
-
// Klavye event'i — text caret tracking için
|
|
2032
|
-
emitTextInputEvent(timestamp, unixTimeMs, location);
|
|
2033
|
-
return event; // Mouse event olarak işleme, ayrı handle edildi
|
|
2034
1985
|
case kCGEventMouseMoved:
|
|
2035
1986
|
default:
|
|
2036
1987
|
eventType = @"move";
|
|
@@ -2040,7 +1991,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
2040
1991
|
if (!ShouldEmitCursorEvent(location, cursorType, eventType)) {
|
|
2041
1992
|
return event;
|
|
2042
1993
|
}
|
|
2043
|
-
|
|
1994
|
+
|
|
2044
1995
|
// Cursor data oluştur
|
|
2045
1996
|
NSDictionary *cursorInfo = @{
|
|
2046
1997
|
@"x": @((int)location.x),
|
|
@@ -2050,7 +2001,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
2050
2001
|
@"cursorType": cursorType,
|
|
2051
2002
|
@"type": eventType
|
|
2052
2003
|
};
|
|
2053
|
-
|
|
2004
|
+
|
|
2054
2005
|
// Direkt dosyaya yaz
|
|
2055
2006
|
writeToFile(cursorInfo);
|
|
2056
2007
|
RememberCursorEvent(location, cursorType, eventType);
|
|
@@ -2086,13 +2037,6 @@ void cursorTimerCallback() {
|
|
|
2086
2037
|
cursorType = @"default";
|
|
2087
2038
|
}
|
|
2088
2039
|
|
|
2089
|
-
// Timer-only mod: CGEventTap yokken kCGEventKeyDown gelmez; caret satırları hiç yazılmazdı.
|
|
2090
|
-
// I-beam görünürken AX ile periyodik textInput üret (emitTextInputEvent içinde throttle var).
|
|
2091
|
-
if ([cursorType isEqualToString:@"text"] ||
|
|
2092
|
-
[cursorType isEqualToString:@"vertical-text"]) {
|
|
2093
|
-
emitTextInputEvent(timestamp, unixTimeMs, location);
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
2040
|
// Mouse button state polling — event tap olmadığında click/drag tespiti
|
|
2097
2041
|
bool currentLeftMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonLeft);
|
|
2098
2042
|
bool currentRightMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonRight);
|
|
@@ -2196,7 +2140,6 @@ void cleanupCursorTracking() {
|
|
|
2196
2140
|
g_lastDetectedCursorType = nil;
|
|
2197
2141
|
g_cursorTypeCounter = 0;
|
|
2198
2142
|
g_isFirstWrite = true;
|
|
2199
|
-
g_lastTextInputEmitTime = 0;
|
|
2200
2143
|
ResetCursorEventHistory();
|
|
2201
2144
|
}
|
|
2202
2145
|
|
|
@@ -2237,7 +2180,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
2237
2180
|
g_trackingStartTime = [NSDate date];
|
|
2238
2181
|
ResetCursorEventHistory();
|
|
2239
2182
|
|
|
2240
|
-
// Create event tap for mouse
|
|
2183
|
+
// Create event tap for mouse events
|
|
2241
2184
|
CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
|
|
2242
2185
|
CGEventMaskBit(kCGEventLeftMouseUp) |
|
|
2243
2186
|
CGEventMaskBit(kCGEventRightMouseDown) |
|
|
@@ -2247,8 +2190,7 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
2247
2190
|
CGEventMaskBit(kCGEventMouseMoved) |
|
|
2248
2191
|
CGEventMaskBit(kCGEventLeftMouseDragged) |
|
|
2249
2192
|
CGEventMaskBit(kCGEventRightMouseDragged) |
|
|
2250
|
-
CGEventMaskBit(kCGEventOtherMouseDragged)
|
|
2251
|
-
CGEventMaskBit(kCGEventKeyDown));
|
|
2193
|
+
CGEventMaskBit(kCGEventOtherMouseDragged));
|
|
2252
2194
|
|
|
2253
2195
|
bool eventTapActive = false;
|
|
2254
2196
|
g_eventTap = CGEventTapCreate(kCGSessionEventTap,
|
|
@@ -2578,33 +2520,6 @@ Napi::Value GetCursorDebugInfo(const Napi::CallbackInfo& info) {
|
|
|
2578
2520
|
}
|
|
2579
2521
|
}
|
|
2580
2522
|
|
|
2581
|
-
static Napi::Value DictToNapiTextInputSnapshot(Napi::Env env, NSDictionary *snap) {
|
|
2582
|
-
Napi::Object o = Napi::Object::New(env);
|
|
2583
|
-
NSNumber *cx = snap[@"caretX"];
|
|
2584
|
-
NSNumber *cy = snap[@"caretY"];
|
|
2585
|
-
o.Set("caretX", Napi::Number::New(env, cx ? [cx doubleValue] : 0));
|
|
2586
|
-
o.Set("caretY", Napi::Number::New(env, cy ? [cy doubleValue] : 0));
|
|
2587
|
-
NSDictionary *frame = snap[@"inputFrame"];
|
|
2588
|
-
Napi::Object fo = Napi::Object::New(env);
|
|
2589
|
-
if ([frame isKindOfClass:[NSDictionary class]]) {
|
|
2590
|
-
fo.Set("x", Napi::Number::New(env, [frame[@"x"] doubleValue]));
|
|
2591
|
-
fo.Set("y", Napi::Number::New(env, [frame[@"y"] doubleValue]));
|
|
2592
|
-
fo.Set("width", Napi::Number::New(env, [frame[@"width"] doubleValue]));
|
|
2593
|
-
fo.Set("height", Napi::Number::New(env, [frame[@"height"] doubleValue]));
|
|
2594
|
-
}
|
|
2595
|
-
o.Set("inputFrame", fo);
|
|
2596
|
-
return o;
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
static Napi::Value GetTextInputSnapshot(const Napi::CallbackInfo& info) {
|
|
2600
|
-
Napi::Env env = info.Env();
|
|
2601
|
-
NSDictionary *snap = MRTextInputSnapshotDictionary();
|
|
2602
|
-
if (!snap) {
|
|
2603
|
-
return env.Null();
|
|
2604
|
-
}
|
|
2605
|
-
return DictToNapiTextInputSnapshot(env, snap);
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
2523
|
// Export functions
|
|
2609
2524
|
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
|
|
2610
2525
|
exports.Set("startCursorTracking", Napi::Function::New(env, StartCursorTracking));
|
|
@@ -2612,7 +2527,6 @@ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
|
|
|
2612
2527
|
exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition));
|
|
2613
2528
|
exports.Set("getCursorTrackingStatus", Napi::Function::New(env, GetCursorTrackingStatus));
|
|
2614
2529
|
exports.Set("getCursorDebugInfo", Napi::Function::New(env, GetCursorDebugInfo));
|
|
2615
|
-
exports.Set("getTextInputSnapshot", Napi::Function::New(env, GetTextInputSnapshot));
|
|
2616
2530
|
|
|
2617
2531
|
return exports;
|
|
2618
2532
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
#import <CoreGraphics/CoreGraphics.h>
|
|
3
3
|
#import <AppKit/AppKit.h>
|
|
4
4
|
#import "../logging.h"
|
|
5
|
-
#import "../text_input_ax_snapshot.h"
|
|
6
5
|
|
|
7
6
|
// Thread-safe cursor tracking for Electron
|
|
8
7
|
static dispatch_queue_t g_cursorQueue = nil;
|
|
@@ -93,45 +92,12 @@ Napi::Value GetCursorPositionElectronSafe(const Napi::CallbackInfo& info) {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
|
|
96
|
-
static Napi::Value DictToNapiTextInputSnapshotElectron(Napi::Env env, NSDictionary *snap) {
|
|
97
|
-
Napi::Object o = Napi::Object::New(env);
|
|
98
|
-
NSNumber *cx = snap[@"caretX"];
|
|
99
|
-
NSNumber *cy = snap[@"caretY"];
|
|
100
|
-
o.Set("caretX", Napi::Number::New(env, cx ? [cx doubleValue] : 0));
|
|
101
|
-
o.Set("caretY", Napi::Number::New(env, cy ? [cy doubleValue] : 0));
|
|
102
|
-
NSDictionary *frame = snap[@"inputFrame"];
|
|
103
|
-
Napi::Object fo = Napi::Object::New(env);
|
|
104
|
-
if ([frame isKindOfClass:[NSDictionary class]]) {
|
|
105
|
-
fo.Set("x", Napi::Number::New(env, [frame[@"x"] doubleValue]));
|
|
106
|
-
fo.Set("y", Napi::Number::New(env, [frame[@"y"] doubleValue]));
|
|
107
|
-
fo.Set("width", Napi::Number::New(env, [frame[@"width"] doubleValue]));
|
|
108
|
-
fo.Set("height", Napi::Number::New(env, [frame[@"height"] doubleValue]));
|
|
109
|
-
}
|
|
110
|
-
o.Set("inputFrame", fo);
|
|
111
|
-
return o;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
Napi::Value GetTextInputSnapshotElectronSafe(const Napi::CallbackInfo& info) {
|
|
115
|
-
Napi::Env env = info.Env();
|
|
116
|
-
@try {
|
|
117
|
-
NSDictionary *snap = MRTextInputSnapshotDictionary();
|
|
118
|
-
if (!snap) {
|
|
119
|
-
return env.Null();
|
|
120
|
-
}
|
|
121
|
-
return DictToNapiTextInputSnapshotElectron(env, snap);
|
|
122
|
-
} @catch (NSException *e) {
|
|
123
|
-
NSLog(@"❌ getTextInputSnapshot: %@", e.reason);
|
|
124
|
-
return env.Null();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
95
|
// Initialize cursor tracker module
|
|
129
96
|
Napi::Object InitCursorTrackerElectron(Napi::Env env, Napi::Object exports) {
|
|
130
97
|
@try {
|
|
131
98
|
initializeCursorQueue();
|
|
132
99
|
|
|
133
100
|
exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPositionElectronSafe));
|
|
134
|
-
exports.Set("getTextInputSnapshot", Napi::Function::New(env, GetTextInputSnapshotElectronSafe));
|
|
135
101
|
|
|
136
102
|
MRLog(@"✅ Electron-safe cursor tracker initialized");
|
|
137
103
|
return exports;
|
|
@@ -13,9 +13,12 @@ static void initializeWindowQueue() {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
static BOOL ShouldAllowElectronWindows(void) {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// NSProcessInfo.environment snapshot'lanır ve runtime'da process.env değişikliklerini yansıtmaz.
|
|
17
|
+
// getenv() libc üzerinden her seferinde güncel değeri okur — menüden toggle'lanabilsin diye.
|
|
18
|
+
const char *raw = getenv("CREAVIT_ALLOW_ELECTRON_WINDOWS");
|
|
19
|
+
if (!raw) return NO;
|
|
18
20
|
|
|
21
|
+
NSString *flag = [NSString stringWithUTF8String:raw];
|
|
19
22
|
NSString *normalized = [[flag lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
20
23
|
return [normalized isEqualToString:@"1"] ||
|
|
21
24
|
[normalized isEqualToString:@"true"] ||
|