Some checks failed
CI / Type Check & Lint (push) Failing after 6s
CI / Build Test (Current Platform) (push) Successful in 6s
Release / build-and-release (push) Successful in 44s
CI / Build All Platforms (push) Successful in 50s
CI / Type Check & Lint (pull_request) Failing after 5s
CI / Build Test (Current Platform) (pull_request) Successful in 5s
CI / Build All Platforms (pull_request) Successful in 49s
Major improvements: - Created color theme system (ts/colors.ts) with ANSI colors - Enhanced logger with colors, table formatting, and styled boxes - Fixed daemon exit bug - now stays running when no UPS configured - Added config hot-reload with file watcher for live updates - Beautified CLI help output with color-coded commands - Added showcase test demonstrating all output features - Fixed ANSI code handling for perfect table/box alignment Features: - Color-coded messages (success=green, error=red, warning=yellow, info=cyan) - Status symbols (●○◐◯ for running/stopped/starting/unknown) - Battery level colors (green>60%, yellow 30-60%, red<30%) - Table formatting with auto-sizing and column alignment - Styled boxes (success, error, warning, info styles) - Hot-reload: daemon watches config file and reloads automatically - Idle mode: daemon stays alive when no devices, checks periodically Daemon improvements: - No longer exits when no UPS devices configured - Enters idle monitoring loop waiting for config - File watcher detects config changes in real-time - Auto-reloads and starts monitoring when devices added - Logs warnings instead of errors for missing devices Technical fixes: - Strip ANSI codes when calculating text width for alignment - Use visible length for padding calculations in tables and boxes - Properly handle colored text in table cells and box lines Breaking changes: None (backward compatible)
334 lines
9.6 KiB
TypeScript
334 lines
9.6 KiB
TypeScript
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<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();
|