UNPKG

mcpretentious

Version:

MCPretentious - Universal Terminal MCP. High-performance terminal automation for iTerm2 (WebSocket) and tmux (control mode). Cross-platform support with cursor position, colors, and layered screenshots.

558 lines (495 loc) 16.2 kB
/** * ANSI escape sequence parser for tmux output * Converts ANSI-coded text to structured format matching iTerm2's output */ import { color256ToRGB, rgbToHex } from './color-utils.js'; // ANSI SGR (Select Graphic Rendition) codes const SGR_CODES = { RESET: 0, BOLD: 1, DIM: 2, ITALIC: 3, UNDERLINE: 4, BLINK: 5, REVERSE: 7, HIDDEN: 8, STRIKETHROUGH: 9, // Reset individual attributes RESET_BOLD_DIM: 22, RESET_ITALIC: 23, RESET_UNDERLINE: 24, RESET_BLINK: 25, RESET_REVERSE: 27, RESET_HIDDEN: 28, RESET_STRIKE: 29, // Foreground colors FG_BLACK: 30, FG_RED: 31, FG_GREEN: 32, FG_YELLOW: 33, FG_BLUE: 34, FG_MAGENTA: 35, FG_CYAN: 36, FG_WHITE: 37, FG_256: 38, FG_DEFAULT: 39, // Background colors BG_BLACK: 40, BG_RED: 41, BG_GREEN: 42, BG_YELLOW: 43, BG_BLUE: 44, BG_MAGENTA: 45, BG_CYAN: 46, BG_WHITE: 47, BG_256: 48, BG_DEFAULT: 49, // Bright foreground colors FG_BRIGHT_BLACK: 90, FG_BRIGHT_RED: 91, FG_BRIGHT_GREEN: 92, FG_BRIGHT_YELLOW: 93, FG_BRIGHT_BLUE: 94, FG_BRIGHT_MAGENTA: 95, FG_BRIGHT_CYAN: 96, FG_BRIGHT_WHITE: 97, // Bright background colors BG_BRIGHT_BLACK: 100, BG_BRIGHT_RED: 101, BG_BRIGHT_GREEN: 102, BG_BRIGHT_YELLOW: 103, BG_BRIGHT_BLUE: 104, BG_BRIGHT_MAGENTA: 105, BG_BRIGHT_CYAN: 106, BG_BRIGHT_WHITE: 107 }; // Standard 16-color palette (matches iTerm2 defaults) const STANDARD_COLORS = { // Normal colors 30: { r: 0, g: 0, b: 0 }, // Black 31: { r: 194, g: 54, b: 33 }, // Red 32: { r: 37, g: 188, b: 36 }, // Green 33: { r: 173, g: 173, b: 39 }, // Yellow 34: { r: 73, g: 46, b: 225 }, // Blue 35: { r: 211, g: 56, b: 211 }, // Magenta 36: { r: 51, g: 187, b: 200 }, // Cyan 37: { r: 203, g: 204, b: 205 }, // White // Bright colors 90: { r: 129, g: 131, b: 131 }, // Bright Black 91: { r: 252, g: 57, b: 31 }, // Bright Red 92: { r: 49, g: 231, b: 34 }, // Bright Green 93: { r: 234, g: 236, b: 35 }, // Bright Yellow 94: { r: 88, g: 51, b: 255 }, // Bright Blue 95: { r: 249, g: 53, b: 248 }, // Bright Magenta 96: { r: 20, g: 240, b: 240 }, // Bright Cyan 97: { r: 233, g: 235, b: 235 } // Bright White }; /** * Parse a single ANSI escape sequence * @param {string} sequence - The escape sequence (without ESC[) * @returns {Object} Parsed attributes */ function parseSGR(sequence) { const codes = sequence.replace('m', '').split(';').map(Number); const attrs = {}; for (let i = 0; i < codes.length; i++) { const code = codes[i]; switch (code) { case SGR_CODES.RESET: attrs.reset = true; break; case SGR_CODES.BOLD: attrs.bold = true; break; case SGR_CODES.ITALIC: attrs.italic = true; break; case SGR_CODES.UNDERLINE: attrs.underline = true; break; case SGR_CODES.RESET_BOLD_DIM: attrs.bold = false; break; case SGR_CODES.RESET_ITALIC: attrs.italic = false; break; case SGR_CODES.RESET_UNDERLINE: attrs.underline = false; break; // Standard foreground colors case SGR_CODES.FG_BLACK: case SGR_CODES.FG_RED: case SGR_CODES.FG_GREEN: case SGR_CODES.FG_YELLOW: case SGR_CODES.FG_BLUE: case SGR_CODES.FG_MAGENTA: case SGR_CODES.FG_CYAN: case SGR_CODES.FG_WHITE: attrs.fgColor = STANDARD_COLORS[code]; attrs.fgStandard = code - 30; break; // Bright foreground colors case SGR_CODES.FG_BRIGHT_BLACK: case SGR_CODES.FG_BRIGHT_RED: case SGR_CODES.FG_BRIGHT_GREEN: case SGR_CODES.FG_BRIGHT_YELLOW: case SGR_CODES.FG_BRIGHT_BLUE: case SGR_CODES.FG_BRIGHT_MAGENTA: case SGR_CODES.FG_BRIGHT_CYAN: case SGR_CODES.FG_BRIGHT_WHITE: attrs.fgColor = STANDARD_COLORS[code]; attrs.fgStandard = code - 90 + 8; break; // Standard background colors case SGR_CODES.BG_BLACK: case SGR_CODES.BG_RED: case SGR_CODES.BG_GREEN: case SGR_CODES.BG_YELLOW: case SGR_CODES.BG_BLUE: case SGR_CODES.BG_MAGENTA: case SGR_CODES.BG_CYAN: case SGR_CODES.BG_WHITE: attrs.bgColor = STANDARD_COLORS[code - 10]; attrs.bgStandard = code - 40; break; // Bright background colors case SGR_CODES.BG_BRIGHT_BLACK: case SGR_CODES.BG_BRIGHT_RED: case SGR_CODES.BG_BRIGHT_GREEN: case SGR_CODES.BG_BRIGHT_YELLOW: case SGR_CODES.BG_BRIGHT_BLUE: case SGR_CODES.BG_BRIGHT_MAGENTA: case SGR_CODES.BG_BRIGHT_CYAN: case SGR_CODES.BG_BRIGHT_WHITE: attrs.bgColor = STANDARD_COLORS[code - 10]; attrs.bgStandard = code - 100 + 8; break; // 256-color mode case SGR_CODES.FG_256: if (codes[i + 1] === 5 && i + 2 < codes.length) { const colorIndex = codes[i + 2]; attrs.fgColor = color256ToRGB(colorIndex); attrs.fgStandard = colorIndex; i += 2; } else if (codes[i + 1] === 2 && i + 4 < codes.length) { // RGB color attrs.fgColor = { r: codes[i + 2], g: codes[i + 3], b: codes[i + 4] }; i += 4; } break; case SGR_CODES.BG_256: if (codes[i + 1] === 5 && i + 2 < codes.length) { const colorIndex = codes[i + 2]; attrs.bgColor = color256ToRGB(colorIndex); attrs.bgStandard = colorIndex; i += 2; } else if (codes[i + 1] === 2 && i + 4 < codes.length) { // RGB color attrs.bgColor = { r: codes[i + 2], g: codes[i + 3], b: codes[i + 4] }; i += 4; } break; case SGR_CODES.FG_DEFAULT: attrs.fgColor = null; attrs.fgAlternate = 'DEFAULT'; break; case SGR_CODES.BG_DEFAULT: attrs.bgColor = null; attrs.bgAlternate = 'DEFAULT'; break; } } return attrs; } /** * Parse a line with ANSI escape codes * @param {string} line - Line with ANSI codes * @returns {Object} Parsed line with text and style runs */ function parseAnsiLine(line) { const result = { text: '', style: [] }; // Current style state let currentStyle = { bold: false, italic: false, underline: false, fgColor: null, bgColor: null, fgStandard: undefined, bgStandard: undefined }; // Split by ANSI escape sequences const parts = line.split(/\x1b\[([0-9;]+m)/); for (let i = 0; i < parts.length; i++) { if (i % 2 === 0) { // Text content const text = parts[i]; if (text.length > 0) { result.text += text; // Create style run for this text const styleRun = { repeats: text.length }; // Add style attributes if not default if (currentStyle.bold) styleRun.bold = true; if (currentStyle.italic) styleRun.italic = true; if (currentStyle.underline) styleRun.underline = true; if (currentStyle.fgColor) { if (currentStyle.fgStandard !== undefined) { styleRun.fgStandard = currentStyle.fgStandard; } else { styleRun.fgRgb = { red: currentStyle.fgColor.r, green: currentStyle.fgColor.g, blue: currentStyle.fgColor.b }; } } else if (currentStyle.fgAlternate) { styleRun.fgAlternate = currentStyle.fgAlternate; } if (currentStyle.bgColor) { if (currentStyle.bgStandard !== undefined) { styleRun.bgStandard = currentStyle.bgStandard; } else { styleRun.bgRgb = { red: currentStyle.bgColor.r, green: currentStyle.bgColor.g, blue: currentStyle.bgColor.b }; } } else if (currentStyle.bgAlternate) { styleRun.bgAlternate = currentStyle.bgAlternate; } result.style.push(styleRun); } } else { // ANSI escape sequence const attrs = parseSGR(parts[i]); if (attrs.reset) { // Reset all attributes currentStyle = { bold: false, italic: false, underline: false, fgColor: null, bgColor: null, fgStandard: undefined, bgStandard: undefined }; } else { // Apply attribute changes if (attrs.bold !== undefined) currentStyle.bold = attrs.bold; if (attrs.italic !== undefined) currentStyle.italic = attrs.italic; if (attrs.underline !== undefined) currentStyle.underline = attrs.underline; if (attrs.fgColor !== undefined) { currentStyle.fgColor = attrs.fgColor; currentStyle.fgStandard = attrs.fgStandard; currentStyle.fgAlternate = attrs.fgAlternate; } if (attrs.bgColor !== undefined) { currentStyle.bgColor = attrs.bgColor; currentStyle.bgStandard = attrs.bgStandard; currentStyle.bgAlternate = attrs.bgAlternate; } } } } // If no styles were added, add a default style run if (result.style.length === 0 && result.text.length > 0) { result.style.push({ repeats: result.text.length }); } return result; } /** * Parse entire screen with ANSI codes * @param {Array<string>} lines - Lines with ANSI codes * @param {Object} cursor - Cursor position {x, y} * @returns {Object} Parsed screen matching iTerm2 format */ export function parseAnsiScreen(lines, cursor = { x: 0, y: 0 }) { const parsedLines = lines.map(line => parseAnsiLine(line)); return { text: parsedLines.map(l => l.text).join('\n'), cursor: cursor, lines: parsedLines }; } /** * Convert parsed ANSI screen to layered format * @param {Object} screen - Parsed screen from parseAnsiScreen * @param {Array<string>} layers - Requested layers * @returns {Object} Layered screen data */ export function convertToLayers(screen, layers = ['text']) { const result = {}; // Always include text result.text = screen.lines.map(l => l.text); // Include cursor if requested if (layers.includes('cursor')) { result.cursor = screen.cursor; } // Process style layers if requested if (layers.some(l => ['fgColors', 'bgColors', 'styles', 'bold', 'italic', 'underline'].includes(l))) { const palette = { '0': null }; let paletteIndex = 1; // First pass: build color palette for (const line of screen.lines) { for (const styleRun of line.style || []) { // Process foreground colors if (styleRun.fgRgb) { const hex = rgbToHex(styleRun.fgRgb); if (!palette[hex]) { palette[hex] = String(paletteIndex++); } } else if (styleRun.fgStandard !== undefined) { const color = color256ToRGB(styleRun.fgStandard); const hex = rgbToHex(color); if (!palette[hex]) { palette[hex] = String(paletteIndex++); } } // Process background colors if (styleRun.bgRgb) { const hex = rgbToHex(styleRun.bgRgb); if (!palette[hex]) { palette[hex] = String(paletteIndex++); } } else if (styleRun.bgStandard !== undefined) { const color = color256ToRGB(styleRun.bgStandard); const hex = rgbToHex(color); if (!palette[hex]) { palette[hex] = String(paletteIndex++); } } } } // Invert palette for output const outputPalette = { '0': null }; for (const [hex, char] of Object.entries(palette)) { if (hex !== '0') { outputPalette[char] = hex; } } // Second pass: build layer arrays if (layers.includes('fgColors')) { result.fgColors = []; } if (layers.includes('bgColors')) { result.bgColors = []; } if (layers.includes('styles')) { result.styles = []; result.styleLegend = { '.': null, 'b': 'bold', 'i': 'italic', 'u': 'underline', 'I': 'bold+italic', 'U': 'bold+underline', 'J': 'italic+underline', 'X': 'bold+italic+underline' }; } if (layers.includes('bold')) { result.bold = []; } if (layers.includes('italic')) { result.italic = []; } if (layers.includes('underline')) { result.underline = []; } for (const line of screen.lines) { const lineLength = line.text.length; let fgLine = layers.includes('fgColors') ? [] : null; let bgLine = layers.includes('bgColors') ? [] : null; let styleLine = layers.includes('styles') ? [] : null; let boldLine = layers.includes('bold') ? [] : null; let italicLine = layers.includes('italic') ? [] : null; let underlineLine = layers.includes('underline') ? [] : null; let position = 0; for (const styleRun of line.style || []) { const repeats = styleRun.repeats || 1; // Determine colors let fgChar = '0'; let bgChar = '0'; if (styleRun.fgRgb) { const hex = rgbToHex(styleRun.fgRgb); fgChar = palette[hex] || '0'; } else if (styleRun.fgStandard !== undefined) { const color = color256ToRGB(styleRun.fgStandard); const hex = rgbToHex(color); fgChar = palette[hex] || '0'; } if (styleRun.bgRgb) { const hex = rgbToHex(styleRun.bgRgb); bgChar = palette[hex] || '0'; } else if (styleRun.bgStandard !== undefined) { const color = color256ToRGB(styleRun.bgStandard); const hex = rgbToHex(color); bgChar = palette[hex] || '0'; } // Determine style character let styleChar = '.'; if (styleRun.bold && styleRun.italic && styleRun.underline) { styleChar = 'X'; } else if (styleRun.bold && styleRun.italic) { styleChar = 'I'; } else if (styleRun.bold && styleRun.underline) { styleChar = 'U'; } else if (styleRun.italic && styleRun.underline) { styleChar = 'J'; } else if (styleRun.bold) { styleChar = 'b'; } else if (styleRun.italic) { styleChar = 'i'; } else if (styleRun.underline) { styleChar = 'u'; } // Fill arrays for (let i = 0; i < repeats && position < lineLength; i++) { if (fgLine) fgLine.push(fgChar); if (bgLine) bgLine.push(bgChar); if (styleLine) styleLine.push(styleChar); if (boldLine) boldLine.push(styleRun.bold ? 'X' : ' '); if (italicLine) italicLine.push(styleRun.italic ? 'X' : ' '); if (underlineLine) underlineLine.push(styleRun.underline ? 'X' : ' '); position++; } } // Pad to line length if necessary while (position < lineLength) { if (fgLine) fgLine.push('0'); if (bgLine) bgLine.push('0'); if (styleLine) styleLine.push('.'); if (boldLine) boldLine.push(' '); if (italicLine) italicLine.push(' '); if (underlineLine) underlineLine.push(' '); position++; } // Add lines to result if (fgLine) result.fgColors.push(fgLine.join('')); if (bgLine) result.bgColors.push(bgLine.join('')); if (styleLine) result.styles.push(styleLine.join('')); if (boldLine) result.bold.push(boldLine.join('')); if (italicLine) result.italic.push(italicLine.join('')); if (underlineLine) result.underline.push(underlineLine.join('')); } // Add palette if we have colors if ((layers.includes('fgColors') || layers.includes('bgColors')) && Object.keys(outputPalette).length > 1) { result.colorPalette = outputPalette; } } return result; }