import * as readline from 'node:readline'; import { EventEmitter } from 'node:events'; import type { log, font_style } from '../types/utils/logger.ts'; // 将 font_style 转为 ANSI 参数;normal 不产生参数 function style_codes(font_style: font_style[]): number[] { const codes: number[] = []; for (const style of font_style ?? []) { switch (style) { case 'bold': codes.push(1); break; case 'light': codes.push(2); break; case 'italic': codes.push(3); break; case 'underline': case 'url': codes.push(4); break; case 'normal': default: break; } } return codes; } // 解析 #rgb / #rrggbb,失败返回 null function parse_hex_color(color: string): [number, number, number] | null { if (typeof color !== 'string') { return null; } const value = color.trim().replace(/^#/, ''); if (/^[0-9a-fA-F]{3}$/.test(value)) { return [ parseInt(value[0] + value[0], 16), parseInt(value[1] + value[1], 16), parseInt(value[2] + value[2], 16), ]; } if (/^[0-9a-fA-F]{6}$/.test(value)) { return [ parseInt(value.slice(0, 2), 16), parseInt(value.slice(2, 4), 16), parseInt(value.slice(4, 6), 16), ]; } return null; } // 将一段 log 渲染为带颜色的字符串 function to_ansi(segment: log): string { const message = segment.message === undefined ? 'undefined' : typeof segment.message === 'string' ? segment.message : String(segment.message); const codes = style_codes(segment.font_style ?? []); const rgb = parse_hex_color(segment.font_color); if (rgb !== null) { codes.push(38, 2, rgb[0], rgb[1], rgb[2]); } if (codes.length === 0) { return message; } return `\x1b[${codes.join(';')}m${message}\x1b[0m`; } export type console_segment = log | string | number | boolean | null | undefined; export type console_input = console_segment | console_segment[]; export interface _ConsolePrint { info(message: any): log; success(message: any): log; warning(message: any): log; error(message: any): log; tip(message: any): log; custom(message: any, font_color: string, font_style: font_style[]): log; log(input: console_input): void; on(event: string | symbol, listener: (...args: any[]) => void): this; once(event: string | symbol, listener: (...args: any[]) => void): this; enable_input(prompt?: string): void; disable_input(): void; set_prompt(prompt: string): void; } export class ConsolePrint extends EventEmitter implements _ConsolePrint { private rl: readline.Interface | null = null; private prompt = '> '; private input_enabled = false; // 输入行提交后是否需要重新显示提示符(write_line 已恢复时置为 false) private need_prompt = false; // 监听 input 时自动开启控制台输入 public on(event: string | symbol, listener: (...args: any[]) => void): this { if (event === 'input') { this.enable_input(); } return super.on(event, listener); } public once(event: string | symbol, listener: (...args: any[]) => void): this { if (event === 'input') { this.enable_input(); } return super.once(event, listener); } public info(message: any): log { // 白色 return { message, font_color: '#ffffff', font_style: ['normal'] }; } public success(message: any): log { // 柔和绿 return { message, font_color: '#8fb98f', font_style: ['normal'] }; } public warning(message: any): log { // 柔和沙黄 return { message, font_color: '#d1b478', font_style: ['normal'] }; } public error(message: any): log { // 柔和玫红 return { message, font_color: '#cf8e8e', font_style: ['normal'] }; } public tip(message: any): log { // 柔和钢蓝 return { message, font_color: '#8fb0c9', font_style: ['normal'] }; } public custom(message: any, font_color: string, font_style: font_style[]): log { return { message, font_color, font_style }; } // 多段拼接后在同一行输出,每段可拥有独立的颜色与样式;支持单段或数组 public log(input: console_input): void { const segments: console_segment[] = Array.isArray(input) ? input : [input]; if (segments.length === 0) { return; } const body = segments.map(segment => this.format(segment)).join(''); this.write_line(this.format(this.timestamp()) + body); } // 自动前缀:[month_day hh:mm:ss],统一使用 success 色 private timestamp(): log { const now = new Date(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hour = String(now.getHours()).padStart(2, '0'); const minute = String(now.getMinutes()).padStart(2, '0'); const second = String(now.getSeconds()).padStart(2, '0'); return this.success(`[${month}-${day} ${hour}:${minute}:${second}] `); } private format(segment: log | string | number | boolean | null | undefined): string { if (segment !== null && segment !== undefined && typeof segment === 'object' && 'message' in segment) { return to_ansi(segment as log); } return String(segment); } // 有输入时先清掉当前输入行,打印日志后再把输入行恢复,避免输入被打断 private write_line(text: string): void { if (this.rl !== null && this.input_enabled && process.stdout.isTTY === true) { readline.cursorTo(process.stdout, 0); readline.clearLine(process.stdout, 0); process.stdout.write(text + '\n'); this.rl.prompt(true); this.need_prompt = false; return; } process.stdout.write(text + '\n'); } // 开启控制台输入,按回车后通过 'input' 事件抛出整行内容 public enable_input(prompt?: string): void { if (prompt !== undefined) { this.prompt = prompt; } if (this.rl !== null) { this.rl.setPrompt(this.prompt); return; } this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdout.isTTY === true }); this.rl.setPrompt(this.prompt); this.rl.on('line', (line: string) => { // 若输入处理期间没有输出日志,readline 已在换行后清空输入行,这里补回提示符; // 若期间输出过日志,write_line 已恢复提示符,避免重复 this.need_prompt = true; this.emit('input', line); if (this.rl !== null && this.need_prompt) { this.rl.prompt(); } this.need_prompt = false; }); this.rl.on('close', () => { this.rl = null; this.input_enabled = false; }); this.input_enabled = true; this.rl.prompt(); } public disable_input(): void { if (this.rl === null) { return; } const rl = this.rl; this.rl = null; this.input_enabled = false; rl.close(); } public set_prompt(prompt: string): void { this.prompt = prompt; this.rl?.setPrompt(prompt); if (this.rl !== null && this.input_enabled) { this.rl.prompt(true); } } } export const console_print = new ConsolePrint();