import { theme, symbols } from './colors.ts'; /** * Table column alignment options */ export type TColumnAlign = 'left' | 'right' | 'center'; /** * Table column definition */ export interface ITableColumn { /** Column header text */ header: string; /** Column key in data object */ key: string; /** Column alignment (default: left) */ align?: TColumnAlign; /** Column width (auto-calculated if not specified) */ width?: number; /** Color function to apply to cell values */ color?: (value: string) => string; } /** * Box style types with colors */ export type TBoxStyle = 'default' | 'success' | 'error' | 'warning' | 'info'; /** * A simple logger class that provides consistent formatting for log messages * including support for logboxes with title, lines, and closing */ export class Logger { private currentBoxWidth: number | null = null; private currentBoxStyle: TBoxStyle = 'default'; private static instance: Logger; /** Default width to use when no width is specified */ private readonly DEFAULT_WIDTH = 60; /** * Creates a new Logger instance */ constructor() { this.currentBoxWidth = null; } /** * Get the singleton logger instance * @returns The singleton logger instance */ public static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } /** * Log a message * @param message Message to log */ public log(message: string): void { console.log(message); } /** * Log an error message (red with ✗ symbol) * @param message Error message to log */ public error(message: string): void { console.error(`${symbols.error} ${theme.error(message)}`); } /** * Log a warning message (yellow with ⚠ symbol) * @param message Warning message to log */ public warn(message: string): void { console.warn(`${symbols.warning} ${theme.warning(message)}`); } /** * Log a success message (green with ✓ symbol) * @param message Success message to log */ public success(message: string): void { console.log(`${symbols.success} ${theme.success(message)}`); } /** * Log an info message (cyan with ℹ symbol) * @param message Info message to log */ public info(message: string): void { console.log(`${symbols.info} ${theme.info(message)}`); } /** * Log a dim/secondary message * @param message Message to log in dim style */ public dim(message: string): void { console.log(theme.dim(message)); } /** * Log a highlighted/bold message * @param message Message to highlight */ public highlight(message: string): void { console.log(theme.highlight(message)); } /** * Get color function for box based on style */ private getBoxColor(style: TBoxStyle): (text: string) => string { switch (style) { case 'success': return theme.borderSuccess; case 'error': return theme.borderError; case 'warning': return theme.borderWarning; case 'info': return theme.borderInfo; case 'default': default: return theme.borderDefault; } } /** * Log a logbox title and set the current box width * @param title Title of the logbox * @param width Width of the logbox (including borders), defaults to DEFAULT_WIDTH * @param style Box style for coloring (default, success, error, warning, info) */ public logBoxTitle(title: string, width?: number, style?: TBoxStyle): void { this.currentBoxWidth = width || this.DEFAULT_WIDTH; this.currentBoxStyle = style || 'default'; const colorFn = this.getBoxColor(this.currentBoxStyle); // Create the title line with appropriate padding const paddedTitle = ` ${title} `; const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length; // Title line: ┌─ Title ───┐ const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}┐`; console.log(colorFn(titleLine)); } /** * Log a logbox line * @param content Content of the line * @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH. */ public logBoxLine(content: string, width?: number): void { if (!this.currentBoxWidth && !width) { // No current width and no width provided, use default width this.logBoxTitle('', this.DEFAULT_WIDTH); } const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; const colorFn = this.getBoxColor(this.currentBoxStyle); // Calculate the available space for content (use visible length) const availableSpace = boxWidth - 2; // Account for left and right borders const visibleLen = this.visibleLength(content); if (visibleLen <= availableSpace - 1) { // If content fits with at least one space for the right border stripe const padding = availableSpace - visibleLen - 1; const line = `│ ${content}${' '.repeat(padding)}│`; console.log(colorFn(line)); } else { // Content is too long, let it flow out of boundaries. const line = `│ ${content}`; console.log(colorFn(line)); } } /** * Log a logbox end * @param width Optional width override. If not provided, uses the current box width or DEFAULT_WIDTH. */ public logBoxEnd(width?: number): void { const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH; const colorFn = this.getBoxColor(this.currentBoxStyle); // Create the bottom border: └────────┘ const bottomLine = `└${'─'.repeat(boxWidth - 2)}┘`; console.log(colorFn(bottomLine)); // Reset the current box width and style this.currentBoxWidth = null; this.currentBoxStyle = 'default'; } /** * Log a complete logbox with title, content lines, and ending * @param title Title of the logbox * @param lines Array of content lines * @param width Width of the logbox, defaults to DEFAULT_WIDTH * @param style Box style for coloring */ public logBox(title: string, lines: string[], width?: number, style?: TBoxStyle): void { this.logBoxTitle(title, width || this.DEFAULT_WIDTH, style); for (const line of lines) { this.logBoxLine(line); } this.logBoxEnd(); } /** * Log a divider line * @param width Width of the divider, defaults to DEFAULT_WIDTH * @param character Character to use for the divider (default: ─) */ public logDivider(width?: number, character: string = '─'): void { console.log(character.repeat(width || this.DEFAULT_WIDTH)); } /** * Strip ANSI color codes from string for accurate length calculation */ private stripAnsi(text: string): string { // Remove ANSI escape codes return text.replace(/\x1b\[[0-9;]*m/g, ''); } /** * Get visible length of string (excluding ANSI codes) */ private visibleLength(text: string): number { return this.stripAnsi(text).length; } /** * Align text within a column (handles ANSI color codes correctly) */ private alignText(text: string, width: number, align: TColumnAlign = 'left'): string { const visibleLen = this.visibleLength(text); if (visibleLen >= width) { // Text is too long, truncate the visible part const stripped = this.stripAnsi(text); return stripped.substring(0, width); } const padding = width - visibleLen; switch (align) { case 'right': return ' '.repeat(padding) + text; case 'center': { const leftPad = Math.floor(padding / 2); const rightPad = padding - leftPad; return ' '.repeat(leftPad) + text + ' '.repeat(rightPad); } case 'left': default: return text + ' '.repeat(padding); } } /** * Log a formatted table * @param columns Column definitions * @param rows Array of data objects * @param title Optional table title */ public logTable(columns: ITableColumn[], rows: Record[], title?: string): void { if (rows.length === 0) { this.dim('No data to display'); return; } // Calculate column widths const columnWidths = columns.map((col) => { if (col.width) return col.width; // Auto-calculate width based on header and data (use visible length) let maxWidth = this.visibleLength(col.header); for (const row of rows) { const value = String(row[col.key] || ''); maxWidth = Math.max(maxWidth, this.visibleLength(value)); } return maxWidth; }); // Calculate total table width const totalWidth = columnWidths.reduce((sum, w) => sum + w, 0) + (columns.length * 3) + 1; // Print title if provided if (title) { this.logBoxTitle(title, totalWidth); } else { // Print top border console.log('┌' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┬') + '┐'); } // Print header row const headerCells = columns.map((col, i) => theme.highlight(this.alignText(col.header, columnWidths[i], col.align)) ); console.log('│ ' + headerCells.join(' │ ') + ' │'); // Print separator console.log('├' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┼') + '┤'); // Print data rows for (const row of rows) { const cells = columns.map((col, i) => { const value = String(row[col.key] || ''); const aligned = this.alignText(value, columnWidths[i], col.align); return col.color ? col.color(aligned) : aligned; }); console.log('│ ' + cells.join(' │ ') + ' │'); } // Print bottom border console.log('└' + columnWidths.map((w) => '─'.repeat(w + 2)).join('┴') + '┘'); } } // Export a singleton instance for easy use export const logger = Logger.getInstance();