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 { Buffer, describe, expect, it } from "./deps.test.ts";
|
||||
import { Buffer, describe, expect, it } from "./testing.ts";
|
||||
|
||||
describe(AnsiTerminalDisplay, () => {
|
||||
it("clears the screen", async () => {
|
||||
const stdout = new Buffer();
|
||||
const display = new AnsiTerminalDisplay(stdout);
|
||||
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
|
||||
*/
|
||||
export class AnsiTerminalDisplay implements Display {
|
||||
private palette_bg: readonly Uint8Array[] = [];
|
||||
private palette_fg: readonly Uint8Array[] = [];
|
||||
|
||||
constructor(
|
||||
private writer: Deno.Writer = Deno.stdout,
|
||||
private reader: Deno.Reader = Deno.stdin,
|
||||
|
@ -20,6 +23,14 @@ export class AnsiTerminalDisplay implements Display {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -28,7 +39,12 @@ export class AnsiTerminalDisplay implements Display {
|
|||
}
|
||||
|
||||
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}`));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BufferDrawing, CharBuffer } from "./base.ts";
|
||||
import { Buffer, describe, expect, it } from "./deps.test.ts";
|
||||
import { describe, expect, it } from "./testing.ts";
|
||||
|
||||
describe(CharBuffer, () => {
|
||||
it("initializes empty, sets and gets characters", () => {
|
||||
|
@ -16,7 +16,7 @@ describe(CharBuffer, () => {
|
|||
describe(BufferDrawing, () => {
|
||||
it("draws text", () => {
|
||||
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("so", { x: 1, y: 1 });
|
||||
expect(buffer.toString()).toEqual("esti so ");
|
||||
|
|
27
base.ts
27
base.ts
|
@ -7,6 +7,8 @@ export type Color = {
|
|||
b: number;
|
||||
};
|
||||
|
||||
export type PaletteMap = ReadonlyArray<number>;
|
||||
|
||||
/**
|
||||
* Displayable character, with background and foreground color taken from the palette
|
||||
*/
|
||||
|
@ -39,7 +41,7 @@ export class CharBuffer {
|
|||
*/
|
||||
get(at: BufferLocation): Char {
|
||||
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];
|
||||
} else {
|
||||
return SPACE;
|
||||
|
@ -72,23 +74,36 @@ export class CharBuffer {
|
|||
* Tools for drawing inside a display buffer
|
||||
*/
|
||||
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
|
||||
*/
|
||||
text(content: string, from: BufferLocation): void {
|
||||
let { w, h } = this.buffer.getSize();
|
||||
text(content: string, from: BufferLocation): BufferDrawing {
|
||||
const { bg, fg, buffer } = this;
|
||||
const { w, h } = buffer.getSize();
|
||||
let { x, y } = from;
|
||||
let buf = this.buffer;
|
||||
if (y >= 0 && y < h) {
|
||||
for (let ch of content) {
|
||||
if (x >= 0 && x < w) {
|
||||
buf.set({ x, y }, { ch, bg: 0, fg: 0 });
|
||||
buffer.set({ x, y }, { ch, bg, fg });
|
||||
}
|
||||
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 { TextUI } from "./ui.ts";
|
||||
|
||||
const display = new AnsiTerminalDisplay();
|
||||
const ui = new TextUI(display);
|
||||
await ui.init();
|
||||
ui.drawing.text("hello", { x: 10, y: 3 });
|
||||
await ui.init([
|
||||
{ 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 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";
|
||||
|
||||
/**
|
||||
|
@ -6,20 +12,22 @@ import { Display } from "./display.ts";
|
|||
*/
|
||||
export class TextUI {
|
||||
private screen = new CharBuffer({ w: 1, h: 1 });
|
||||
private palettemap: PaletteMap = [];
|
||||
|
||||
constructor(private display: Display) {
|
||||
}
|
||||
|
||||
get drawing(): BufferDrawing {
|
||||
return new BufferDrawing(this.screen);
|
||||
return new BufferDrawing(this.screen, this.palettemap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the display
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
async init(palette: UIPalette): Promise<void> {
|
||||
var size = await this.display.getSize();
|
||||
this.screen = new CharBuffer(size);
|
||||
this.palettemap = await this.getPaletteMapping(palette);
|
||||
await this.display.clear();
|
||||
}
|
||||
|
||||
|
@ -52,4 +60,30 @@ export class TextUI {
|
|||
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