diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2025-10-05 16:42:02 -0700 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2025-10-05 23:11:41 -0700 |
| commit | de43eb05d2e43ab31effce3dcca62ad91a556b26 (patch) | |
| tree | 47a62b61bfc97dda639dea70627ecf3005ba7b02 /src/utils | |
| parent | 35add63ec4dce39710095f17abd86777de9e5b49 (diff) | |
| download | ansicolor-de43eb05d2e43ab31effce3dcca62ad91a556b26.tar.gz ansicolor-de43eb05d2e43ab31effce3dcca62ad91a556b26.zip | |
Diffstat (limited to 'src/utils')
| -rw-r--r-- | src/utils/ansi.ts | 12 | ||||
| -rw-r--r-- | src/utils/grid.ts | 83 | ||||
| -rw-r--r-- | src/utils/storage.ts | 42 |
3 files changed, 136 insertions, 1 deletions
diff --git a/src/utils/ansi.ts b/src/utils/ansi.ts index 551a365..e57c075 100644 --- a/src/utils/ansi.ts +++ b/src/utils/ansi.ts @@ -66,3 +66,15 @@ export const getStyleForAnsiColor = (color: AnsiTermColor): CSSProperties => { color: foreground ? ansiRgbToHex(foreground) : undefined, }; }; + +export const parseAnsiColorCode = (code: number): AnsiRgb | null => { + // Only handle 216 color cube (16-231) + if (code < 16 || code > 231) return null; + + const adjusted = code - 16; + const r = Math.floor(adjusted / 36) as AnsiColorVal; + const g = Math.floor((adjusted % 36) / 6) as AnsiColorVal; + const b = (adjusted % 6) as AnsiColorVal; + + return { r, g, b }; +}; diff --git a/src/utils/grid.ts b/src/utils/grid.ts index 71370da..1a2aa29 100644 --- a/src/utils/grid.ts +++ b/src/utils/grid.ts @@ -1,7 +1,88 @@ import type { AnsiTermColor, Grid } from '@/types/grid'; -import { getAnsiColorEscape, getAnsiEscapeCodeFromDiff } from './ansi'; +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, diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 0000000..ee862ae --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,42 @@ +import type { Grid } from '@/types/grid'; + +export interface SavedArt { + id: string; + name: string; + grid: Grid; + timestamp: number; +} + +const STORAGE_KEY = 'ansicolor-saved-art'; +const MAX_SAVES = 8; + +export const saveArt = (name: string, grid: Grid): void => { + const saves = getSavedArt(); + const newSave: SavedArt = { + id: Date.now().toString(), + name, + grid, + timestamp: Date.now(), + }; + + const updatedSaves = [newSave, ...saves].slice(0, MAX_SAVES); + localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedSaves)); +}; + +export const getSavedArt = (): SavedArt[] => { + const saved = localStorage.getItem(STORAGE_KEY); + if (!saved) return []; + + try { + return JSON.parse(saved); + } catch (e) { + console.error('Failed to parse saved art', e); + return []; + } +}; + +export const deleteSavedArt = (id: string): void => { + const saves = getSavedArt(); + const filtered = saves.filter(save => save.id !== id); + localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); +}; |
