Add true color output
This commit is contained in:
parent
c9292c0199
commit
b0a8e2b5b7
12 changed files with 167 additions and 17 deletions
22
ansi.test.ts
22
ansi.test.ts
|
@ -1,11 +1,29 @@
|
||||||
import { AnsiTerminalDisplay } from "./ansi.ts";
|
import { AnsiTerminalDisplay } from "./ansi.ts";
|
||||||
import { Buffer, describe, expect, it } from "./deps.test.ts";
|
import { Buffer, describe, expect, it } from "./testing.ts";
|
||||||
|
|
||||||
describe(AnsiTerminalDisplay, () => {
|
describe(AnsiTerminalDisplay, () => {
|
||||||
it("clears the screen", async () => {
|
it("clears the screen", async () => {
|
||||||
const stdout = new Buffer();
|
const stdout = new Buffer();
|
||||||
const display = new AnsiTerminalDisplay(stdout);
|
const display = new AnsiTerminalDisplay(stdout);
|
||||||
await display.clear();
|
await display.clear();
|
||||||
expect(stdout.bytes()).toEqual(new Uint8Array([27, 91, 50, 74]));
|
checkSequence(stdout, "![2J");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes colored characters", async () => {
|
||||||
|
const stdout = new Buffer();
|
||||||
|
const display = new AnsiTerminalDisplay(stdout);
|
||||||
|
await display.setupPalette([
|
||||||
|
{ r: 0.0, g: 0.0, b: 0.0 },
|
||||||
|
{ r: 0.5, g: 0.1, b: 1.0 },
|
||||||
|
]);
|
||||||
|
await display.setChar({ x: 0, y: 0 }, { ch: "$", fg: 1, bg: 0 });
|
||||||
|
checkSequence(stdout, "![38;2;128;26;255m![48;2;0;0;0m![0;0H$");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function checkSequence(buffer: Buffer, expected: string) {
|
||||||
|
const decoded = new TextDecoder().decode(
|
||||||
|
buffer.bytes().map((x) => x == 0x1B ? 0x21 : x),
|
||||||
|
);
|
||||||
|
expect(decoded).toEqual(expected);
|
||||||
|
}
|
||||||
|
|
18
ansi.ts
18
ansi.ts
|
@ -5,6 +5,9 @@ import { Display } from "./display.ts";
|
||||||
* ANSI terminal display
|
* ANSI terminal display
|
||||||
*/
|
*/
|
||||||
export class AnsiTerminalDisplay implements Display {
|
export class AnsiTerminalDisplay implements Display {
|
||||||
|
private palette_bg: readonly Uint8Array[] = [];
|
||||||
|
private palette_fg: readonly Uint8Array[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private writer: Deno.Writer = Deno.stdout,
|
private writer: Deno.Writer = Deno.stdout,
|
||||||
private reader: Deno.Reader = Deno.stdin,
|
private reader: Deno.Reader = Deno.stdin,
|
||||||
|
@ -20,6 +23,14 @@ export class AnsiTerminalDisplay implements Display {
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupPalette(colors: readonly Color[]): Promise<readonly Color[]> {
|
async setupPalette(colors: readonly Color[]): Promise<readonly Color[]> {
|
||||||
|
// TODO handle not fully rgb compatible terminals
|
||||||
|
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;
|
return colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +39,12 @@ export class AnsiTerminalDisplay implements Display {
|
||||||
}
|
}
|
||||||
|
|
||||||
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
||||||
// TODO colors
|
// TODO do not move the cursor if already at good location
|
||||||
|
// TODO do not change the color if already good
|
||||||
|
const fg = this.palette_fg[char.fg];
|
||||||
|
const bg = this.palette_bg[char.bg];
|
||||||
|
await this.writer.write(fg);
|
||||||
|
await this.writer.write(bg);
|
||||||
await this.writer.write(escape(`[${at.y};${at.x}H${char.ch}`));
|
await this.writer.write(escape(`[${at.y};${at.x}H${char.ch}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BufferDrawing, CharBuffer } from "./base.ts";
|
import { BufferDrawing, CharBuffer } from "./base.ts";
|
||||||
import { Buffer, describe, expect, it } from "./deps.test.ts";
|
import { describe, expect, it } from "./testing.ts";
|
||||||
|
|
||||||
describe(CharBuffer, () => {
|
describe(CharBuffer, () => {
|
||||||
it("initializes empty, sets and gets characters", () => {
|
it("initializes empty, sets and gets characters", () => {
|
||||||
|
@ -16,7 +16,7 @@ describe(CharBuffer, () => {
|
||||||
describe(BufferDrawing, () => {
|
describe(BufferDrawing, () => {
|
||||||
it("draws text", () => {
|
it("draws text", () => {
|
||||||
const buffer = new CharBuffer({ w: 4, h: 2 });
|
const buffer = new CharBuffer({ w: 4, h: 2 });
|
||||||
const drawing = new BufferDrawing(buffer);
|
const drawing = new BufferDrawing(buffer, []);
|
||||||
drawing.text("testing", { x: -1, y: 0 });
|
drawing.text("testing", { x: -1, y: 0 });
|
||||||
drawing.text("so", { x: 1, y: 1 });
|
drawing.text("so", { x: 1, y: 1 });
|
||||||
expect(buffer.toString()).toEqual("esti so ");
|
expect(buffer.toString()).toEqual("esti so ");
|
||||||
|
|
27
base.ts
27
base.ts
|
@ -7,6 +7,8 @@ export type Color = {
|
||||||
b: number;
|
b: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PaletteMap = ReadonlyArray<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displayable character, with background and foreground color taken from the palette
|
* Displayable character, with background and foreground color taken from the palette
|
||||||
*/
|
*/
|
||||||
|
@ -39,7 +41,7 @@ export class CharBuffer {
|
||||||
*/
|
*/
|
||||||
get(at: BufferLocation): Char {
|
get(at: BufferLocation): Char {
|
||||||
const i = at.y * this.size.w + at.x;
|
const i = at.y * this.size.w + at.x;
|
||||||
if (i > 0 && i < this.chars.length) {
|
if (i >= 0 && i < this.chars.length) {
|
||||||
return this.chars[i];
|
return this.chars[i];
|
||||||
} else {
|
} else {
|
||||||
return SPACE;
|
return SPACE;
|
||||||
|
@ -72,23 +74,36 @@ export class CharBuffer {
|
||||||
* Tools for drawing inside a display buffer
|
* Tools for drawing inside a display buffer
|
||||||
*/
|
*/
|
||||||
export class BufferDrawing {
|
export class BufferDrawing {
|
||||||
constructor(private readonly buffer: CharBuffer) {
|
private bg = 0;
|
||||||
|
private fg = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly buffer: CharBuffer,
|
||||||
|
private readonly palettemap: PaletteMap,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
color(fg: number, bg: number): BufferDrawing {
|
||||||
|
this.fg = this.palettemap[fg];
|
||||||
|
this.bg = this.palettemap[bg];
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw a piece of text of the same color
|
* Draw a piece of text of the same color
|
||||||
*/
|
*/
|
||||||
text(content: string, from: BufferLocation): void {
|
text(content: string, from: BufferLocation): BufferDrawing {
|
||||||
let { w, h } = this.buffer.getSize();
|
const { bg, fg, buffer } = this;
|
||||||
|
const { w, h } = buffer.getSize();
|
||||||
let { x, y } = from;
|
let { x, y } = from;
|
||||||
let buf = this.buffer;
|
|
||||||
if (y >= 0 && y < h) {
|
if (y >= 0 && y < h) {
|
||||||
for (let ch of content) {
|
for (let ch of content) {
|
||||||
if (x >= 0 && x < w) {
|
if (x >= 0 && x < w) {
|
||||||
buf.set({ x, y }, { ch, bg: 0, fg: 0 });
|
buffer.set({ x, y }, { ch, bg, fg });
|
||||||
}
|
}
|
||||||
x++;
|
x++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
config/run.flags
Normal file
1
config/run.flags
Normal file
|
@ -0,0 +1 @@
|
||||||
|
--allow-env --unstable
|
1
config/test.flags
Normal file
1
config/test.flags
Normal file
|
@ -0,0 +1 @@
|
||||||
|
--unstable
|
1
config/types.flags
Normal file
1
config/types.flags
Normal file
|
@ -0,0 +1 @@
|
||||||
|
--unstable
|
10
demo.ts
10
demo.ts
|
@ -1,11 +1,15 @@
|
||||||
#!/usr/bin/env -S deno run --allow-env --unstable
|
#!./run
|
||||||
|
|
||||||
import { AnsiTerminalDisplay } from "./ansi.ts";
|
import { AnsiTerminalDisplay } from "./ansi.ts";
|
||||||
import { TextUI } from "./ui.ts";
|
import { TextUI } from "./ui.ts";
|
||||||
|
|
||||||
const display = new AnsiTerminalDisplay();
|
const display = new AnsiTerminalDisplay();
|
||||||
const ui = new TextUI(display);
|
const ui = new TextUI(display);
|
||||||
await ui.init();
|
await ui.init([
|
||||||
ui.drawing.text("hello", { x: 10, y: 3 });
|
{ r: 0, g: 0, b: 0 },
|
||||||
|
{ r: 1, g: 1, b: 1 },
|
||||||
|
{ r: 0, g: 1, b: 1 },
|
||||||
|
]);
|
||||||
|
ui.drawing.color(2, 0).text("hello", { x: 10, y: 3 });
|
||||||
await ui.flush();
|
await ui.flush();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
|
|
19
run
Executable file
19
run
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Simplified run tool for deno commands
|
||||||
|
|
||||||
|
if test $# -eq 0
|
||||||
|
then
|
||||||
|
echo "Usage: $0 [file or command]"
|
||||||
|
exit 1
|
||||||
|
elif echo $1 | grep -q '.*.ts'
|
||||||
|
then
|
||||||
|
denocmd=run
|
||||||
|
denoargs=$1
|
||||||
|
shift
|
||||||
|
else
|
||||||
|
denocmd=$1
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@"
|
||||||
|
exec deno $denocmd $denoargs
|
41
ui.test.ts
Normal file
41
ui.test.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { BufferLocation, BufferSize, Char, Color, SPACE } from "./base.ts";
|
||||||
|
import { describe, expect, it } from "./testing.ts";
|
||||||
|
import { Display } from "./display.ts";
|
||||||
|
import { TextUI } from "./ui.ts";
|
||||||
|
|
||||||
|
describe(TextUI, () => {
|
||||||
|
it("maps a full RGB color palette directly", async () => {
|
||||||
|
const palette = [
|
||||||
|
{ r: 0.0, g: 0.0, b: 0.0 },
|
||||||
|
{ r: 1.0, g: 0.0, b: 0.0 },
|
||||||
|
];
|
||||||
|
const display = new PaletteTestDisplay(palette);
|
||||||
|
const ui = new TextUI(display);
|
||||||
|
await ui.init(palette);
|
||||||
|
ui.drawing.color(1, 0).text("x", { x: 0, y: 0 });
|
||||||
|
await ui.flush();
|
||||||
|
expect(display.last).toEqual({ ch: "x", bg: 0, fg: 1 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class PaletteTestDisplay implements Display {
|
||||||
|
last = SPACE;
|
||||||
|
|
||||||
|
constructor(public palette: Color[]) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSize(): Promise<BufferSize> {
|
||||||
|
return { w: 1, h: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async setupPalette(colors: readonly Color[]): Promise<readonly Color[]> {
|
||||||
|
return this.palette;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
||||||
|
this.last = char;
|
||||||
|
}
|
||||||
|
}
|
40
ui.ts
40
ui.ts
|
@ -1,4 +1,10 @@
|
||||||
import { BufferDrawing, BufferSize, CharBuffer } from "./base.ts";
|
import {
|
||||||
|
BufferDrawing,
|
||||||
|
BufferSize,
|
||||||
|
CharBuffer,
|
||||||
|
Color,
|
||||||
|
PaletteMap,
|
||||||
|
} from "./base.ts";
|
||||||
import { Display } from "./display.ts";
|
import { Display } from "./display.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,20 +12,22 @@ import { Display } from "./display.ts";
|
||||||
*/
|
*/
|
||||||
export class TextUI {
|
export class TextUI {
|
||||||
private screen = new CharBuffer({ w: 1, h: 1 });
|
private screen = new CharBuffer({ w: 1, h: 1 });
|
||||||
|
private palettemap: PaletteMap = [];
|
||||||
|
|
||||||
constructor(private display: Display) {
|
constructor(private display: Display) {
|
||||||
}
|
}
|
||||||
|
|
||||||
get drawing(): BufferDrawing {
|
get drawing(): BufferDrawing {
|
||||||
return new BufferDrawing(this.screen);
|
return new BufferDrawing(this.screen, this.palettemap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the display
|
* Initializes the display
|
||||||
*/
|
*/
|
||||||
async init(): Promise<void> {
|
async init(palette: UIPalette): Promise<void> {
|
||||||
var size = await this.display.getSize();
|
var size = await this.display.getSize();
|
||||||
this.screen = new CharBuffer(size);
|
this.screen = new CharBuffer(size);
|
||||||
|
this.palettemap = await this.getPaletteMapping(palette);
|
||||||
await this.display.clear();
|
await this.display.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,4 +60,30 @@ export class TextUI {
|
||||||
await new Promise((resolve) => setTimeout(resolve, refresh));
|
await new Promise((resolve) => setTimeout(resolve, refresh));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getPaletteMapping(
|
||||||
|
palette: UIPalette,
|
||||||
|
): Promise<PaletteMap> {
|
||||||
|
// get the colors supported by display
|
||||||
|
const allcolors = palette
|
||||||
|
.map((c): Color[] => Array.isArray(c) ? c : [c])
|
||||||
|
.reduce((acc, val) => acc.concat(val), []);
|
||||||
|
const supported = await this.display.setupPalette(allcolors);
|
||||||
|
|
||||||
|
// TODO find the best color mapping for each source color
|
||||||
|
|
||||||
|
return palette.map((_, idx) => idx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color palette requirements.
|
||||||
|
*
|
||||||
|
* The array represents the "ideal" colors desired by the application.
|
||||||
|
* When drawing things, *bg* and *fg* color information should be an index
|
||||||
|
* in this palette.
|
||||||
|
*
|
||||||
|
* For each palette index, a single color can be requested, or an
|
||||||
|
* array of accepted alternatives, with decreasing priority.
|
||||||
|
*/
|
||||||
|
export type UIPalette = ReadonlyArray<Color | ReadonlyArray<Color>>;
|
||||||
|
|
Loading…
Reference in a new issue