interm-mcp
Version:
MCP server for terminal applications and TUI automation with 127 tools
133 lines (132 loc) • 5.74 kB
JavaScript
import sharp from 'sharp';
import { createTerminalError } from './utils/error-utils.js';
export class TerminalScreenshot {
static DEFAULT_THEME = {
dark: {
background: '#1e1e1e',
foreground: '#d4d4d4',
colors: {
black: '#000000',
red: '#cd3131',
green: '#0dbc79',
yellow: '#e5e510',
blue: '#2472c8',
magenta: '#bc3fbc',
cyan: '#11a8cd',
white: '#e5e5e5',
brightBlack: '#666666',
brightRed: '#f14c4c',
brightGreen: '#23d18b',
brightYellow: '#f5f543',
brightBlue: '#3b8eea',
brightMagenta: '#d670d6',
brightCyan: '#29b8db',
brightWhite: '#e5e5e5'
}
},
light: {
background: '#ffffff',
foreground: '#383a42',
colors: {
black: '#383a42',
red: '#e45649',
green: '#50a14f',
yellow: '#c18401',
blue: '#0184bc',
magenta: '#a626a4',
cyan: '#0997b3',
white: '#fafafa',
brightBlack: '#4f525e',
brightRed: '#e45649',
brightGreen: '#50a14f',
brightYellow: '#c18401',
brightBlue: '#0184bc',
brightMagenta: '#a626a4',
brightCyan: '#0997b3',
brightWhite: '#ffffff'
}
}
};
static async captureTerminal(terminalState, options = {}) {
try {
const { format = 'png', quality = 90, fontSize = 14, fontFamily = 'monospace', theme = 'dark', background } = options;
const selectedTheme = this.DEFAULT_THEME[theme];
const bgColor = background || selectedTheme.background;
// Calculate image dimensions based on terminal size
const charWidth = Math.ceil(fontSize * 0.6); // Approximate monospace character width
const lineHeight = Math.ceil(fontSize * 1.2); // Line height with spacing
const width = terminalState.dimensions.cols * charWidth + 40; // Add padding
const height = terminalState.dimensions.rows * lineHeight + 40;
// Create SVG representation of terminal content
const svg = this.createTerminalSVG(terminalState, {
width,
height,
fontSize,
fontFamily,
theme: selectedTheme,
background: bgColor
});
// Convert SVG to image using Sharp
let image = sharp(Buffer.from(svg))
.resize(width, height);
if (format === 'jpeg') {
image = image.jpeg({ quality });
}
else {
image = image.png();
}
return await image.toBuffer();
}
catch (error) {
throw createTerminalError('SCREENSHOT_ERROR', `Failed to capture terminal screenshot: ${error}`);
}
}
static createTerminalSVG(terminalState, options) {
const { width, height, fontSize, fontFamily, theme, background } = options;
const charWidth = Math.ceil(fontSize * 0.6);
const lineHeight = Math.ceil(fontSize * 1.2);
const padding = 20;
// Split content into lines
const lines = terminalState.content.split('\n');
const maxLines = Math.min(lines.length, terminalState.dimensions.rows);
let textElements = '';
for (let i = 0; i < maxLines; i++) {
const line = lines[i] || '';
const y = padding + (i + 1) * lineHeight;
// Simple text rendering - in a real implementation, you'd parse ANSI codes
const escapedLine = this.escapeXml(line);
textElements += `<text x="${padding}" y="${y}" fill="${theme.foreground}" font-size="${fontSize}" font-family="${fontFamily}">${escapedLine}</text>\n`;
}
// Add cursor if visible
if (terminalState.cursor.visible) {
const cursorX = padding + terminalState.cursor.x * charWidth;
const cursorY = padding + terminalState.cursor.y * lineHeight;
textElements += `<rect x="${cursorX}" y="${cursorY - fontSize}" width="${charWidth}" height="${lineHeight}" fill="${theme.foreground}" opacity="0.7"/>\n`;
}
return `<?xml version="1.0" encoding="UTF-8"?>
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<rect width="${width}" height="${height}" fill="${background}"/>
${textElements}
</svg>`;
}
static escapeXml(text) {
// First, remove ANSI escape sequences that cause XML parsing issues
const cleanText = this.stripAnsiSequences(text);
return cleanText
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
// Remove any remaining control characters that could corrupt XML
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
}
static stripAnsiSequences(text) {
// Remove ANSI escape sequences: ESC[...m (colors), ESC[...J (clear), ESC[...H (cursor), etc.
return text
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '') // Standard ANSI escape sequences
.replace(/\x1b\][0-9]*;[^\x07]*\x07/g, '') // OSC sequences
.replace(/\x1b[PX^_][^\x1b]*\x1b\\/g, '') // String terminators
.replace(/\x1b./g, ''); // Any remaining escape sequences
}
}