import { BufferLocation, BufferSize, Char, Color } from "./base.ts"; import { readKeypress } from "./deps.ts"; import { Display } from "./display.ts"; export enum AnsiColorMode { AUTODETECT, COLORS256, TRUECOLOR, } /** * ANSI terminal display */ export class AnsiTerminalDisplay extends Display { private palette_bg: readonly Uint8Array[] = []; private palette_fg: readonly Uint8Array[] = []; private width = 1; private state = { x: -1, y: -1, f: -1, b: -1 }; // current location and color constructor( private writer: Deno.Writer = Deno.stdout, reader: Deno.Reader = Deno.stdin, ) { super(); if (hasRawMode(reader)) { this.readKeyPresses(reader); // purposefully not awaited } } async init(): Promise { await this.writer.write(CLEAR); } async uninit(): Promise { await this.writer.write(CLEAR); } async flush(): Promise { } async getSize(): Promise { const size = Deno.consoleSize(Deno.stdout.rid); this.width = size.columns; return { w: size.columns, h: size.rows, }; } async setupPalette( colors: readonly Color[], mode = AnsiColorMode.AUTODETECT, ): Promise { if (mode == AnsiColorMode.AUTODETECT) { const colorterm = Deno.env.get("COLORTERM"); if (colorterm?.search(/truecolor|24bit/)) { mode = AnsiColorMode.TRUECOLOR; } else { mode = AnsiColorMode.COLORS256; } } if (mode == AnsiColorMode.TRUECOLOR) { // True color is supported, use the request palette as-is const cr = (x: number) => Math.round(x * 255); this.palette_bg = colors.map((col) => escape(`[48;2;${cr(col.r)};${cr(col.g)};${cr(col.b)}m`) ); this.palette_fg = colors.map((col) => escape(`[38;2;${cr(col.r)};${cr(col.g)};${cr(col.b)}m`) ); return colors; } else { // True color not supported, fallback to 256-colors const result = get256Colors(); this.palette_bg = result.map((_, idx) => escape(`[48;5;${idx}m`)); this.palette_fg = result.map((_, idx) => escape(`[38;5;${idx}m`)); return result; } } async setCursorVisibility(visible: boolean): Promise { await this.writer.write(visible ? escape("[?25h") : escape("[?25l")); } async setChar(at: BufferLocation, char: Char): Promise { let { x, y, f, b } = this.state; if (f != char.fg) { f = char.fg; const col = this.palette_fg[f]; if (col) { await this.writer.write(col); } } if (b != char.bg) { b = char.bg; const col = this.palette_bg[b]; if (col) { await this.writer.write(col); } } if (x != at.x || y != at.y) { x = at.x; y = at.y; await this.writer.write(escape(`[${y + 1};${x + 1}H`)); } await this.writer.write(new TextEncoder().encode(char.ch)); x += 1; if (x >= this.width) { x = 0; y += 1; } this.state = { x, y, f, b }; } /** * Force the display size for subsequent prints */ forceSize(size: BufferSize) { this.width = size.w; } private async readKeyPresses(reader: Deno.Reader & { rid: number }) { for await (const keypress of readKeypress(reader)) { let key = keypress.key; if (key) { if (keypress.shiftKey) { key = "shift+" + (key.length == 1 ? key.toLocaleLowerCase() : key); } if (keypress.metaKey) { key = "alt+" + key; } if (keypress.ctrlKey) { key = "ctrl+" + key; } await this.pushEvent({ key }); } } } } function escape(sequence: string): Uint8Array { return new Uint8Array([0x1B, ...new TextEncoder().encode(sequence)]); } function get256Colors(): readonly Color[] { const result: Color[] = [ { r: 0, g: 0, b: 0 }, { r: 0.5, g: 0, b: 0 }, { r: 0, g: 0.5, b: 0 }, { r: 0.5, g: 0.5, b: 0 }, { r: 0, g: 0, b: 0.5 }, { r: 0.5, g: 0, b: 0.5 }, { r: 0, g: 0.5, b: 0.5 }, { r: 0.75, g: 0.75, b: 0.75 }, { r: 0.5, g: 0.5, b: 0.5 }, { r: 1, g: 0, b: 0 }, { r: 0, g: 1, b: 0 }, { r: 1, g: 1, b: 0 }, { r: 0, g: 0, b: 1 }, { r: 1, g: 0, b: 1 }, { r: 0, g: 1, b: 1 }, { r: 1, g: 1, b: 1 }, ]; for (let r = 0; r < 6; r++) { for (let g = 0; g < 6; g++) { for (let b = 0; b < 6; b++) { result.push({ r: r / 5, g: g / 5, b: b / 5 }); } } } for (let l = 0; l < 24; l++) { result.push({ r: (l + 1) / 25, g: (l + 1) / 25, b: (l + 1) / 25 }); } return result; } const CLEAR = escape("[2J"); /** * Check if a reader will be compatible with raw mode */ function hasRawMode( reader: Deno.Reader, ): reader is Deno.Reader & { rid: number } { return typeof ( reader).rid == "number"; }