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 | 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); } } }