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 screen = 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.screen, 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.screen = new CharBuffer(size); this.palettemap = await getPaletteMapping( this.config.palette, this.display, ); if (this.config.renderingEnabled) { if (this.config.hideCursor) { await this.display.setCursorVisibility(false); } await this.display.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; if (this.config.renderingEnabled) { await this.display.clear(); await this.display.setCursorVisibility(true); } if (typeof Deno != "undefined") { Deno.exit(); } } /** * Get the current display size */ getSize(): BufferSize { return this.screen.getSize(); } /** * Flush the internal buffer to the display */ async flush(): Promise { if (!this.config.renderingEnabled) { 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 })); } } } /** * Start the event loop, waiting for input */ async loop(refresh = 1000): Promise { while (!this.quitting) { for (const key of await this.display.getKeyStrokes()) { if (!this.config.ignoreCtrlC && key == "ctrl+c") { await this.quit(); } else { this.config.onKeyStroke(key); } } await this.flush(); 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), ); }