import type { AnsiTermColor, Grid } from '@/types/grid'; import { getAnsiColorEscape, getAnsiEscapeCodeFromDiff, parseAnsiColorCode } from './ansi'; const defaultColor: AnsiTermColor = { foreground: null, background: null }; export const gridFromAnsi = (ansiText: string): Grid => { const lines = ansiText.split('\n'); const grid: Grid = []; for (let y = 0; y < lines.length; y++) { const line = lines[y]; const row: Grid[0] = []; let x = 0; let currentColor: AnsiTermColor = { ...defaultColor }; // Regex to match ANSI escape codes const ansiRegex = /\x1b\[([0-9;]+)m/g; let lastIndex = 0; let match; while ((match = ansiRegex.exec(line)) !== null) { // Add characters before this escape code const text = line.slice(lastIndex, match.index); for (const char of text) { row.push({ char, color: { ...currentColor }, x: x++, y }); } // Parse escape code const codes = match[1].split(';').map(Number); let i = 0; while (i < codes.length) { const code = codes[i]; if (code === 38 && codes[i + 1] === 5) { // Foreground color: ESC[38;5;{code}m const colorCode = codes[i + 2]; currentColor.foreground = parseAnsiColorCode(colorCode); i += 3; } else if (code === 48 && codes[i + 1] === 5) { // Background color: ESC[48;5;{code}m const colorCode = codes[i + 2]; currentColor.background = parseAnsiColorCode(colorCode); i += 3; } else if (code === 39) { // Reset foreground currentColor.foreground = null; i++; } else if (code === 49) { // Reset background currentColor.background = null; i++; } else { i++; } } lastIndex = ansiRegex.lastIndex; } // Add remaining characters const remainingText = line.slice(lastIndex); for (const char of remainingText) { row.push({ char, color: { ...currentColor }, x: x++, y }); } grid.push(row); } // Normalize grid width const maxWidth = Math.max(...grid.map(row => row.length)); for (let y = 0; y < grid.length; y++) { while (grid[y].length < maxWidth) { const x = grid[y].length; grid[y].push({ char: ' ', color: { ...defaultColor }, x, y, }); } } return grid; }; export const gridFromAscii = ( ascii: string, color: AnsiTermColor = defaultColor, ): Grid => { const lineWidth = Math.max(...ascii.split('\n').map((line) => line.length)); return ascii.split('\n').map((line, y) => line .split('') .map((char, x) => ({ char, color, x, y, })) .concat( Array(lineWidth - line.length) .fill(0) .map((_, x) => ({ char: ' ', color, x: x + line.length, y, })), ), ); }; export const gridToAnsi = (grid: Grid) => { const reset: AnsiTermColor = { foreground: null, background: null }; const { fg, bg } = getAnsiColorEscape(reset); const resetCode = `${fg}${bg}`; const rows = []; for (let y = 0; y < grid.length; y++) { let row = ''; for (let x = 0; x < grid[y].length; x++) { const cell = grid[y][x]; const previousColor = x > 0 ? grid[y][x - 1].color : reset; row += getAnsiEscapeCodeFromDiff(previousColor, cell.color) + cell.char; } rows.push(row); } return resetCode + rows.join(resetCode + '\n'); };