UNPKG

interm-mcp

Version:

MCP server for terminal applications and TUI automation with 127 tools

266 lines (265 loc) 12.7 kB
import { createTerminalError } from './utils/error-utils.js'; export class MouseManager { static instance; currentPosition = { x: 0, y: 0 }; dragThreshold = 3; doubleClickThreshold = 500; // milliseconds lastClickTime = 0; lastClickPosition = { x: 0, y: 0 }; clickCount = 0; constructor() { } static getInstance() { if (!MouseManager.instance) { MouseManager.instance = new MouseManager(); } return MouseManager.instance; } updatePosition(x, y) { this.currentPosition = { x, y }; } getCurrentPosition() { return { ...this.currentPosition }; } generateMouseSequence(sessionId, button, x, y, clickCount = 1) { // Generate terminal mouse sequence // Format: \x1b[M<button><x+32><y+32> for button press // Format: \x1b[M<button+32><x+32><y+32> for button release const buttonCodes = { 'left': 0, 'middle': 1, 'right': 2, 'wheel_up': 64, 'wheel_down': 65, 'wheel_left': 66, 'wheel_right': 67, 'x1': 3, 'x2': 4 }; const buttonCode = buttonCodes[button] ?? 0; // Clamp coordinates to valid range const clampedX = Math.max(1, Math.min(x + 1, 223)); const clampedY = Math.max(1, Math.min(y + 1, 223)); let sequence = ''; for (let i = 0; i < clickCount; i++) { // Button press sequence += `\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`; // Button release sequence += `\x1b[M${String.fromCharCode(buttonCode + 32 + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`; } this.updatePosition(x, y); return sequence; } generateMoveSequence(x, y, smooth = false, duration = 200) { if (!smooth) { this.updatePosition(x, y); // Generate mouse tracking sequence for movement return [`\x1b[M${String.fromCharCode(35)}${String.fromCharCode(x + 33)}${String.fromCharCode(y + 33)}`]; } // Generate smooth movement sequence const steps = Math.max(1, duration / 16); // ~60fps const sequences = []; const startX = this.currentPosition.x; const startY = this.currentPosition.y; const deltaX = (x - startX) / steps; const deltaY = (y - startY) / steps; for (let i = 0; i <= steps; i++) { const currentX = Math.round(startX + deltaX * i); const currentY = Math.round(startY + deltaY * i); const clampedX = Math.max(1, Math.min(currentX + 1, 223)); const clampedY = Math.max(1, Math.min(currentY + 1, 223)); sequences.push(`\x1b[M${String.fromCharCode(35)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); } this.updatePosition(x, y); return sequences; } generateDragSequence(startX, startY, endX, endY, button = 'left', smooth = true) { const sequences = []; // Check drag threshold const distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); if (distance < this.dragThreshold) { // Not enough distance for drag, perform regular click return [this.generateMouseSequence('', button, startX, startY, 1)]; } const buttonCodes = { 'left': 0, 'middle': 1, 'right': 2 }; const buttonCode = buttonCodes[button] ?? 0; // Button press at start position const startClampedX = Math.max(1, Math.min(startX + 1, 223)); const startClampedY = Math.max(1, Math.min(startY + 1, 223)); sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(startClampedX + 32)}${String.fromCharCode(startClampedY + 32)}`); if (smooth) { // Generate intermediate positions const steps = Math.max(5, Math.floor(distance / 10)); const deltaX = (endX - startX) / steps; const deltaY = (endY - startY) / steps; for (let i = 1; i < steps; i++) { const currentX = Math.round(startX + deltaX * i); const currentY = Math.round(startY + deltaY * i); const clampedX = Math.max(1, Math.min(currentX + 1, 223)); const clampedY = Math.max(1, Math.min(currentY + 1, 223)); // Drag movement (button held down) sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); } } // Button release at end position const endClampedX = Math.max(1, Math.min(endX + 1, 223)); const endClampedY = Math.max(1, Math.min(endY + 1, 223)); sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32 + 32)}${String.fromCharCode(endClampedX + 32)}${String.fromCharCode(endClampedY + 32)}`); this.updatePosition(endX, endY); return sequences; } generateScrollSequence(direction, amount, x, y, precision = false) { const directionCodes = { 'up': 64, 'down': 65, 'left': 66, 'right': 67 }; const buttonCode = directionCodes[direction] ?? 64; const clampedX = Math.max(1, Math.min(x + 1, 223)); const clampedY = Math.max(1, Math.min(y + 1, 223)); let sequence = ''; if (precision) { // For precision scrolling, generate multiple small scroll events const steps = Math.max(1, amount); for (let i = 0; i < steps; i++) { sequence += `\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`; } } else { // Regular scrolling for (let i = 0; i < amount; i++) { sequence += `\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`; } } return sequence; } detectMultiClick(x, y) { const currentTime = Date.now(); const timeDiff = currentTime - this.lastClickTime; const distance = Math.sqrt(Math.pow(x - this.lastClickPosition.x, 2) + Math.pow(y - this.lastClickPosition.y, 2)); if (timeDiff < this.doubleClickThreshold && distance < this.dragThreshold) { this.clickCount++; } else { this.clickCount = 1; } this.lastClickTime = currentTime; this.lastClickPosition = { x, y }; return this.clickCount; } generateGestureSequence(gestureType, startX, startY, size) { const sequences = []; switch (gestureType) { case 'swipe_left': return this.generateDragSequence(startX, startY, startX - size, startY, 'left', true); case 'swipe_right': return this.generateDragSequence(startX, startY, startX + size, startY, 'left', true); case 'swipe_up': return this.generateDragSequence(startX, startY, startX, startY - size, 'left', true); case 'swipe_down': return this.generateDragSequence(startX, startY, startX, startY + size, 'left', true); case 'circle_clockwise': return this.generateCircleGesture(startX, startY, size, true); case 'circle_counterclockwise': return this.generateCircleGesture(startX, startY, size, false); case 'zigzag_horizontal': return this.generateZigzagGesture(startX, startY, size, true); case 'zigzag_vertical': return this.generateZigzagGesture(startX, startY, size, false); default: throw createTerminalError('UNKNOWN_ERROR', `Unknown gesture type: ${gestureType}`); } } generateCircleGesture(centerX, centerY, radius, clockwise) { const sequences = []; const steps = 36; // 10-degree increments const angleIncrement = (2 * Math.PI) / steps * (clockwise ? 1 : -1); // Start at the top of the circle let angle = -Math.PI / 2; const startX = Math.round(centerX + radius * Math.cos(angle)); const startY = Math.round(centerY + radius * Math.sin(angle)); // Button press at start const buttonCode = 0; // left button let clampedX = Math.max(1, Math.min(startX + 1, 223)); let clampedY = Math.max(1, Math.min(startY + 1, 223)); sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); // Generate circle points for (let i = 1; i <= steps; i++) { angle += angleIncrement; const x = Math.round(centerX + radius * Math.cos(angle)); const y = Math.round(centerY + radius * Math.sin(angle)); clampedX = Math.max(1, Math.min(x + 1, 223)); clampedY = Math.max(1, Math.min(y + 1, 223)); sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); } // Button release sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32 + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); return sequences; } generateZigzagGesture(startX, startY, size, horizontal) { const sequences = []; const buttonCode = 0; // left button const segments = 4; const amplitude = size / 4; // Button press at start let clampedX = Math.max(1, Math.min(startX + 1, 223)); let clampedY = Math.max(1, Math.min(startY + 1, 223)); sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); for (let i = 1; i <= segments; i++) { const progress = i / segments; let x, y; if (horizontal) { x = Math.round(startX + size * progress); y = Math.round(startY + amplitude * (i % 2 === 0 ? 1 : -1)); } else { x = Math.round(startX + amplitude * (i % 2 === 0 ? 1 : -1)); y = Math.round(startY + size * progress); } clampedX = Math.max(1, Math.min(x + 1, 223)); clampedY = Math.max(1, Math.min(y + 1, 223)); sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); } // Button release sequences.push(`\x1b[M${String.fromCharCode(buttonCode + 32 + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`); return sequences; } generateMultiButtonSequence(buttons, x, y, holdDuration) { const buttonCodes = { 'left': 0, 'middle': 1, 'right': 2, 'x1': 3, 'x2': 4 }; const clampedX = Math.max(1, Math.min(x + 1, 223)); const clampedY = Math.max(1, Math.min(y + 1, 223)); let sequence = ''; // Press all buttons for (const button of buttons) { const buttonCode = buttonCodes[button] ?? 0; sequence += `\x1b[M${String.fromCharCode(buttonCode + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`; } // Hold duration would be handled by timing in the calling code // Release all buttons for (const button of buttons) { const buttonCode = buttonCodes[button] ?? 0; sequence += `\x1b[M${String.fromCharCode(buttonCode + 32 + 32)}${String.fromCharCode(clampedX + 32)}${String.fromCharCode(clampedY + 32)}`; } this.updatePosition(x, y); return sequence; } enableMouseTracking() { // Enable mouse tracking mode in terminal return '\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h'; } disableMouseTracking() { // Disable mouse tracking mode in terminal return '\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l'; } setDragThreshold(threshold) { this.dragThreshold = Math.max(1, Math.min(threshold, 50)); } setDoubleClickThreshold(threshold) { this.doubleClickThreshold = Math.max(50, Math.min(threshold, 2000)); } }