// Shared utilities used by TUI components export interface TextSegment { text: string; color?: string; background?: string; bold?: boolean; dim?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; overline?: boolean; // For inline icons icon?: string; iconSize?: number; // For inline clickable text href?: string; action?: () => void; } // Color maps for each theme export type ThemeColorMap = Record; // Default color map (fallback) - hex values as defaults, overwritten by theme colorMap at runtime export const defaultColorMap: ThemeColorMap = { // Basic colors 'red': '#f38ba8', 'green': '#a6e3a1', 'yellow': '#f9e2af', 'blue': '#89b4fa', 'magenta': '#cba6f7', 'cyan': '#94e2d5', 'white': '#cdd6f4', 'gray': '#6c7086', 'orange': '#fab387', 'pink': '#f5c2e7', 'black': '#1e1e2e', 'surface': '#313244', // Extended colors 'teal': '#94e2d5', 'sky': '#89dceb', 'sapphire': '#74c7ec', 'lavender': '#b4befe', 'peach': '#fab387', 'maroon': '#eba0ac', 'mauve': '#cba6f7', 'flamingo': '#f2cdcd', 'rosewater': '#f5e0dc', // Semantic colors 'primary': '#cba6f7', 'accent': '#a6e3a1', 'muted': '#6c7086', 'error': '#f38ba8', 'success': '#a6e3a1', 'warning': '#f9e2af', 'info': '#89b4fa', // Theme colors 'text': '#cdd6f4', 'textMuted': '#a6adc8', 'background': '#1e1e2e', 'backgroundLight': '#313244', 'border': '#45475a', 'terminal': '#1e1e2e', 'terminalPrompt': '#cba6f7', 'terminalUser': '#a6e3a1', 'terminalPath': '#89b4fa', }; // Legacy alias for backwards compatibility export const colorMap = defaultColorMap; // Text style keywords const textStyles = ['bold', 'dim', 'italic', 'underline', 'strikethrough', 'overline']; export function parseColorText(text: string, colors: ThemeColorMap = colorMap): TextSegment[] { const segments: TextSegment[] = []; // Match (&icon, iconName) patterns FIRST, then (&specs)content(&) // This prevents (&icon, ...) from being consumed as the start of a color block const regex = /\(&icon,\s*([^)]+)\)|\(&([^)]+)\)(.*?)\(&\)/g; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = regex.exec(text)) !== null) { if (match.index > lastIndex) { segments.push({ text: text.slice(lastIndex, match.index) }); } // Check if this is an icon match (Group 1 is the icon name) if (match[1]) { const iconName = match[1].trim(); segments.push({ text: '', icon: iconName }); lastIndex = match.index + match[0].length; continue; } // Standard text formatting (Group 2 is specs, Group 3 is content) const specs = match[2].split(',').map(s => s.trim().toLowerCase()); const content = match[3]; const segment: TextSegment = { text: content }; for (const spec of specs) { // Text styles if (spec === 'bold') segment.bold = true; else if (spec === 'dim') segment.dim = true; else if (spec === 'italic') segment.italic = true; else if (spec === 'underline') segment.underline = true; else if (spec === 'strikethrough' || spec === 'strike') segment.strikethrough = true; else if (spec === 'overline') segment.overline = true; // Background color (bg-colorname or bg-#hex) else if (spec.startsWith('bg-')) { const bgColor = spec.slice(3); if (colors[bgColor]) { segment.background = colors[bgColor]; } else if (bgColor.startsWith('#')) { segment.background = bgColor; } } // Foreground color else if (colors[spec] && !textStyles.includes(spec)) { segment.color = colors[spec]; } else if (spec.startsWith('#')) { segment.color = spec; } } segments.push(segment); lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { segments.push({ text: text.slice(lastIndex) }); } if (segments.length === 0) { segments.push({ text }); } return segments; } // Get plain text from segments (for length calculation) export function getPlainText(segments: TextSegment[]): string { return segments.map(s => s.text).join(''); } // Get segments up to a certain character count (for typing animation) export function getSegmentsUpToChar(segments: TextSegment[], charCount: number): TextSegment[] { const result: TextSegment[] = []; let remaining = charCount; for (const segment of segments) { if (remaining <= 0) break; if (segment.text.length <= remaining) { result.push(segment); remaining -= segment.text.length; } else { // Partial segment result.push({ ...segment, text: segment.text.slice(0, remaining) }); remaining = 0; } } return result; } export function getSegmentStyle(segment: TextSegment): string { const styles: string[] = []; if (segment.color) styles.push(`color: ${segment.color}`); if (segment.background) styles.push(`background-color: ${segment.background}; padding: 0.1em 0.25em; border-radius: 3px`); if (segment.bold) styles.push('font-weight: bold'); if (segment.dim) styles.push('opacity: 0.6'); if (segment.italic) styles.push('font-style: italic'); // Combine text decorations const decorations: string[] = []; if (segment.underline) decorations.push('underline'); if (segment.strikethrough) decorations.push('line-through'); if (segment.overline) decorations.push('overline'); if (decorations.length > 0) { styles.push(`text-decoration: ${decorations.join(' ')}`); } return styles.join('; '); } export function getLinePrefix(type: string): string { switch (type) { case 'error': return '✗ '; case 'success': return '✓ '; case 'warning': return '⚠ '; case 'info': return '› '; default: return ''; } } export function getButtonStyle(style?: string): string { switch (style) { case 'primary': return 'var(--terminal-primary)'; case 'accent': return 'var(--terminal-accent)'; case 'warning': return '#f9e2af'; case 'error': return '#f38ba8'; default: return 'var(--terminal-text)'; } } export function parseDimension(value: string | number | undefined): string | undefined { if (value === undefined) return undefined; if (typeof value === 'number') { // If it's a decimal between 0 and 1, treat as percentage if (value > 0 && value <= 1) { return `${value * 100}%`; } return `${value}px`; } if (typeof value === 'string') { // Handle fractions like "1/2", "1/3" if (value.includes('/')) { const [num, den] = value.split('/').map(Number); if (!isNaN(num) && !isNaN(den) && den !== 0) { return `${(num / den) * 100}%`; } } // Return as is if it already has a unit or is just a number string return value; } return undefined; }