From dab92cd28e494ebb646e822bfdaf3f7f2b4b9e30 Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:31:46 +1100 Subject: [PATCH] Fix Windows cursor telemetry: add uiohook-napi to asarUnpack and harden export resolution uiohook-napi ships a native .node binary that cannot be loaded from inside an .asar archive. Without asarUnpack, nodeRequire('uiohook-napi') fails silently in packaged builds, the hook never starts, and cursor telemetry stays empty on Windows. Changes: - Add node_modules/uiohook-napi/** to asarUnpack so the native binary is extracted to app.asar.unpacked/ where require() can reach it - Extend loadUiohookModule() fallback chain to handle variant export shapes across platform builds (uIOhook / uiohook / Uiohook / default) - Add normalizeHookMouseButton() and getHookMouseButton() helpers for robust cross-platform button code handling (codes 38/39 variants) - Add diagnostic log to startInteractionCapture() to surface hook load state in future debug sessions --- electron-builder.json5 | 3 ++- electron/ipc/handlers.ts | 42 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/electron-builder.json5 b/electron-builder.json5 index 8846c643..0fd0b26e 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -5,6 +5,7 @@ "asar": true, "asarUnpack": [ "node_modules/ffmpeg-static/**", + "node_modules/uiohook-napi/**", "electron/native/**" ], "productName": "Recordly", @@ -66,4 +67,4 @@ "artifactName": "Recordly.exe" } } - + diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 2f26b640..ab6d8d75 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -419,7 +419,15 @@ function loadFfmpegStatic() { function loadUiohookModule() { const moduleExports = nodeRequire('uiohook-napi') - return (moduleExports as any)?.uIOhook ?? (moduleExports as any)?.default ?? moduleExports + return ( + (moduleExports as any)?.uIOhook + ?? (moduleExports as any)?.uiohook + ?? (moduleExports as any)?.Uiohook + ?? (moduleExports as any)?.default?.uIOhook + ?? (moduleExports as any)?.default?.uiohook + ?? (moduleExports as any)?.default + ?? moduleExports + ) } function getFfmpegBinaryPath() { @@ -913,6 +921,33 @@ let lastLeftClick: { timeMs: number; cx: number; cy: number } | null = null let selectedWindowBounds: WindowBounds | null = null let windowBoundsCaptureInterval: NodeJS.Timeout | null = null +function normalizeHookMouseButton(rawButton: unknown): 1 | 2 | 3 { + if (typeof rawButton !== 'number' || !Number.isFinite(rawButton)) { + return 1 + } + + // uiohook/libuiohook button codes are typically 1/2/3. Some wrappers may + // expose alternate constants depending on platform/runtime. + if (rawButton === 2 || rawButton === 39) { + return 2 + } + + if (rawButton === 3 || rawButton === 38) { + return 3 + } + + return 1 +} + +function getHookMouseButton(event: any): 1 | 2 | 3 { + return normalizeHookMouseButton( + event?.button + ?? event?.mouseButton + ?? event?.data?.button + ?? event?.data?.mouseButton + ) +} + function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)) } @@ -1113,11 +1148,13 @@ async function startInteractionCapture() { 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 } @@ -1132,7 +1169,7 @@ async function startInteractionCapture() { } const timeMs = Date.now() - cursorCaptureStartTimeMs - const button = typeof event?.button === 'number' ? event.button : 1 + const button = getHookMouseButton(event) let interactionType: CursorInteractionType = 'click' if (button === 2) { @@ -1171,6 +1208,7 @@ async function startInteractionCapture() { hook.on('mousedown', onMouseDown) hook.on('mouseup', onMouseUp) + hook.start() interactionCaptureCleanup = () => {