chore: initial commit
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import * as readline from 'node:readline';
|
||||
import { EventEmitter } from 'node:events';
|
||||
type font_style = 'normal' | 'italic' | 'bold' | 'light' |'underline' | 'url';
|
||||
|
||||
interface log {
|
||||
message: any;
|
||||
font_color: string;
|
||||
font_style: font_style[];
|
||||
}
|
||||
|
||||
// 将 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();
|
||||
Reference in New Issue
Block a user