diff --git a/.gitignore b/.gitignore index 43ddf77b..82f26506 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,6 @@ release/** .tmp/ .history/ -# WGC native capture build artifacts +# Native capture build artifacts electron/native/wgc-capture/build/ +electron/native/cursor-monitor/build/ diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index ef1d22d9..a434c77d 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -710,6 +710,10 @@ function getWgcCaptureExePath() { return resolveUnpackedAppPath('electron', 'native', 'wgc-capture', 'build', 'Release', 'wgc-capture.exe') } +function getCursorMonitorExePath() { + return resolveUnpackedAppPath('electron', 'native', 'cursor-monitor', 'build', 'Release', 'cursor-monitor.exe') +} + async function isWgcCaptureAvailable(): Promise { if (process.platform !== 'win32') return false @@ -1067,50 +1071,58 @@ async function ensureNativeCursorMonitorBinary() { ) } +function handleCursorMonitorStdout(chunk: Buffer) { + nativeCursorMonitorOutputBuffer += chunk.toString() + const lines = nativeCursorMonitorOutputBuffer.split(/\r?\n/) + nativeCursorMonitorOutputBuffer = lines.pop() ?? '' + + for (const line of lines) { + const match = line.match(/^STATE:(.+)$/) + if (!match) continue + const next = match[1].trim() as CursorVisualType + if ( + next === 'arrow' + || next === 'text' + || next === 'pointer' + || next === 'crosshair' + || next === 'open-hand' + || next === 'closed-hand' + || next === 'resize-ew' + || next === 'resize-ns' + || next === 'not-allowed' + ) { + if (currentCursorVisualType !== next) { + currentCursorVisualType = next + sampleCursorStateChange(next) + emitCursorStateChanged(next) + } + } + } +} + async function startNativeCursorMonitor() { stopNativeCursorMonitor() - if (process.platform !== 'darwin') { + if (process.platform !== 'darwin' && process.platform !== 'win32') { currentCursorVisualType = undefined return } try { - const helperPath = await ensureNativeCursorMonitorBinary() + let helperPath: string + if (process.platform === 'win32') { + helperPath = getCursorMonitorExePath() + } else { + helperPath = await ensureNativeCursorMonitorBinary() + } + nativeCursorMonitorOutputBuffer = '' currentCursorVisualType = undefined nativeCursorMonitorProcess = spawn(helperPath, [], { stdio: ['pipe', 'pipe', 'pipe'], }) - nativeCursorMonitorProcess.stdout.on('data', (chunk: Buffer) => { - nativeCursorMonitorOutputBuffer += chunk.toString() - const lines = nativeCursorMonitorOutputBuffer.split(/\r?\n/) - nativeCursorMonitorOutputBuffer = lines.pop() ?? '' - - for (const line of lines) { - const match = line.match(/^STATE:(.+)$/) - if (!match) continue - const next = match[1].trim() as CursorVisualType - if ( - next === 'arrow' - || next === 'text' - || next === 'pointer' - || next === 'crosshair' - || next === 'open-hand' - || next === 'closed-hand' - || next === 'resize-ew' - || next === 'resize-ns' - || next === 'not-allowed' - ) { - if (currentCursorVisualType !== next) { - currentCursorVisualType = next - sampleCursorStateChange(next) - emitCursorStateChanged(next) - } - } - } - }) + nativeCursorMonitorProcess.stdout.on('data', handleCursorMonitorStdout) nativeCursorMonitorProcess.once('close', () => { nativeCursorMonitorProcess = null diff --git a/electron/native/cursor-monitor/CMakeLists.txt b/electron/native/cursor-monitor/CMakeLists.txt new file mode 100644 index 00000000..8155aed9 --- /dev/null +++ b/electron/native/cursor-monitor/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(cursor-monitor LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(cursor-monitor src/main.cpp) + +target_link_libraries(cursor-monitor PRIVATE user32) diff --git a/electron/native/cursor-monitor/src/main.cpp b/electron/native/cursor-monitor/src/main.cpp new file mode 100644 index 00000000..7ac86945 --- /dev/null +++ b/electron/native/cursor-monitor/src/main.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include +#include + +static std::atomic g_running{true}; + +static void stdinListener() { + std::string line; + while (std::getline(std::cin, line)) { + if (line == "stop") { + g_running.store(false); + return; + } + } + g_running.store(false); +} + +int main() { + std::setvbuf(stdout, nullptr, _IONBF, 0); + + std::unordered_map cursorMap; + cursorMap[LoadCursor(NULL, IDC_ARROW)] = "arrow"; + cursorMap[LoadCursor(NULL, IDC_IBEAM)] = "text"; + cursorMap[LoadCursor(NULL, IDC_HAND)] = "pointer"; + cursorMap[LoadCursor(NULL, IDC_CROSS)] = "crosshair"; + cursorMap[LoadCursor(NULL, IDC_NO)] = "not-allowed"; + cursorMap[LoadCursor(NULL, IDC_SIZEWE)] = "resize-ew"; + cursorMap[LoadCursor(NULL, IDC_SIZENS)] = "resize-ns"; + cursorMap[LoadCursor(NULL, IDC_SIZEALL)] = "open-hand"; + cursorMap[LoadCursor(NULL, IDC_WAIT)] = "arrow"; + cursorMap[LoadCursor(NULL, IDC_APPSTARTING)] = "arrow"; + + std::thread listener(stdinListener); + listener.detach(); + + std::string lastType; + + while (g_running.load()) { + CURSORINFO ci = {}; + ci.cbSize = sizeof(ci); + + if (GetCursorInfo(&ci) && (ci.flags & CURSOR_SHOWING)) { + auto it = cursorMap.find(ci.hCursor); + std::string type = (it != cursorMap.end()) ? it->second : "arrow"; + + if (type != lastType) { + lastType = type; + std::cout << "STATE:" << type << "\n"; + } + } + + Sleep(50); + } + + return 0; +} diff --git a/package.json b/package.json index 247a89e6..d41cd055 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,9 @@ "rebuild:native": "electron-rebuild --force --only uiohook-napi", "build:native-helpers": "node scripts/build-native-helpers.mjs", "build:wgc-capture": "node scripts/build-wgc-capture.mjs", + "build:cursor-monitor": "node scripts/build-cursor-monitor.mjs", "build:mac": "npm run build:native-helpers && tsc && vite build && electron-builder --mac", - "build:win": "npm run build:wgc-capture && tsc && vite build && electron-builder --win", + "build:win": "npm run build:wgc-capture && npm run build:cursor-monitor && tsc && vite build && electron-builder --win", "build:linux": "tsc && vite build && electron-builder --linux", "i18n:check": "node scripts/i18n-check.mjs", "test": "vitest --run", diff --git a/scripts/build-cursor-monitor.mjs b/scripts/build-cursor-monitor.mjs new file mode 100644 index 00000000..387d528f --- /dev/null +++ b/scripts/build-cursor-monitor.mjs @@ -0,0 +1,103 @@ +import { execSync } from "node:child_process"; +import { existsSync, mkdirSync } from "node:fs"; +import path from "node:path"; + +const projectRoot = process.cwd(); +const sourceDir = path.join(projectRoot, "electron", "native", "cursor-monitor"); +const buildDir = path.join(sourceDir, "build"); + +if (process.platform !== "win32") { + console.log("[build-cursor-monitor] Skipping: host platform is not Windows."); + process.exit(0); +} + +if (!existsSync(path.join(sourceDir, "CMakeLists.txt"))) { + console.error("[build-cursor-monitor] CMakeLists.txt not found at", sourceDir); + process.exit(1); +} + +function findCmake() { + // Check PATH first + try { + execSync("cmake --version", { stdio: "pipe" }); + return "cmake"; + } catch { + // not on PATH + } + + // VS 2022 bundled CMake + const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"]; + for (const edition of vsEditions) { + const cmakePath = path.join( + "C:", + "Program Files", + "Microsoft Visual Studio", + "2022", + edition, + "Common7", + "IDE", + "CommonExtensions", + "Microsoft", + "CMake", + "CMake", + "bin", + "cmake.exe", + ); + if (existsSync(cmakePath)) { + return `"${cmakePath}"`; + } + } + + return null; +} + +const cmake = findCmake(); +if (!cmake) { + console.error( + "[build-cursor-monitor] CMake not found. Install Visual Studio with C++ CMake tools or standalone CMake.", + ); + process.exit(1); +} + +mkdirSync(buildDir, { recursive: true }); + +console.log("[build-cursor-monitor] Configuring CMake..."); +try { + execSync(`${cmake} .. -G "Visual Studio 17 2022" -A x64`, { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }); +} catch { + console.log("[build-cursor-monitor] VS 2022 generator not found, trying VS 2019..."); + try { + execSync(`${cmake} .. -G "Visual Studio 16 2019" -A x64`, { + cwd: buildDir, + stdio: "inherit", + timeout: 120000, + }); + } catch (innerError) { + console.error("[build-cursor-monitor] CMake configure failed:", innerError.message); + process.exit(1); + } +} + +console.log("[build-cursor-monitor] Building..."); +try { + execSync(`${cmake} --build . --config Release`, { + cwd: buildDir, + stdio: "inherit", + timeout: 300000, + }); +} catch (error) { + console.error("[build-cursor-monitor] Build failed:", error.message); + process.exit(1); +} + +const exePath = path.join(buildDir, "Release", "cursor-monitor.exe"); +if (existsSync(exePath)) { + console.log(`[build-cursor-monitor] Built successfully: ${exePath}`); +} else { + console.error("[build-cursor-monitor] Expected exe not found at", exePath); + process.exit(1); +}