From 55238b50653946fcedd63c55f5e8fe7099d1706a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Fri, 14 May 2021 00:04:47 +0200 Subject: [PATCH] Add buffers with text drawing --- ansi.ts | 21 ++++++++++-- base.test.ts | 24 ++++++++++++++ base.ts | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++ common.ts | 13 -------- demo.ts | 6 ++-- deps.ts | 2 +- display.ts | 28 ++++++---------- ui.ts | 38 +++++++++++++++++++++ 8 files changed, 191 insertions(+), 35 deletions(-) create mode 100644 base.test.ts create mode 100644 base.ts delete mode 100644 common.ts create mode 100644 ui.ts diff --git a/ansi.ts b/ansi.ts index 0e66c98..0685806 100644 --- a/ansi.ts +++ b/ansi.ts @@ -1,10 +1,22 @@ -import { Color, Display } from "./display.ts"; +import { BufferLocation, BufferSize, Char, Color } from "./base.ts"; +import { Display } from "./display.ts"; /** * ANSI terminal display */ export class AnsiTerminalDisplay implements Display { - constructor(private writer: Deno.Writer = Deno.stdout) { + constructor( + private writer: Deno.Writer = Deno.stdout, + private reader: Deno.Reader = Deno.stdin, + ) { + } + + async getSize(): Promise { + const size = Deno.consoleSize(Deno.stdout.rid); + return { + w: size.columns, + h: size.rows, + }; } async setupPalette(colors: readonly Color[]): Promise { @@ -14,6 +26,11 @@ export class AnsiTerminalDisplay implements Display { async clear(): Promise { await this.writer.write(CLEAR); } + + async setChar(at: BufferLocation, char: Char): Promise { + // TODO colors + await this.writer.write(escape(`[${at.y};${at.x}H${char.ch}`)); + } } function escape(sequence: string): Uint8Array { diff --git a/base.test.ts b/base.test.ts new file mode 100644 index 0000000..e50990a --- /dev/null +++ b/base.test.ts @@ -0,0 +1,24 @@ +import { BufferDrawing, CharBuffer } from "./base.ts"; +import { Buffer, describe, expect, it } from "./deps.test.ts"; + +describe(CharBuffer, () => { + it("initializes empty, sets and gets characters", () => { + const buffer = new CharBuffer({ w: 3, h: 2 }); + expect(buffer.toString()).toEqual(" "); + buffer.set({ x: 2, y: 0 }, { ch: "x", fg: 1, bg: 4 }); + buffer.set({ x: 1, y: 1 }, { ch: "y", fg: 2, bg: 5 }); + expect(buffer.toString()).toEqual(" x y "); + expect(buffer.get({ x: 0, y: 0 })).toEqual({ ch: " ", fg: 0, bg: 0 }); + expect(buffer.get({ x: 1, y: 1 })).toEqual({ ch: "y", fg: 2, bg: 5 }); + }); +}); + +describe(BufferDrawing, () => { + it("draws text", () => { + const buffer = new CharBuffer({ w: 4, h: 2 }); + 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 new file mode 100644 index 0000000..35fe5d7 --- /dev/null +++ b/base.ts @@ -0,0 +1,94 @@ +/** + * Color represented by RGB (0.0-1.0) components + */ +export type Color = { + r: number; + g: number; + b: number; +}; + +/** + * Displayable character, with background and foreground color taken from the palette + */ +export type Char = Readonly<{ + ch: string; + bg: number; + fg: number; +}>; + +export type BufferSize = Readonly<{ w: number; h: number }>; +export type BufferLocation = Readonly<{ x: number; y: number }>; + +export const SPACE: Char = { ch: " ", bg: 0, fg: 0 } as const; + +/** + * Rectangular buffer of displayable characters + */ +export class CharBuffer { + private chars: Array; + + constructor(private size: BufferSize) { + this.chars = new Array(size.w * size.h).fill(SPACE); + } + + /** + * Get the character buffered at a given at + * + * This does not properly check for out-of-bounds coordinates, + * use BufferDrawing for this + */ + get(at: BufferLocation): Char { + const i = at.y * this.size.w + at.x; + if (i > 0 && i < this.chars.length) { + return this.chars[i]; + } else { + return SPACE; + } + } + + /** + * Change the character buffered at a given location + * + * This does not properly check for out-of-bounds coordinates, + * use BufferDrawing for this + */ + set(at: BufferLocation, char: Char): void { + const i = at.y * this.size.w + at.x; + if (i >= 0 && i < this.chars.length) { + this.chars[i] = char; + } + } + + getSize(): BufferSize { + return this.size; + } + + toString(): string { + return this.chars.map((c) => c.ch).join(""); + } +} + +/** + * Tools for drawing inside a display buffer + */ +export class BufferDrawing { + constructor(private readonly buffer: CharBuffer) { + } + + /** + * Draw a piece of text of the same color + */ + text(content: string, from: BufferLocation): void { + let { w, h } = this.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 }); + } + x++; + } + } + } +} diff --git a/common.ts b/common.ts deleted file mode 100644 index 6cfe7b1..0000000 --- a/common.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Display } from "./display.ts"; - -/** - * Common abstraction for a textual UI - */ -export class TextUI { - constructor(private display: Display) { - } - - async init(): Promise { - await this.display.clear(); - } -} diff --git a/demo.ts b/demo.ts index e18dbca..a27e039 100755 --- a/demo.ts +++ b/demo.ts @@ -1,9 +1,11 @@ -#!/usr/bin/env -S deno run +#!/usr/bin/env -S deno run --allow-env --unstable import { AnsiTerminalDisplay } from "./ansi.ts"; -import { TextUI } from "./common.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.flush(); await new Promise((resolve) => setTimeout(resolve, 3000)); diff --git a/deps.ts b/deps.ts index f23b5a8..14da2b0 100644 --- a/deps.ts +++ b/deps.ts @@ -1 +1 @@ -export { Sys } from "https://code.thunderk.net/typescript/devtools/raw/1.2.2/system.ts"; +export * from "https://code.thunderk.net/typescript/functional/raw/1.0.0/all.ts"; diff --git a/display.ts b/display.ts index b9cf326..d055145 100644 --- a/display.ts +++ b/display.ts @@ -1,25 +1,14 @@ -/** - * Color represented by RGB (0.0-1.0) components - */ -export type Color = { - r: number; - g: number; - b: number; -}; - -/** - * Displayable character, with background and foreground color taken from the palette - */ -export type Char = { - ch: string; - bg: number; - fg: number; -}; +import { BufferLocation, BufferSize, Char, Color } from "./base.ts"; /** * Display protocol, to allow the UI to draw things on "screen" */ export interface Display { + /** + * Get the displayable grid size + */ + getSize(): Promise; + /** * Setup the palette for color display * @@ -34,4 +23,9 @@ export interface Display { * Clear the whole screen */ clear(): Promise; + + /** + * Draw a single character on screen + */ + setChar(at: BufferLocation, char: Char): Promise; } diff --git a/ui.ts b/ui.ts new file mode 100644 index 0000000..6b5f7f8 --- /dev/null +++ b/ui.ts @@ -0,0 +1,38 @@ +import { BufferDrawing, CharBuffer } from "./base.ts"; +import { Display } from "./display.ts"; + +/** + * Common abstraction for a textual UI + */ +export class TextUI { + private screen = new CharBuffer({ w: 1, h: 1 }); + + constructor(private display: Display) { + } + + get drawing(): BufferDrawing { + return new BufferDrawing(this.screen); + } + + /** + * Initializes the display + */ + async init(): Promise { + var size = await this.display.getSize(); + this.screen = new CharBuffer(size); + await this.display.clear(); + } + + /** + * Flush the internal buffer to the display + */ + async flush(): Promise { + // TODO only dirty chars + const { w, h } = this.screen.getSize(); + for (let x = 0; x < w; x++) { + for (let y = 0; y < h; y++) { + await this.display.setChar({ x, y }, this.screen.get({ x, y })); + } + } + } +}