// ============================================================================ // MOUSE SUPPORT FOR TERMINAL UI // SGR Mouse Protocol implementation for accurate mouse tracking // ============================================================================ // ============================================================================ // MOUSE EVENT TYPES // ============================================================================ export interface MouseEvent { x: number // Terminal column (0-based) y: number // Terminal row (0-based) button: MouseButton action: MouseAction modifiers: MouseModifiers } export enum MouseButton { LEFT = 0, MIDDLE = 1, RIGHT = 2, WHEEL_UP = 64, WHEEL_DOWN = 65, NONE = -1, } export enum MouseAction { PRESS = 0, RELEASE = 1, MOVE = 2, DRAG = 3, } export interface MouseModifiers { shift: boolean ctrl: boolean alt: boolean meta: boolean } // ============================================================================ // HIT TESTING // ============================================================================ export class HitGrid { private grid: Int16Array // -1 = empty, >= 0 = component index private width: number private height: number constructor(width: number, height: number) { this.width = width this.height = height this.grid = new Int16Array(width * height).fill(-1) } /** * Set a component index at a position */ set(x: number, y: number, componentIndex: number): void { if (x < 0 || x >= this.width || y < 0 || y >= this.height) return this.grid[y * this.width + x] = componentIndex } /** * Get component index at a position */ get(x: number, y: number): number { if (x < 0 || x >= this.width || y < 0 || y >= this.height) return -1 return this.grid[y * this.width + x] ?? -1 } /** * Fill a rectangle with a component index */ fillRect( x: number, y: number, width: number, height: number, componentIndex: number ): void { const x1 = Math.max(0, x) const y1 = Math.max(0, y) const x2 = Math.min(this.width, x + width) const y2 = Math.min(this.height, y + height) for (let row = y1; row < y2; row++) { const offset = row * this.width for (let col = x1; col < x2; col++) { this.grid[offset + col] = componentIndex } } } /** * Clear the grid */ clear(): void { this.grid.fill(-1) } /** * Resize the grid */ resize(width: number, height: number): void { if (width === this.width && height === this.height) return const newGrid = new Int16Array(width * height).fill(-1) // Copy existing data if possible const copyWidth = Math.min(this.width, width) const copyHeight = Math.min(this.height, height) for (let y = 0; y < copyHeight; y++) { const oldOffset = y * this.width const newOffset = y * width for (let x = 0; x < copyWidth; x++) { newGrid[newOffset + x] = this.grid[oldOffset + x] ?? -1 } } this.grid = newGrid this.width = width this.height = height } } // ============================================================================ // SGR MOUSE PROTOCOL // ============================================================================ export class SGRMouseProtocol { private enabled = false private handlers = new Map void>() /** * Enable SGR mouse protocol * This sends the appropriate escape sequences to the terminal */ enable(stream: NodeJS.WriteStream = process.stdout): void { if (this.enabled) return // Enable SGR extended mouse mode (1006) // Enable mouse button tracking (1002) - buttons and motion while pressed // Enable any-event mouse tracking (1003) - all mouse events including motion stream.write('\x1b[?1006h') // SGR mode stream.write('\x1b[?1002h') // Button tracking stream.write('\x1b[?1003h') // Any-event tracking this.enabled = true } /** * Disable SGR mouse protocol */ disable(stream: NodeJS.WriteStream = process.stdout): void { if (!this.enabled) return stream.write('\x1b[?1006l') // Disable SGR mode stream.write('\x1b[?1002l') // Disable button tracking stream.write('\x1b[?1003l') // Disable any-event tracking this.enabled = false } /** * Parse SGR mouse sequence * Format: \x1b[