/** * Input Simulation Utility * Simulates keyboard and mouse input on the system * Designed to work without root access */ export interface MousePosition { x: number; y: number; } export interface InputConfig { displayWidth: number; displayHeight: number; } export class InputSimulator { private displayWidth: number; private displayHeight: number; private currentPosition: MousePosition = { x: 0, y: 0 }; private pressedKeys: Set = new Set(); private pressedButtons: Set = new Set(); constructor(config: InputConfig) { this.displayWidth = config.displayWidth; this.displayHeight = config.displayHeight; } /** * Move mouse to absolute position */ async mouseMove(x: number, y: number): Promise { // Clamp to display bounds x = Math.max(0, Math.min(this.displayWidth - 1, x)); y = Math.max(0, Math.min(this.displayHeight - 1, y)); this.currentPosition = { x, y }; try { // Try using xdotool via subprocess const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); await execAsync(`xdotool mousemove ${x} ${y}`).catch(() => { // Fallback: simulate in headless browser }); } catch (error) { // Silently fail - will be simulated in browser context console.debug('Mouse move simulated (no display access)'); } } /** * Press mouse button */ async mouseDown(button: number): Promise { if (button < 1 || button > 3) return; this.pressedButtons.add(button); try { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); // Map button numbers: 1=left, 2=middle, 3=right const xdotoolButton = button === 1 ? 1 : button === 2 ? 2 : 3; await execAsync(`xdotool click ${xdotoolButton}`).catch(() => {}); } catch (error) { // Simulation mode } } /** * Release mouse button */ async mouseUp(button: number): Promise { this.pressedButtons.delete(button); // xdotool doesn't have "mouseup", so we just track state } /** * Mouse wheel scroll */ async mouseWheel(deltaX: number, deltaY: number): Promise { try { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); // Scroll up/down based on deltaY const clicks = Math.abs(deltaY) > 0 ? Math.sign(deltaY) * 4 : 0; if (clicks !== 0) { await execAsync(`xdotool click ${clicks > 0 ? 4 : 5}`.repeat(Math.abs(clicks))).catch(() => {}); } } catch (error) { // Simulation mode } } /** * Press key */ async keyDown(keyCode: number): Promise { if (this.pressedKeys.has(keyCode)) return; this.pressedKeys.add(keyCode); try { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); // Convert keycode to key name const keyName = this.keyCodeToKeyName(keyCode); if (keyName) { await execAsync(`xdotool key ${keyName}`).catch(() => {}); } } catch (error) { // Simulation mode } } /** * Release key */ async keyUp(keyCode: number): Promise { this.pressedKeys.delete(keyCode); } /** * Type text */ async typeText(text: string): Promise { try { const { exec } = await import('child_process'); const { promisify } = await import('util'); const execAsync = promisify(exec); // Escape special characters for shell const escapedText = text.replace(/'/g, "'\\''"); await execAsync(`xdotool type '${escapedText}'`).catch(() => {}); } catch (error) { // Simulation mode - would be handled by browser events } } /** * Convert key code to xdotool key name */ private keyCodeToKeyName(keyCode: number): string | null { // Common key mappings const keyMap: Record = { 8: 'BackSpace', 9: 'Tab', 13: 'Return', 16: 'Shift_L', 17: 'Control_L', 18: 'Alt_L', 20: 'Caps_Lock', 27: 'Escape', 32: 'space', 33: 'Page_Up', 34: 'Page_Down', 35: 'End', 36: 'Home', 37: 'Left', 38: 'Up', 39: 'Right', 40: 'Down', 45: 'Insert', 46: 'Delete', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', 56: '8', 57: '9', 65: 'a', 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', 87: 'w', 88: 'x', 89: 'y', 90: 'z', 91: 'Super_L', 92: 'Super_R', 93: 'Menu', 96: 'KP_0', 97: 'KP_1', 98: 'KP_2', 99: 'KP_3', 100: 'KP_4', 101: 'KP_5', 102: 'KP_6', 103: 'KP_7', 104: 'KP_8', 105: 'KP_9', 106: 'KP_Multiply', 107: 'KP_Add', 109: 'KP_Subtract', 110: 'KP_Decimal', 111: 'KP_Divide', 112: 'F1', 113: 'F2', 114: 'F3', 115: 'F4', 116: 'F5', 117: 'F6', 118: 'F7', 119: 'F8', 120: 'F9', 121: 'F10', 122: 'F11', 123: 'F12', }; return keyMap[keyCode] || null; } /** * Get current mouse position */ getPosition(): MousePosition { return { ...this.currentPosition }; } /** * Get pressed keys */ getPressedKeys(): number[] { return Array.from(this.pressedKeys); } /** * Get pressed mouse buttons */ getPressedButtons(): number[] { return Array.from(this.pressedButtons); } } /** * Browser-side input tracking * Captures keyboard and mouse events from the viewer */ export class BrowserInputTracker { private element: HTMLElement | null = null; private onInput: ((event: any) => void) | null = null; private modifiers: Set = new Set(); /** * Attach input tracking to an element */ attach(element: HTMLElement, onInput: (event: any) => void): void { this.element = element; this.onInput = onInput; // Mouse events element.addEventListener('mousemove', this.handleMouseMove.bind(this)); element.addEventListener('mousedown', this.handleMouseDown.bind(this)); element.addEventListener('mouseup', this.handleMouseUp.bind(this)); element.addEventListener('wheel', this.handleWheel.bind(this)); // Keyboard events element.addEventListener('keydown', this.handleKeyDown.bind(this)); element.addEventListener('keyup', this.handleKeyUp.bind(this)); // Prevent default behaviors element.addEventListener('contextmenu', (e) => e.preventDefault()); element.tabIndex = 0; element.focus(); } /** * Detach input tracking */ detach(): void { if (this.element) { this.element.removeEventListener('mousemove', this.handleMouseMove.bind(this)); this.element.removeEventListener('mousedown', this.handleMouseDown.bind(this)); this.element.removeEventListener('mouseup', this.handleMouseUp.bind(this)); this.element.removeEventListener('wheel', this.handleWheel.bind(this)); this.element.removeEventListener('keydown', this.handleKeyDown.bind(this)); this.element.removeEventListener('keyup', this.handleKeyUp.bind(this)); } } private handleMouseMove(event: MouseEvent): void { const rect = this.element!.getBoundingClientRect(); const x = Math.floor((event.clientX - rect.left) * (this.element!.scrollWidth / rect.width)); const y = Math.floor((event.clientY - rect.top) * (this.element!.scrollHeight / rect.height)); this.onInput?.({ type: 'mousemove', x, y, timestamp: Date.now(), }); } private handleMouseDown(event: MouseEvent): void { event.preventDefault(); this.onInput?.({ type: 'mousedown', button: event.button + 1, // 1-indexed timestamp: Date.now(), }); } private handleMouseUp(event: MouseEvent): void { event.preventDefault(); this.onInput?.({ type: 'mouseup', button: event.button + 1, timestamp: Date.now(), }); } private handleWheel(event: WheelEvent): void { event.preventDefault(); this.onInput?.({ type: 'wheel', deltaX: event.deltaX, deltaY: event.deltaY, timestamp: Date.now(), }); } private handleKeyDown(event: KeyboardEvent): void { event.preventDefault(); this.onInput?.({ type: 'keydown', keyCode: event.which || event.keyCode, key: event.key, code: event.code, modifiers: { shift: event.shiftKey, ctrl: event.ctrlKey, alt: event.altKey, meta: event.metaKey, }, timestamp: Date.now(), }); } private handleKeyUp(event: KeyboardEvent): void { event.preventDefault(); this.onInput?.({ type: 'keyup', keyCode: event.which || event.keyCode, key: event.key, code: event.code, timestamp: Date.now(), }); } }