Spaces:
Paused
Paused
| /** | |
| * 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<number> = new Set(); | |
| private pressedButtons: Set<number> = 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<void> { | |
| // 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<void> { | |
| 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<void> { | |
| this.pressedButtons.delete(button); | |
| // xdotool doesn't have "mouseup", so we just track state | |
| } | |
| /** | |
| * Mouse wheel scroll | |
| */ | |
| async mouseWheel(deltaX: number, deltaY: number): Promise<void> { | |
| 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<void> { | |
| 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<void> { | |
| this.pressedKeys.delete(keyCode); | |
| } | |
| /** | |
| * Type text | |
| */ | |
| async typeText(text: string): Promise<void> { | |
| 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<number, string> = { | |
| 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<string> = 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(), | |
| }); | |
| } | |
| } | |