chore: initial commit

This commit is contained in:
2026-10-22 17:07:08 +08:00
commit 6ccb102d38
20 changed files with 5949 additions and 0 deletions
+260
View File
@@ -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();