diff --git a/ansi.ts b/ansi.ts index 8b73b09..2927656 100644 --- a/ansi.ts +++ b/ansi.ts @@ -20,7 +20,7 @@ export class AnsiTerminalDisplay implements Display { constructor( private writer: Deno.Writer = Deno.stdout, - private reader: Deno.Reader = Deno.stdin, + reader: Deno.Reader = Deno.stdin, ) { if (hasRawMode(reader)) { this.readKeyPresses(reader); // purposefully not awaited diff --git a/base.test.ts b/base.test.ts index 1f7ab5e..12aecb4 100644 --- a/base.test.ts +++ b/base.test.ts @@ -1,4 +1,4 @@ -import { BufferDrawing, CharBuffer } from "./base.ts"; +import { BufferDrawing, BufferLocation, CharBuffer } from "./base.ts"; import { describe, expect, it } from "./testing.ts"; describe(CharBuffer, () => { @@ -11,6 +11,26 @@ describe(CharBuffer, () => { 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 }); }); + + it("keeps track of dirty lines", async () => { + const buffer = new CharBuffer({ w: 16, h: 3 }); + expect(buffer.isDirty({ x: 0, y: 0 })).toBe(false); + buffer.set({ x: 0, y: 0 }, { ch: "x", bg: 0, fg: 0 }); + expect(buffer.isDirty({ x: 0, y: 0 })).toBe(true); + expect(buffer.isDirty({ x: 1, y: 0 })).toBe(true); + expect(buffer.isDirty({ x: 2, y: 0 })).toBe(false); + expect(buffer.isDirty({ x: 0, y: 1 })).toBe(false); + let calls: BufferLocation[] = []; + await buffer.forEachDirty(async (at) => { + calls.push(at); + }); + expect(calls).toEqual([{ x: 0, y: 0 }, { x: 1, y: 0 }]); + calls = []; + await buffer.forEachDirty(async (at) => { + calls.push(at); + }); + expect(calls).toEqual([]); + }); }); describe(BufferDrawing, () => { diff --git a/base.ts b/base.ts index 9fe61fe..60eb320 100644 --- a/base.ts +++ b/base.ts @@ -25,19 +25,25 @@ export const SPACE: Char = { ch: " ", bg: 0, fg: 0 } as const; /** * Rectangular buffer of displayable characters + * + * All methods in this class do not properly check for + * out-of-bounds coordinates, use BufferDrawing for this */ export class CharBuffer { private chars: Array; + private dirty: boolean; + private dirty_lines: Uint8Array; + private dirty_seg_length: number; constructor(private size: BufferSize) { this.chars = new Array(size.w * size.h).fill(SPACE); + this.dirty = false; + this.dirty_lines = new Uint8Array(size.h).fill(0x00); + this.dirty_seg_length = Math.ceil(size.w / 8); } /** - * Get the character buffered at a given at - * - * This does not properly check for out-of-bounds coordinates, - * use BufferDrawing for this + * Get the character buffered at a given location */ get(at: BufferLocation): Char { const i = at.y * this.size.w + at.x; @@ -50,17 +56,60 @@ export class CharBuffer { /** * 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) { + // TODO only if changed? this.chars[i] = char; + this.dirty = true; + this.dirty_lines[at.y] |= 1 << Math.floor(at.x / this.dirty_seg_length); } } + /** + * Call a method for each dirty character, then clear the dirty flags + */ + async forEachDirty( + op: (at: BufferLocation, char: Char) => Promise, + ): Promise { + if (this.dirty) { + const { w, h } = this.getSize(); + for (let y = 0; y < h; y++) { + const line = this.dirty_lines[y]; + if (line) { + for (let sx = 0; sx < 8; sx++) { + if (line & 1 << sx) { + const limit = Math.min((sx + 1) * this.dirty_seg_length, w); + for (let x = sx * this.dirty_seg_length; x < limit; x++) { + await op({ x, y }, this.get({ x, y })); + } + } + } + } + } + this.setDirty(false); + } + } + + /** + * Set the dirty state for all characters + */ + setDirty(dirty: boolean): void { + this.dirty = dirty; + this.dirty_lines.fill(dirty ? 0xFF : 0x00); + } + + /** + * Check if a given location is potentially dirty + * + * This ignores the global dirty flag, only checks dirty line segments + */ + isDirty(at: BufferLocation): boolean { + const seg = Math.floor(at.x / this.dirty_seg_length); + return (this.dirty_lines[at.y] & (1 << seg)) != 0; + } + getSize(): BufferSize { return this.size; } diff --git a/ui.ts b/ui.ts index 7f30f1d..704deff 100644 --- a/ui.ts +++ b/ui.ts @@ -80,19 +80,15 @@ export class TextUI { return; } - // TODO only dirty chars - const { w, h } = this.screen.getSize(); - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - await this.display.setChar({ x, y }, this.screen.get({ x, y })); - } - } + await this.screen.forEachDirty((at, char) => + this.display.setChar(at, char) + ); } /** * Start the event loop, waiting for input */ - async loop(refresh = 1000): Promise { + async loop(refresh = 100): Promise { while (!this.quitting) { for (const key of await this.display.getKeyStrokes()) { if (!this.config.ignoreCtrlC && key == "ctrl+c") {