import { BufferDrawing, BufferSize, CharBuffer, Color, PaletteMap, } from "./base.ts"; import { UI_CONFIG_DEFAULTS, UIConfig, UIPalette } from "./config.ts"; import { cmp } from "./deps.ts"; import { Display } from "./display.ts"; /** * Common abstraction for a textual UI */ export class TextUI { private config: UIConfig; private buffer = new CharBuffer({ w: 1, h: 1 }); private palettemap: PaletteMap = []; private quitting = false; constructor(private display: Display, config: Partial) { this.config = { ...UI_CONFIG_DEFAULTS, ...config }; } get drawing(): BufferDrawing { return new BufferDrawing(this.buffer, this.palettemap); } /** * Initializes the UI and display * * If config.loopInterval is defined, the UI loop is * started but not awaited. */ async init(): Promise { var size = await this.display.getSize(); this.buffer = new CharBuffer(size); await this.display.init(); this.palettemap = await getPaletteMapping( this.config.palette, this.display, ); if (this.config.hideCursor) { await this.display.setCursorVisibility(false); } await this.clear(); if (this.config.loopInterval) { this.loop(this.config.loopInterval); // purposefully not awaited } } /** * Quit the UI (this will exit the executable) */ async quit(): Promise { this.quitting = true; await this.clear(); await this.display.setCursorVisibility(true); await this.display.uninit(); if (typeof Deno != "undefined") { Deno.exit(); } } /** * Get the current display size */ getSize(): BufferSize { return this.buffer.getSize(); } /** * Flush the internal buffer to the display */ async flush(): Promise { await this.buffer.forEachDirty((at, char) => this.display.setChar(at, char) ); await this.display.flush(); } /** * Clear the whole screen */ async clear(bg = 0): Promise { const { w, h } = this.getSize(); const drawing = this.drawing.color(bg, bg); for (let y = 0; y < h; y++) { drawing.text(Array(w).fill(" ").join(""), { x: 0, y }); } await this.flush(); } /** * Resize the buffer to match the display */ async resize(size: BufferSize): Promise { this.buffer = new CharBuffer(size); await this.clear(); this.config.onResize(size); } /** * Start the event loop, waiting for input */ async loop(refresh = 100): Promise { while (!this.quitting) { // handle events for (const event of await this.display.getEvents()) { const { key, click, size } = event; if (key) { if (!this.config.ignoreCtrlC && key == "ctrl+c") { await this.quit(); } else { this.config.onKeyStroke(key); } } if (size) { await this.resize(size); } if (click) { this.config.onMouseClick(click); } } // flush await this.flush(); // wait await new Promise((resolve) => setTimeout(resolve, refresh)); } } } async function getPaletteMapping( palette: UIPalette, display: Display, ): Promise { // get the colors supported by display const app_colors = palette.map((c): Color[] => Array.isArray(c) ? c : [c]); const all_colors = app_colors.reduce((acc, val) => acc.concat(val), []); const display_colors = await display.setupPalette(all_colors); // rank all supported colors by proximity to each app color let ranked: { color: Color; idx: number; penalty: number; matches: { color: Color; idx: number; distance: number }[]; }[] = []; app_colors.forEach((colors, idx) => { colors.forEach((color, alt) => { ranked.push({ color, idx, penalty: alt + 1, matches: display_colors.map((display_color, didx) => { return { color: display_color, idx: didx, distance: colorDistance(color, display_color), }; }).sort(cmp({ key: (info) => info.distance })), }); }); }); // TODO negatively score colors too much near previously chosen ones // find the best color mapping for each source color const result = palette.map(() => -1); while (ranked.length > 0) { ranked.sort( cmp({ key: (info) => info.matches[0].distance * info.penalty }), ); const best = ranked[0]; const app_idx = best.idx; const display_idx = best.matches[0].idx; result[app_idx] = display_idx; for (const color of ranked) { color.matches = color.matches.filter((match) => match.idx !== display_idx ); } ranked = ranked.filter((color) => color.idx !== app_idx && color.matches.length > 0 ); } return result; } function colorDistance(e1: Color, e2: Color): number { /*return (e2.r - e1.r) * (e2.r - e1.r) + (e2.g - e1.g) * (e2.g - e1.g) + (e2.b - e1.b) * (e2.b - e1.b);*/ const c = (x: number) => Math.round(x * 255); const rmean = (c(e1.r) + c(e2.r)) / 2; const r = c(e1.r) - c(e2.r); const g = c(e1.g) - c(e2.g); const b = c(e1.b) - c(e2.b); return Math.sqrt( (((512 + rmean) * r * r) >> 8) + 4 * g * g + (((767 - rmean) * b * b) >> 8), ); }