193 lines
4.7 KiB
TypeScript
193 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";
|
|
}
|