Files
nupst/ts/logger.ts

334 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { symbols, theme } 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<string, string>[], 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();