Files
node_server/utils/console_print.ts
T
2026-10-22 17:07:08 +08:00

255 lines
8.0 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 * 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();