magic / src /lib /input-simulation.ts
diamond-in's picture
Rename input-simulation.ts to src/lib/input-simulation.ts
d1cd6a6 verified
/**
* 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(),
});
}
}