@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
JavaScript
/**
* 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);
}