From b0a8e2b5b73db286e545672513595794e3f1739e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Fri, 25 Jun 2021 00:41:34 +0200 Subject: [PATCH] Add true color output --- ansi.test.ts | 22 ++++++++++++++++++-- ansi.ts | 18 ++++++++++++++++- base.test.ts | 4 ++-- base.ts | 27 +++++++++++++++++++------ config/run.flags | 1 + config/test.flags | 1 + config/types.flags | 1 + demo.ts | 10 +++++++--- run | 19 ++++++++++++++++++ deps.test.ts => testing.ts | 0 ui.test.ts | 41 ++++++++++++++++++++++++++++++++++++++ ui.ts | 40 ++++++++++++++++++++++++++++++++++--- 12 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 config/run.flags create mode 100644 config/test.flags create mode 100644 config/types.flags create mode 100755 run rename deps.test.ts => testing.ts (100%) create mode 100644 ui.test.ts diff --git a/ansi.test.ts b/ansi.test.ts index 8d51b4f..0b003de 100644 --- a/ansi.test.ts +++ b/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); +} diff --git a/ansi.ts b/ansi.ts index 0685806..ca3db9d 100644 --- a/ansi.ts +++ b/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 { + // 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 { - // 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}`)); } } diff --git a/base.test.ts b/base.test.ts index e50990a..1f7ab5e 100644 --- a/base.test.ts +++ b/base.test.ts @@ -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 "); diff --git a/base.ts b/base.ts index 35fe5d7..9fe61fe 100644 --- a/base.ts +++ b/base.ts @@ -7,6 +7,8 @@ export type Color = { b: number; }; +export type PaletteMap = ReadonlyArray; + /** * 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; } } diff --git a/config/run.flags b/config/run.flags new file mode 100644 index 0000000..b1c44e5 --- /dev/null +++ b/config/run.flags @@ -0,0 +1 @@ +--allow-env --unstable \ No newline at end of file diff --git a/config/test.flags b/config/test.flags new file mode 100644 index 0000000..418455f --- /dev/null +++ b/config/test.flags @@ -0,0 +1 @@ +--unstable \ No newline at end of file diff --git a/config/types.flags b/config/types.flags new file mode 100644 index 0000000..418455f --- /dev/null +++ b/config/types.flags @@ -0,0 +1 @@ +--unstable \ No newline at end of file diff --git a/demo.ts b/demo.ts index a27e039..041dbbd 100755 --- a/demo.ts +++ b/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)); diff --git a/run b/run new file mode 100755 index 0000000..74d1c6d --- /dev/null +++ b/run @@ -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 diff --git a/deps.test.ts b/testing.ts similarity index 100% rename from deps.test.ts rename to testing.ts diff --git a/ui.test.ts b/ui.test.ts new file mode 100644 index 0000000..3d4c394 --- /dev/null +++ b/ui.test.ts @@ -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 { + return { w: 1, h: 1 }; + } + + async setupPalette(colors: readonly Color[]): Promise { + return this.palette; + } + + async clear(): Promise { + } + + async setChar(at: BufferLocation, char: Char): Promise { + this.last = char; + } +} diff --git a/ui.ts b/ui.ts index 706c926..31e4bfd 100644 --- a/ui.ts +++ b/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 { + async init(palette: UIPalette): Promise { 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 { + // 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>;