textui/ansi.ts

194 lines
4.7 KiB
TypeScript

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<void> {
await this.writer.write(CLEAR);
}
async uninit(): Promise<void> {
await this.writer.write(CLEAR);
}
async flush(): Promise<void> {
}
async getSize(): Promise<BufferSize> {
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<readonly Color[]> {
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<void> {
await this.writer.write(visible ? escape("[?25h") : escape("[?25l"));
}
async setChar(at: BufferLocation, char: Char): Promise<void> {
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 (<any> reader).rid == "number";
}