UNPKG

@emmahyde/thinking-patterns

Version:

MCP server combining systematic thinking, mental models, debugging approaches, and stochastic algorithms for comprehensive cognitive pattern support

220 lines (219 loc) 7.68 kB
/** * UI Utilities for consistent formatting and display * Provides shared utilities for creating boxes, headers, and sections * * Modernized implementation using boxen + ecosystem packages */ import stringWidth from 'string-width'; import wrapAnsi from 'wrap-ansi'; // ANSI color codes for console output export const Colors = { reset: '\x1b[0m', bright: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m', }; /** * Box drawing characters for consistent borders */ export const BoxChars = { horizontal: '─', vertical: '│', topLeft: '┌', topRight: '┐', bottomLeft: '└', bottomRight: '┘', tee: '├', teeRight: '┤', cross: '┼', }; export const DEFAULT_BOX_CONFIG = { padding: 1, minWidth: 40, maxWidth: 100, borderColor: Colors.cyan, headerColor: Colors.bright + Colors.white, sectionColor: Colors.yellow, }; /** * Calculate the display width of a string, accounting for ANSI escape sequences and emojis */ export function getDisplayWidth(text) { return stringWidth(text); } /** * Pad a string to the specified width, accounting for display width */ function padToWidth(text, width, char = ' ') { const displayWidth = getDisplayWidth(text); const padding = Math.max(0, width - displayWidth); return text + char.repeat(padding); } /** * Wrap text to fit within specified width */ function wrapText(text, width) { if (getDisplayWidth(text) <= width) { return [text]; } return wrapAnsi(text, width, { hard: true }).split('\n'); } /** * Format a header with optional emoji and consistent styling */ export function formatHeader(text, emoji, config = {}) { const fullConfig = { ...DEFAULT_BOX_CONFIG, ...config }; const headerText = emoji ? `${emoji} ${text}` : text; return `${fullConfig.headerColor}${headerText}${Colors.reset}`; } /** * Format a section with title and content */ export function formatSection(title, content, config = {}) { const fullConfig = { ...DEFAULT_BOX_CONFIG, ...config }; const lines = []; // Add section title lines.push(`${fullConfig.sectionColor}${title}:${Colors.reset}`); // Process content const contentArray = Array.isArray(content) ? content : [content]; for (const item of contentArray) { if (typeof item === 'string') { const wrappedLines = wrapText(item, fullConfig.maxWidth - fullConfig.padding * 2 - 2); for (const line of wrappedLines) { lines.push(` ${line}`); } } } return lines.join('\n'); } /** * Create a formatted box with title and sections */ export function boxed(title, sections, config = {}) { const fullConfig = { ...DEFAULT_BOX_CONFIG, ...config }; const lines = []; // Collect all content to calculate box width const allContent = []; allContent.push(title); // Process sections and collect content const sectionLines = []; for (const [sectionTitle, sectionContent] of Object.entries(sections)) { const formattedSection = formatSection(sectionTitle, sectionContent, config); const sectionTextLines = formattedSection.split('\n'); sectionLines.push(...sectionTextLines); // Add to content for width calculation (without colors) for (const line of sectionTextLines) { allContent.push(line.replace(/\x1b\[[0-9;]*m/g, '')); } } // Calculate optimal box width const maxContentWidth = Math.max(...allContent.map(getDisplayWidth)); const boxWidth = Math.min(Math.max(maxContentWidth + fullConfig.padding * 2, fullConfig.minWidth), fullConfig.maxWidth); const innerWidth = boxWidth - 2; // Subtract border characters // Build the box const borderColor = fullConfig.borderColor; const reset = Colors.reset; // Top border lines.push(`${borderColor}${BoxChars.topLeft}${BoxChars.horizontal.repeat(innerWidth)}${BoxChars.topRight}${reset}`); // Title line const paddedTitle = padToWidth(` ${formatHeader(title)} `, innerWidth); lines.push(`${borderColor}${BoxChars.vertical}${reset}${paddedTitle}${borderColor}${BoxChars.vertical}${reset}`); // Separator after title if there are sections if (Object.keys(sections).length > 0) { lines.push(`${borderColor}${BoxChars.tee}${BoxChars.horizontal.repeat(innerWidth)}${BoxChars.teeRight}${reset}`); } // Content sections for (const line of sectionLines) { const paddedLine = padToWidth(` ${line} `, innerWidth); lines.push(`${borderColor}${BoxChars.vertical}${reset}${paddedLine}${borderColor}${BoxChars.vertical}${reset}`); } // Add padding line if content exists if (sectionLines.length > 0) { const emptyLine = padToWidth(' ', innerWidth); lines.push(`${borderColor}${BoxChars.vertical}${reset}${emptyLine}${borderColor}${BoxChars.vertical}${reset}`); } // Bottom border lines.push(`${borderColor}${BoxChars.bottomLeft}${BoxChars.horizontal.repeat(innerWidth)}${BoxChars.bottomRight}${reset}`); return lines.join('\n'); } /** * Create a simple border around text */ export function bordered(text, config = {}) { const fullConfig = { ...DEFAULT_BOX_CONFIG, ...config }; const lines = text.split('\n'); const maxWidth = Math.max(...lines.map(getDisplayWidth)); const boxWidth = Math.min(Math.max(maxWidth + fullConfig.padding * 2, fullConfig.minWidth), fullConfig.maxWidth); const result = []; const borderColor = fullConfig.borderColor; const reset = Colors.reset; const innerWidth = boxWidth - 2; // Top border result.push(`${borderColor}${BoxChars.topLeft}${BoxChars.horizontal.repeat(innerWidth)}${BoxChars.topRight}${reset}`); // Content lines for (const line of lines) { const paddedLine = padToWidth(` ${line} `, innerWidth); result.push(`${borderColor}${BoxChars.vertical}${reset}${paddedLine}${borderColor}${BoxChars.vertical}${reset}`); } // Bottom border result.push(`${borderColor}${BoxChars.bottomLeft}${BoxChars.horizontal.repeat(innerWidth)}${BoxChars.bottomRight}${reset}`); return result.join('\n'); } /** * Create a simple divider line */ export function divider(width = 80, char = BoxChars.horizontal) { return char.repeat(width); } /** * Color themes for different types of output */ export const Themes = { success: { borderColor: Colors.green, headerColor: Colors.bright + Colors.green, sectionColor: Colors.green, }, error: { borderColor: Colors.red, headerColor: Colors.bright + Colors.red, sectionColor: Colors.red, }, warning: { borderColor: Colors.yellow, headerColor: Colors.bright + Colors.yellow, sectionColor: Colors.yellow, }, info: { borderColor: Colors.blue, headerColor: Colors.bright + Colors.blue, sectionColor: Colors.blue, }, subtle: { borderColor: Colors.gray, headerColor: Colors.white, sectionColor: Colors.gray, }, }; /** * Convenience functions for themed boxes */ export function successBox(title, sections) { return boxed(title, sections, Themes.success); } export function errorBox(title, sections) { return boxed(title, sections, Themes.error); } export function warningBox(title, sections) { return boxed(title, sections, Themes.warning); } export function infoBox(title, sections) { return boxed(title, sections, Themes.info); }