mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 07:16:02 +00:00
228 lines
5.2 KiB
TypeScript
228 lines
5.2 KiB
TypeScript
import { createRequire } from "node:module";
|
|
import type { HookMouseEvent, UiohookLike, UiohookModuleNamespace, CursorInteractionType } from "../types";
|
|
import {
|
|
isCursorCaptureActive,
|
|
cursorCaptureStartTimeMs,
|
|
interactionCaptureCleanup,
|
|
setInteractionCaptureCleanup,
|
|
hasLoggedInteractionHookFailure,
|
|
setHasLoggedInteractionHookFailure,
|
|
lastLeftClick,
|
|
setLastLeftClick,
|
|
setLinuxCursorScreenPoint,
|
|
} from "../state";
|
|
import {
|
|
getNormalizedCursorPoint,
|
|
getHookCursorScreenPoint,
|
|
pushCursorSample,
|
|
} from "./telemetry";
|
|
|
|
const nodeRequire = createRequire(import.meta.url);
|
|
|
|
export function normalizeHookMouseButton(rawButton: unknown): 1 | 2 | 3 {
|
|
if (typeof rawButton !== "number" || !Number.isFinite(rawButton)) {
|
|
return 1;
|
|
}
|
|
|
|
if (rawButton === 2 || rawButton === 39) {
|
|
return 2;
|
|
}
|
|
|
|
if (rawButton === 3 || rawButton === 38) {
|
|
return 3;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
export function getHookMouseButton(event: HookMouseEvent | null | undefined): 1 | 2 | 3 {
|
|
return normalizeHookMouseButton(
|
|
event?.button ?? event?.mouseButton ?? event?.data?.button ?? event?.data?.mouseButton,
|
|
);
|
|
}
|
|
|
|
export function stopInteractionCapture() {
|
|
if (interactionCaptureCleanup) {
|
|
interactionCaptureCleanup();
|
|
setInteractionCaptureCleanup(null);
|
|
}
|
|
}
|
|
|
|
function isUiohookLike(value: unknown): value is UiohookLike {
|
|
const candidate = value as Partial<UiohookLike> | null;
|
|
return typeof candidate?.on === "function" && typeof candidate?.start === "function";
|
|
}
|
|
|
|
function loadUiohookModule() {
|
|
const moduleExports = nodeRequire("uiohook-napi") as UiohookModuleNamespace;
|
|
const defaultExport = moduleExports.default;
|
|
|
|
if (moduleExports.uIOhook) {
|
|
return moduleExports.uIOhook;
|
|
}
|
|
|
|
if (moduleExports.uiohook) {
|
|
return moduleExports.uiohook;
|
|
}
|
|
|
|
if (moduleExports.Uiohook) {
|
|
return moduleExports.Uiohook;
|
|
}
|
|
|
|
if (isUiohookLike(defaultExport)) {
|
|
return defaultExport;
|
|
}
|
|
|
|
if (defaultExport?.uIOhook) {
|
|
return defaultExport.uIOhook;
|
|
}
|
|
|
|
if (defaultExport?.uiohook) {
|
|
return defaultExport.uiohook;
|
|
}
|
|
|
|
if (defaultExport?.Uiohook) {
|
|
return defaultExport.Uiohook;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export async function startInteractionCapture() {
|
|
if (!isCursorCaptureActive) {
|
|
return;
|
|
}
|
|
|
|
if (!["darwin", "win32", "linux"].includes(process.platform)) {
|
|
return;
|
|
}
|
|
|
|
stopInteractionCapture();
|
|
|
|
try {
|
|
const hook = loadUiohookModule();
|
|
console.log(
|
|
"[CursorTelemetry] hook loaded:",
|
|
!!hook,
|
|
"has.on:",
|
|
typeof hook?.on,
|
|
"has.start:",
|
|
typeof hook?.start,
|
|
);
|
|
if (!isCursorCaptureActive) {
|
|
return;
|
|
}
|
|
|
|
if (!hook || typeof hook.on !== "function" || typeof hook.start !== "function") {
|
|
console.log("[CursorTelemetry] hook unusable — aborting interaction capture");
|
|
return;
|
|
}
|
|
|
|
const onMouseDown = (event: HookMouseEvent) => {
|
|
if (!isCursorCaptureActive) {
|
|
return;
|
|
}
|
|
|
|
const point = getNormalizedCursorPoint();
|
|
if (!point) {
|
|
return;
|
|
}
|
|
|
|
const timeMs = Date.now() - cursorCaptureStartTimeMs;
|
|
const button = getHookMouseButton(event);
|
|
let interactionType: CursorInteractionType = "click";
|
|
|
|
if (button === 2) {
|
|
interactionType = "right-click";
|
|
} else if (button === 3) {
|
|
interactionType = "middle-click";
|
|
} else {
|
|
const thresholdMs = 350;
|
|
const distance = lastLeftClick
|
|
? Math.hypot(point.cx - lastLeftClick.cx, point.cy - lastLeftClick.cy)
|
|
: Number.POSITIVE_INFINITY;
|
|
|
|
if (
|
|
lastLeftClick &&
|
|
timeMs - lastLeftClick.timeMs <= thresholdMs &&
|
|
distance <= 0.04
|
|
) {
|
|
interactionType = "double-click";
|
|
}
|
|
|
|
setLastLeftClick({ timeMs, cx: point.cx, cy: point.cy });
|
|
}
|
|
|
|
pushCursorSample(point.cx, point.cy, timeMs, interactionType);
|
|
};
|
|
|
|
const onMouseUp = () => {
|
|
if (!isCursorCaptureActive) {
|
|
return;
|
|
}
|
|
|
|
const point = getNormalizedCursorPoint();
|
|
if (!point) {
|
|
return;
|
|
}
|
|
|
|
const timeMs = Date.now() - cursorCaptureStartTimeMs;
|
|
pushCursorSample(point.cx, point.cy, timeMs, "mouseup");
|
|
};
|
|
|
|
const onMouseMove = (event: HookMouseEvent) => {
|
|
if (process.platform !== "linux" || !isCursorCaptureActive) {
|
|
return;
|
|
}
|
|
|
|
const point = getHookCursorScreenPoint(event);
|
|
if (!point) {
|
|
return;
|
|
}
|
|
|
|
setLinuxCursorScreenPoint({ x: point.x, y: point.y, updatedAt: Date.now() });
|
|
};
|
|
|
|
hook.on("mousedown", onMouseDown);
|
|
hook.on("mouseup", onMouseUp);
|
|
if (process.platform === "linux") {
|
|
hook.on("mousemove", onMouseMove);
|
|
}
|
|
|
|
setInteractionCaptureCleanup(() => {
|
|
try {
|
|
if (typeof hook.off === "function") {
|
|
hook.off("mousedown", onMouseDown);
|
|
hook.off("mouseup", onMouseUp);
|
|
if (process.platform === "linux") {
|
|
hook.off("mousemove", onMouseMove);
|
|
}
|
|
} else if (typeof hook.removeListener === "function") {
|
|
hook.removeListener("mousedown", onMouseDown);
|
|
hook.removeListener("mouseup", onMouseUp);
|
|
if (process.platform === "linux") {
|
|
hook.removeListener("mousemove", onMouseMove);
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore listener cleanup errors
|
|
}
|
|
|
|
try {
|
|
if (typeof hook.stop === "function") {
|
|
hook.stop();
|
|
}
|
|
} catch {
|
|
// ignore hook shutdown errors
|
|
}
|
|
});
|
|
|
|
hook.start();
|
|
} catch (error) {
|
|
if (!hasLoggedInteractionHookFailure) {
|
|
setHasLoggedInteractionHookFailure(true);
|
|
console.warn("[CursorTelemetry] Global interaction capture unavailable:", error);
|
|
}
|
|
}
|
|
}
|