Add buffers with text drawing

This commit is contained in:
Michaël Lemaire 2021-05-14 00:04:47 +02:00
parent 1d10420cad
commit 55238b5065
8 changed files with 191 additions and 35 deletions

21
ansi.ts
View file

@ -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<BufferSize> {
const size = Deno.consoleSize(Deno.stdout.rid);
return {
w: size.columns,
h: size.rows,
};
}
async setupPalette(colors: readonly Color[]): Promise<readonly Color[]> {
@ -14,6 +26,11 @@ export class AnsiTerminalDisplay implements Display {
async clear(): Promise<void> {
await this.writer.write(CLEAR);
}
async setChar(at: BufferLocation, char: Char): Promise<void> {
// TODO colors
await this.writer.write(escape(`[${at.y};${at.x}H${char.ch}`));
}
}
function escape(sequence: string): Uint8Array {

24
base.test.ts Normal file
View file

@ -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 ");
});
});

94
base.ts Normal file
View file

@ -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<Char>;
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++;
}
}
}
}

View file

@ -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<void> {
await this.display.clear();
}
}

View file

@ -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));

View file

@ -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";

View file

@ -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<BufferSize>;
/**
* Setup the palette for color display
*
@ -34,4 +23,9 @@ export interface Display {
* Clear the whole screen
*/
clear(): Promise<void>;
/**
* Draw a single character on screen
*/
setChar(at: BufferLocation, char: Char): Promise<void>;
}

38
ui.ts Normal file
View file

@ -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<void> {
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<void> {
// 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 }));
}
}
}
}