diff --git a/src/lib/util/ansi.ts b/src/lib/util/ansi.ts new file mode 100644 index 0000000..72665be --- /dev/null +++ b/src/lib/util/ansi.ts @@ -0,0 +1,131 @@ +import { tryCatch, type Result } from '@maxmorozoff/try-catch-tuple'; + +const encoder = new TextEncoder(); + +export const ANSI = { + // Text Styles + RESET: '\x1B[0m', + BOLD: '\x1B[1m', + DIM: '\x1B[2m', + ITALIC: '\x1B[3m', + UNDERLINE: '\x1B[4m', + BLINK: '\x1B[5m', + REVERSE: '\x1B[7m', + HIDDEN: '\x1B[8m', + + // Foreground Colors + BLACK: '\x1B[30m', + RED: '\x1B[31m', + GREEN: '\x1B[32m', + YELLOW: '\x1B[33m', + BLUE: '\x1B[34m', + MAGENTA: '\x1B[35m', + CYAN: '\x1B[36m', + WHITE: '\x1B[37m', + + // Background Colors + BG_BLACK: '\x1B[40m', + BG_RED: '\x1B[41m', + BG_GREEN: '\x1B[42m', + BG_YELLOW: '\x1B[43m', + BG_BLUE: '\x1B[44m', + BG_MAGENTA: '\x1B[45m', + BG_CYAN: '\x1B[46m', + BG_WHITE: '\x1B[47m', + + // Cursor Control + SAVE_CURSOR: '\x1B[s', + RESTORE_CURSOR: '\x1B[u', + CURSOR_TO: (x: number, y: number) => `\x1B[${y};${x}H`, + CURSOR_UP: (n = 1) => `\x1B[${n}A`, + CURSOR_DOWN: (n = 1) => `\x1B[${n}B`, + CURSOR_FORWARD: (n = 1) => `\x1B[${n}C`, + CURSOR_BACK: (n = 1) => `\x1B[${n}D`, + + // Screen Control + CLEAR_SCREEN: '\x1B[2J', + CLEAR_LINE: '\x1B[2K', + CLEAR_END_LINE: '\x1B[0K', + CLEAR_START_LINE: '\x1B[1K', + CLEAR_BELOW: '\x1B[J', +}; + +export const ANSI_BUFFERS = { + // Static code buffers + RESET: encoder.encode(ANSI.RESET), + BOLD: encoder.encode(ANSI.BOLD), + DIM: encoder.encode(ANSI.DIM), + ITALIC: encoder.encode(ANSI.ITALIC), + UNDERLINE: encoder.encode(ANSI.UNDERLINE), + BLINK: encoder.encode(ANSI.BLINK), + REVERSE: encoder.encode(ANSI.REVERSE), + HIDDEN: encoder.encode(ANSI.HIDDEN), + + // Colors + BLACK: encoder.encode(ANSI.BLACK), + RED: encoder.encode(ANSI.RED), + GREEN: encoder.encode(ANSI.GREEN), + YELLOW: encoder.encode(ANSI.YELLOW), + BLUE: encoder.encode(ANSI.BLUE), + MAGENTA: encoder.encode(ANSI.MAGENTA), + CYAN: encoder.encode(ANSI.CYAN), + WHITE: encoder.encode(ANSI.WHITE), + + // Backgrounds + BG_BLACK: encoder.encode(ANSI.BG_BLACK), + BG_RED: encoder.encode(ANSI.BG_RED), + BG_GREEN: encoder.encode(ANSI.BG_GREEN), + BG_YELLOW: encoder.encode(ANSI.BG_YELLOW), + BG_BLUE: encoder.encode(ANSI.BG_BLUE), + BG_MAGENTA: encoder.encode(ANSI.BG_MAGENTA), + BG_CYAN: encoder.encode(ANSI.BG_CYAN), + BG_WHITE: encoder.encode(ANSI.BG_WHITE), + + // Cursor Control + SAVE_CURSOR: encoder.encode(ANSI.SAVE_CURSOR), + RESTORE_CURSOR: encoder.encode(ANSI.RESTORE_CURSOR), + CLEAR_SCREEN: encoder.encode(ANSI.CLEAR_SCREEN), + CLEAR_LINE: encoder.encode(ANSI.CLEAR_LINE), + CLEAR_BELOW: encoder.encode(ANSI.CLEAR_BELOW), +}; + +type AnsiBufferKey = keyof typeof ANSI_BUFFERS; + +export async function writeAnsi( + codes: AnsiBufferKey[], + text?: string, + autoReset: boolean = true +): Promise> { + const buffers: Uint8Array[] = []; + + // Add requested ANSI codes + for (const code of codes) { + buffers.push(ANSI_BUFFERS[code]); + } + + // Add optional text + if (text) { + buffers.push(encoder.encode(text)); + } + + // Auto-reset if requested + if (autoReset && !codes.includes('RESET')) { + buffers.push(ANSI_BUFFERS.RESET); + } + + // Combine all buffers into single write + const combined = new Uint8Array( + buffers.reduce((acc, buf) => acc + buf.length, 0) + ); + + let offset = 0; + for (const buf of buffers) { + combined.set(buf, offset); + offset += buf.length; + } + + return tryCatch(() => Bun.write(Bun.stdout, combined)); +} + +// For CommonJS compatibility +export default { ANSI, ANSI_BUFFERS, writeAnsi };