UNPKG

selection-hook

Version:

Text selection monitoring of native Node.js module with N-API across applications

728 lines (641 loc) 22.7 kB
/** * Text Selection Hook - Test Application * * This test file demonstrates the functionality of the Node-API native module * for monitoring text selections across applications on Windows. * * Features demonstrated: * - Text selection detection (drag and double-click) * - Mouse and keyboard event monitoring * - Interactive controls for testing different features */ // =========================== // === Module Dependencies === // =========================== const SelectionHook = require("../index.js"); // =========================== // === Configuration ======== // =========================== /** * Configuration flags for event display * Toggle these using keyboard shortcuts during runtime */ const config = { showMouseMoveEvents: false, // High CPU usage when enabled showMouseEvents: false, // Mouse clicks and wheel events showKeyboardEvents: false, // Keyboard key press/release events clipboardFallbackEnabled: false, // Use clipboard as fallback for text selection clipboardMode: 0, // Default clipboard mode (see SelectionHook.FilterMode) globalFilterMode: 0, // Default global filter mode (see SelectionHook.FilterMode) passiveModeEnabled: false, // Passive mode for text selection fineTunedListEnabled: false, // Fine-tuned list for specific application behaviors hookStarted: true, // Track if the hook is started (default: ON) }; // Add variables for Ctrl key hold detection let ctrlKeyPressTime = null; let ctrlKeyTriggered = false; // Flag to prevent multiple triggers const CTRL_HOLD_THRESHOLD = 500; // 500ms threshold for Ctrl key hold // Program list for clipboard mode and global filter const programList = ["cursor.exe", "vscode.exe", "notepad.exe"]; // Fine-tuned list for specific application behaviors const fineTunedList = ["acrobat.exe", "wps.exe", "cajviewer.exe"]; /** * ANSI color codes for console output formatting */ const colors = { info: "\x1b[36m%s\x1b[0m", // Cyan - Used for general information success: "\x1b[32m%s\x1b[0m", // Green - Used for successful operations and selections warning: "\x1b[33m%s\x1b[0m", // Yellow - Used for position data and toggles error: "\x1b[31m%s\x1b[0m", // Red - Used for errors highlight: "\x1b[35m%s\x1b[0m", // Magenta - Used for keyboard events }; // =========================== // === Initialize Hook ====== // =========================== // Create instance of the hook const hook = new SelectionHook(); // Exit if initialization failed if (!hook) { console.error(colors.error, "Failed to initialize text selection hook"); process.exit(1); } // =========================== // === Core Functions ======= // =========================== /** * Main entry point - initializes and starts the application */ function main() { printWelcomeMessage(); setupEventListeners(); startHook(); setupInputHandlers(); } /** * Display welcome message and usage instructions */ function printWelcomeMessage() { console.log(colors.info, "=== Text Selection Hook Test ==="); console.log("Testing mouse action detection for text selection"); console.log("\nPlease perform the following actions:"); console.log("1. Drag to select text (distance > 8px) - should log 'action_drag_selection'"); console.log("2. Double-click to select text - should log 'action_dblclick_selection'"); console.log("\nCoordinates Information:"); console.log(" - startTop/startBottom: First paragraph's left-top and left-bottom points"); console.log(" - endTop/endBottom: Last paragraph's right-top and right-bottom points"); console.log(" - mousePosStart: Initial mouse position when selection started"); console.log(" - mousePosEnd: Final mouse position when selection ended"); console.log("\nKeyboard Controls:"); console.log(" S - Toggle start/stop hook (default: ON)"); console.log(" M - Toggle mouse move events (default: OFF)"); console.log(" E - Toggle other mouse events (default: OFF)"); console.log(" K - Toggle keyboard events (default: OFF)"); console.log(" C - Display current selection"); console.log(" B - Toggle clipboard fallback (default: OFF)"); console.log(" L - Toggle clipboard mode (DEFAULT → EXCLUDE_LIST → INCLUDE_LIST)"); console.log(" F - Toggle global filter mode (DEFAULT → EXCLUDE_LIST → INCLUDE_LIST)"); console.log(" P - Toggle passive mode (default: OFF)"); console.log(" T - Toggle fine-tuned list (default: OFF)"); console.log(" W - Write text 'Test clipboard write from selection-hook' to clipboard"); console.log(" R - Read text from clipboard"); console.log(" A - Check accessibility permissions (macOS only)"); console.log(" Q - Request accessibility permissions (macOS only)"); console.log(" ? - Show help"); console.log(" Ctrl+C - Exit program"); console.log("\nIn passive mode, press & hold Ctrl key to trigger text selection"); } /** * Start the hook and begin listening for events */ function startHook() { if (hook.start({ debug: true })) { console.log(colors.success, "Text selection listener started successfully"); console.log(colors.info, "Mouse move events are disabled by default (press 'M' to toggle)"); } else { console.error(colors.error, "Failed to start text selection listener"); } } /** * Clean up resources and exit the application */ function cleanup() { console.log(colors.info, "\nStopping text selection listener..."); // Make sure to disable mouse move events before stopping to reduce resource usage if (config.showMouseMoveEvents) { hook.disableMouseMoveEvent(); } // Disable clipboard fallback if enabled if (config.clipboardFallbackEnabled) { hook.disableClipboard(); } hook.stop(); hook.cleanup(); process.exit(0); } // =========================== // === Event Listeners ====== // =========================== /** * Set up all event listeners for the hook */ function setupEventListeners() { // Text selection events - our primary functionality hook.on("text-selection", (selectionData) => { showSelection(selectionData); }); // Status events from the native module hook.on("status", (status) => { console.log(colors.info, "Listener status:", status); }); // Setup mouse events setupMouseEventListeners(); // Setup keyboard events setupKeyboardEventListeners(); // Error events hook.on("error", (error) => { console.error(colors.error, "Error:", error.message); }); } /** * Set up mouse-related event listeners */ function setupMouseEventListeners() { // Mouse move events (high CPU usage - disabled by default) hook.on("mouse-move", (eventData) => { if (config.showMouseMoveEvents) { console.log( colors.warning, "Mouse move:", `x: ${eventData.x}, y: ${eventData.y}, button: ${eventData.button}` ); } }); // Mouse button events hook.on("mouse-up", (eventData) => { if (config.showMouseEvents) { console.log( colors.warning, "Mouse up:", `button: ${eventData.button}, x: ${eventData.x}, y: ${eventData.y}` ); } }); hook.on("mouse-down", (eventData) => { if (config.showMouseEvents) { console.log( colors.warning, "Mouse down:", `button: ${eventData.button}, x: ${eventData.x}, y: ${eventData.y}` ); } }); // Mouse wheel events hook.on("mouse-wheel", (eventData) => { if (config.showMouseEvents) { console.log( colors.warning, "Mouse wheel:", `button: ${eventData.button}, direction: ${eventData.flag > 0 ? "up/right" : "down/left"}` ); } }); } /** * Set up keyboard-related event listeners */ function setupKeyboardEventListeners() { // Key down events hook.on("key-down", (eventData) => { if (config.showKeyboardEvents) { console.log( colors.highlight, "Key down:", `uniKey: ${eventData.uniKey}, vkCode: ${eventData.vkCode}, scanCode: ${ eventData.scanCode }, flags: ${eventData.flags}${eventData.sys ? ", system key" : ""}` ); } // Handle Ctrl+C for exit if (eventData.vkCode === 67 && eventData.flags & 0x0001) { // 67 is 'C', 0x0001 is Ctrl flag cleanup(); return; } // Special handling for Ctrl key (vkCode 162) to get current selection in passive mode if (eventData.vkCode === 162 && config.passiveModeEnabled) { const currentTime = Date.now(); // Initialize press time if not set if (ctrlKeyPressTime === null) { ctrlKeyPressTime = currentTime; ctrlKeyTriggered = false; // Reset trigger flag return; } // Check if held long enough and not already triggered const holdDuration = currentTime - ctrlKeyPressTime; if (holdDuration >= CTRL_HOLD_THRESHOLD && !ctrlKeyTriggered) { console.log( colors.info, `Ctrl held for ${holdDuration}ms, requesting current selection in passive mode` ); const selectionData = hook.getCurrentSelection(); if (selectionData) { showSelection(selectionData); } else { console.log(colors.warning, "No selection data available"); } ctrlKeyTriggered = true; // Set flag to prevent further triggers } } }); // Key up events hook.on("key-up", (eventData) => { if (config.showKeyboardEvents) { console.log( colors.highlight, "Key up:", `uniKey: ${eventData.uniKey}, vkCode: ${eventData.vkCode}, scanCode: ${ eventData.scanCode }, flags: ${eventData.flags}${eventData.sys ? ", system key" : ""}` ); } // Reset variables when Ctrl is released if (eventData.vkCode === 162 && config.passiveModeEnabled) { ctrlKeyPressTime = null; ctrlKeyTriggered = false; } }); } // =========================== // === User Input Handling == // =========================== /** * Configure handlers for user input */ function setupInputHandlers() { // Set up standard input in raw mode for immediate key handling process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.on("data", handleKeyPress); // Handle Ctrl+C for graceful exit process.on("SIGINT", cleanup); } /** * Process keyboard input from the user * @param {Buffer} key - The key buffer from stdin */ function handleKeyPress(key) { const keyStr = key.toString(); // Exit on Ctrl+C if (keyStr === "\u0003") { cleanup(); return; } switch (keyStr.toLowerCase()) { case "s": // Toggle start/stop hook toggleHookStartStop(); break; case "m": // Toggle mouse move events toggleMouseMoveEvents(); break; case "e": // Toggle other mouse events config.showMouseEvents = !config.showMouseEvents; console.log(colors.success, `Mouse events: ${config.showMouseEvents ? "ON" : "OFF"}`); break; case "k": // Toggle keyboard events config.showKeyboardEvents = !config.showKeyboardEvents; console.log(colors.success, `Keyboard events: ${config.showKeyboardEvents ? "ON" : "OFF"}`); break; case "c": // Display current selection const selectionData = hook.getCurrentSelection(); if (selectionData) { console.log(colors.success, "Current selection retrieved"); showSelection(selectionData); } else { console.log(colors.warning, "No current selection available"); } break; case "b": // Toggle clipboard fallback config.clipboardFallbackEnabled = !config.clipboardFallbackEnabled; if (config.clipboardFallbackEnabled) { // Enable clipboard fallback in the native module const success = hook.enableClipboard(); if (success) { console.log(colors.success, "Clipboard fallback: ENABLED"); } else { config.clipboardFallbackEnabled = false; console.log(colors.error, "Failed to enable clipboard fallback"); } } else { // Disable clipboard fallback in the native module const success = hook.disableClipboard(); if (success) { console.log(colors.warning, "Clipboard fallback: DISABLED"); } else { console.log(colors.error, "Failed to disable clipboard fallback"); } } break; case "l": // Toggle clipboard mode toggleClipboardMode(); break; case "f": // Toggle global filter mode toggleGlobalFilterMode(); break; case "p": // Toggle passive mode togglePassiveMode(); break; case "t": // Toggle fine-tuned list toggleFineTunedList(); break; case "w": // Write test text to clipboard const testText = "Test clipboard write from selection-hook"; const success = hook.writeToClipboard(testText); if (success) { console.log(colors.success, `Successfully wrote text to clipboard: "${testText}"`); } else { console.log(colors.error, "Failed to write text to clipboard"); } break; case "r": // Read text from clipboard const clipboardText = hook.readFromClipboard(); if (clipboardText) { console.log(colors.success, `Text read from clipboard: "${clipboardText}"`); } else { console.log(colors.warning, "Failed to read text from clipboard"); } break; case "a": // Check accessibility permissions (macOS only) if (process.platform === "darwin") { const isTrusted = hook.macIsProcessTrusted(); if (isTrusted) { console.log(colors.success, "Process is trusted for accessibility on macOS"); } else { console.log(colors.warning, "Process is NOT trusted for accessibility on macOS"); console.log( colors.info, "You may need to grant accessibility permissions in System Preferences" ); } } else { console.log(colors.info, "Accessibility permission check is only available on macOS"); } break; case "q": // Request accessibility permissions (macOS only) if (process.platform === "darwin") { console.log(colors.info, "Requesting accessibility permissions..."); const currentStatus = hook.macRequestProcessTrust(); if (currentStatus) { console.log(colors.success, "Process is already trusted for accessibility on macOS"); } else { console.log(colors.warning, "Process is not trusted for accessibility on macOS"); console.log(colors.info, "A system dialog may have appeared to grant permissions"); console.log( colors.info, "Please check System Preferences > Security & Privacy > Accessibility" ); } } else { console.log(colors.info, "Accessibility permission request is only available on macOS"); } break; case "?": // Show help printWelcomeMessage(); break; default: break; } } /** * Toggle hook start/stop * Controls whether the hook is actively listening for events */ function toggleHookStartStop() { config.hookStarted = !config.hookStarted; if (config.hookStarted) { // Start the hook const success = hook.start({ debug: true }); if (success) { console.log(colors.success, "Text selection hook: STARTED"); } else { config.hookStarted = false; // Revert the toggle console.log(colors.error, "Failed to start text selection hook"); } } else { // Stop the hook const success = hook.stop(); if (success) { console.log(colors.warning, "Text selection hook: STOPPED"); } else { config.hookStarted = true; // Revert the toggle console.log(colors.error, "Failed to stop text selection hook"); } } } /** * Toggle mouse move event tracking * Mouse move events use significant CPU resources, so they're disabled by default */ function toggleMouseMoveEvents() { config.showMouseMoveEvents = !config.showMouseMoveEvents; if (config.showMouseMoveEvents) { // Enable mouse move events in the native module const success = hook.enableMouseMoveEvent(); if (success) { console.log(colors.success, "Mouse move events: ENABLED"); } else { config.showMouseMoveEvents = false; console.log(colors.error, "Failed to enable mouse move events"); } } else { // Disable mouse move events in the native module const success = hook.disableMouseMoveEvent(); if (success) { console.log(colors.warning, "Mouse move events: DISABLED"); } else { console.log( colors.error, "Failed to disable mouse move events, but events won't be displayed" ); } } } /** * Toggle passive mode for text selection * In passive mode, the hook doesn't automatically monitor selections * and requires manual triggering (e.g., with Alt key) */ function togglePassiveMode() { config.passiveModeEnabled = !config.passiveModeEnabled; // Set passive mode in the native module const success = hook.setSelectionPassiveMode(config.passiveModeEnabled); if (success) { console.log( colors.success, `Passive mode: ${config.passiveModeEnabled ? "ENABLED" : "DISABLED"}` ); if (config.passiveModeEnabled) { console.log(colors.info, "Press & hold Ctrl key to trigger text selection manually"); } } else { config.passiveModeEnabled = !config.passiveModeEnabled; // Revert the toggle console.log(colors.error, "Failed to set passive mode"); } } /** * Toggle fine-tuned list for specific application behaviors */ function toggleFineTunedList() { config.fineTunedListEnabled = !config.fineTunedListEnabled; const programList = config.fineTunedListEnabled ? fineTunedList : []; // Set fine-tuned list for both types simultaneously const excludeClipboardSuccess = hook.setFineTunedList( SelectionHook.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT, programList ); const includeDelaySuccess = hook.setFineTunedList( SelectionHook.FineTunedListType.INCLUDE_CLIPBOARD_DELAY_READ, programList ); if (excludeClipboardSuccess && includeDelaySuccess) { console.log( colors.success, `Fine-tuned list: ${config.fineTunedListEnabled ? "ENABLED" : "DISABLED"}` ); if (config.fineTunedListEnabled) { console.log(colors.info, `Programs in fine-tuned list: ${fineTunedList.join(", ")}`); console.log( colors.info, "Applied to both EXCLUDE_CLIPBOARD_CURSOR_DETECT and INCLUDE_CLIPBOARD_DELAY_READ" ); } } else { config.fineTunedListEnabled = !config.fineTunedListEnabled; // Revert the toggle console.log(colors.error, "Failed to set fine-tuned list"); if (!excludeClipboardSuccess) { console.log(colors.error, "- EXCLUDE_CLIPBOARD_CURSOR_DETECT failed"); } if (!includeDelaySuccess) { console.log(colors.error, "- INCLUDE_CLIPBOARD_DELAY_READ failed"); } } } /** * Get clipboard mode name from numeric value * @param {number} mode - Clipboard mode value * @returns {string} Mode name */ function getFilterModeName(mode) { const modeNames = { [SelectionHook.FilterMode.DEFAULT]: "DEFAULT", [SelectionHook.FilterMode.EXCLUDE_LIST]: "EXCLUDE_LIST", [SelectionHook.FilterMode.INCLUDE_LIST]: "INCLUDE_LIST", }; return modeNames[mode] || "UNKNOWN"; } /** * Toggle clipboard mode (DEFAULT -> EXCLUDE_LIST -> INCLUDE_LIST -> DEFAULT) */ function toggleClipboardMode() { // Rotate through the modes config.clipboardMode = (config.clipboardMode + 1) % 3; // Apply the new mode if clipboard fallback is enabled const success = hook.setClipboardMode(config.clipboardMode, programList); if (success) { console.log( colors.success, `Clipboard mode set to ${getFilterModeName(config.clipboardMode)} for ${programList.join( ", " )}` ); } else { console.log(colors.error, "Failed to set clipboard mode"); } } /** * Toggle global filter mode (DEFAULT -> EXCLUDE_LIST -> INCLUDE_LIST -> DEFAULT) */ function toggleGlobalFilterMode() { // Rotate through the modes config.globalFilterMode = (config.globalFilterMode + 1) % 3; // Apply the new mode const success = hook.setGlobalFilterMode(config.globalFilterMode, programList); if (success) { console.log( colors.success, `Global filter mode set to ${getFilterModeName( config.globalFilterMode )} for ${programList.join(", ")}` ); } else { console.log(colors.error, "Failed to set global filter mode"); } } // =========================== // === Utility Functions ==== // =========================== /** * Display text selection data * @param {Object} selectionData - Selection data from the hook */ function showSelection(selectionData) { if (!selectionData) { return; } console.log(colors.info, `=== Detected selected text (${selectionData.text.length}) ===`); console.log(selectionData.text.replace(/\r\n/g, "\n").replace(/\r(?!\n)/g, "\n")); // Selection method and position level maps const methodMap = { 0: "None", 1: "UI Automation", 2: "Focus Control", 3: "Accessibility", 99: "Clipboard", }; const posLevelMap = { 0: "None", 1: "Mouse Single", 2: "Mouse Dual", 3: "Selection Full", 4: "Selection Detailed", }; console.log(colors.warning, "=== Selection Information ==="); console.log(colors.warning, "Program Name:", selectionData.programName); console.log( colors.warning, "Method: ", `${methodMap[selectionData.method] || selectionData.method}` ); console.log( colors.warning, "Position Level: ", `${posLevelMap[selectionData.posLevel] || selectionData.posLevel}` ); // Display all available coordinates if (selectionData.posLevel >= 1) { console.log( colors.warning, "Mouse: ", `(${selectionData.mousePosStart.x}, ${selectionData.mousePosStart.y}) -> (${selectionData.mousePosEnd.x}, ${selectionData.mousePosEnd.y})` ); } if (selectionData.posLevel >= 3) { console.log( colors.warning, ` startTop(${selectionData.startTop.x}, ${selectionData.startTop.y})\t endTop(${selectionData.endTop.x}, ${selectionData.endTop.y})` ); console.log( colors.warning, `startBottom(${selectionData.startBottom.x}, ${selectionData.startBottom.y})\tendBottom(${selectionData.endBottom.x}, ${selectionData.endBottom.y})` ); } } // =========================== // === Start Application ==== // =========================== // Launch the application main();