Add key strokes callback
This commit is contained in:
parent
731f601cdb
commit
285550e766
26
ansi.test.ts
26
ansi.test.ts
|
@ -1,17 +1,26 @@
|
||||||
import { AnsiColorMode, AnsiTerminalDisplay } from "./ansi.ts";
|
import { AnsiColorMode, AnsiTerminalDisplay } from "./ansi.ts";
|
||||||
import { Buffer, describe, expect, it } from "./testing.ts";
|
import { Buffer, describe, expect, it } from "./testing.ts";
|
||||||
|
|
||||||
|
function createTestDisplay(): {
|
||||||
|
stdout: Buffer;
|
||||||
|
stdin: Buffer;
|
||||||
|
display: AnsiTerminalDisplay;
|
||||||
|
} {
|
||||||
|
const stdout = new Buffer();
|
||||||
|
const stdin = new Buffer();
|
||||||
|
const display = new AnsiTerminalDisplay(stdout, stdin);
|
||||||
|
return { stdout, stdin, display };
|
||||||
|
}
|
||||||
|
|
||||||
describe(AnsiTerminalDisplay, () => {
|
describe(AnsiTerminalDisplay, () => {
|
||||||
it("clears the screen", async () => {
|
it("clears the screen", async () => {
|
||||||
const stdout = new Buffer();
|
const { stdout, display } = createTestDisplay();
|
||||||
const display = new AnsiTerminalDisplay(stdout);
|
|
||||||
await display.clear();
|
await display.clear();
|
||||||
checkSequence(stdout, "![2J");
|
checkSequence(stdout, "![2J");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes truecolor characters", async () => {
|
it("writes truecolor characters", async () => {
|
||||||
const stdout = new Buffer();
|
const { stdout, display } = createTestDisplay();
|
||||||
const display = new AnsiTerminalDisplay(stdout);
|
|
||||||
await display.setupPalette([
|
await display.setupPalette([
|
||||||
{ r: 0.0, g: 0.0, b: 0.0 },
|
{ r: 0.0, g: 0.0, b: 0.0 },
|
||||||
{ r: 0.5, g: 0.1, b: 1.0 },
|
{ r: 0.5, g: 0.1, b: 1.0 },
|
||||||
|
@ -21,8 +30,7 @@ describe(AnsiTerminalDisplay, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to 256 colors", async () => {
|
it("falls back to 256 colors", async () => {
|
||||||
const stdout = new Buffer();
|
const { stdout, display } = createTestDisplay();
|
||||||
const display = new AnsiTerminalDisplay(stdout);
|
|
||||||
const palette = await display.setupPalette([
|
const palette = await display.setupPalette([
|
||||||
{ r: 0.0, g: 0.0, b: 0.0 },
|
{ r: 0.0, g: 0.0, b: 0.0 },
|
||||||
], AnsiColorMode.COLORS256);
|
], AnsiColorMode.COLORS256);
|
||||||
|
@ -38,8 +46,7 @@ describe(AnsiTerminalDisplay, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves the cursor only when needed", async () => {
|
it("moves the cursor only when needed", async () => {
|
||||||
const stdout = new Buffer();
|
const { stdout, display } = createTestDisplay();
|
||||||
const display = new AnsiTerminalDisplay(stdout);
|
|
||||||
display.forceSize({ w: 4, h: 3 });
|
display.forceSize({ w: 4, h: 3 });
|
||||||
await display.setChar({ x: 0, y: 0 }, { ch: "a", fg: 0, bg: 0 });
|
await display.setChar({ x: 0, y: 0 }, { ch: "a", fg: 0, bg: 0 });
|
||||||
await display.setChar({ x: 1, y: 0 }, { ch: "b", fg: 0, bg: 0 });
|
await display.setChar({ x: 1, y: 0 }, { ch: "b", fg: 0, bg: 0 });
|
||||||
|
@ -50,8 +57,7 @@ describe(AnsiTerminalDisplay, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("changes colors only when needed", async () => {
|
it("changes colors only when needed", async () => {
|
||||||
const stdout = new Buffer();
|
const { stdout, display } = createTestDisplay();
|
||||||
const display = new AnsiTerminalDisplay(stdout);
|
|
||||||
display.forceSize({ w: 10, h: 1 });
|
display.forceSize({ w: 10, h: 1 });
|
||||||
await display.setupPalette([
|
await display.setupPalette([
|
||||||
{ r: 0.0, g: 0.0, b: 0.0 },
|
{ r: 0.0, g: 0.0, b: 0.0 },
|
||||||
|
|
42
ansi.ts
42
ansi.ts
|
@ -1,4 +1,5 @@
|
||||||
import { BufferLocation, BufferSize, Char, Color } from "./base.ts";
|
import { BufferLocation, BufferSize, Char, Color } from "./base.ts";
|
||||||
|
import { readKeypress } from "./deps.ts";
|
||||||
import { Display } from "./display.ts";
|
import { Display } from "./display.ts";
|
||||||
|
|
||||||
export enum AnsiColorMode {
|
export enum AnsiColorMode {
|
||||||
|
@ -15,11 +16,15 @@ export class AnsiTerminalDisplay implements Display {
|
||||||
private palette_fg: readonly Uint8Array[] = [];
|
private palette_fg: readonly Uint8Array[] = [];
|
||||||
private width = 1;
|
private width = 1;
|
||||||
private state = { x: -1, y: -1, f: -1, b: -1 }; // current location and color
|
private state = { x: -1, y: -1, f: -1, b: -1 }; // current location and color
|
||||||
|
private keys: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private writer: Deno.Writer = Deno.stdout,
|
private writer: Deno.Writer = Deno.stdout,
|
||||||
private reader: Deno.Reader = Deno.stdin,
|
private reader: Deno.Reader = Deno.stdin,
|
||||||
) {
|
) {
|
||||||
|
if (hasRawMode(reader)) {
|
||||||
|
this.readKeyPresses(reader); // purposefully not awaited
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSize(): Promise<BufferSize> {
|
async getSize(): Promise<BufferSize> {
|
||||||
|
@ -67,6 +72,10 @@ export class AnsiTerminalDisplay implements Display {
|
||||||
await this.writer.write(CLEAR);
|
await this.writer.write(CLEAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setCursorVisibility(visible: boolean): Promise<void> {
|
||||||
|
await this.writer.write(visible ? escape("[?25h") : escape("[?25l"));
|
||||||
|
}
|
||||||
|
|
||||||
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
||||||
let { x, y, f, b } = this.state;
|
let { x, y, f, b } = this.state;
|
||||||
|
|
||||||
|
@ -102,12 +111,36 @@ export class AnsiTerminalDisplay implements Display {
|
||||||
this.state = { x, y, f, b };
|
this.state = { x, y, f, b };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getKeyStrokes(): Promise<string[]> {
|
||||||
|
const result = this.keys;
|
||||||
|
this.keys = [];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force the display size for subsequent prints
|
* Force the display size for subsequent prints
|
||||||
*/
|
*/
|
||||||
forceSize(size: BufferSize) {
|
forceSize(size: BufferSize) {
|
||||||
this.width = size.w;
|
this.width = size.w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async readKeyPresses(reader: Deno.Reader & { rid: number }) {
|
||||||
|
for await (const keypress of readKeypress(reader)) {
|
||||||
|
let key = keypress.key;
|
||||||
|
if (key) {
|
||||||
|
if (keypress.shiftKey) {
|
||||||
|
key = "shift+" + (key.length == 1 ? key.toLocaleLowerCase() : key);
|
||||||
|
}
|
||||||
|
if (keypress.metaKey) {
|
||||||
|
key = "alt+" + key;
|
||||||
|
}
|
||||||
|
if (keypress.ctrlKey) {
|
||||||
|
key = "ctrl+" + key;
|
||||||
|
}
|
||||||
|
this.keys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escape(sequence: string): Uint8Array {
|
function escape(sequence: string): Uint8Array {
|
||||||
|
@ -147,3 +180,12 @@ function get256Colors(): readonly Color[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLEAR = escape("[2J");
|
const CLEAR = escape("[2J");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a reader will be compatible with raw mode
|
||||||
|
*/
|
||||||
|
function hasRawMode(
|
||||||
|
reader: Deno.Reader,
|
||||||
|
): reader is Deno.Reader & { rid: number } {
|
||||||
|
return typeof (<any> reader).rid == "number";
|
||||||
|
}
|
||||||
|
|
40
config.ts
Normal file
40
config.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { Color } from "./base.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color palette requirements.
|
||||||
|
*
|
||||||
|
* The array represents the "ideal" colors desired by the application.
|
||||||
|
* When drawing things, *bg* and *fg* color information should be an index
|
||||||
|
* in this palette.
|
||||||
|
*
|
||||||
|
* For each palette index, a single color can be requested, or an
|
||||||
|
* array of accepted alternatives, with decreasing priority.
|
||||||
|
*/
|
||||||
|
export type UIPalette = ReadonlyArray<Color | ReadonlyArray<Color>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration of the UI
|
||||||
|
*/
|
||||||
|
export type UIConfig = Readonly<{
|
||||||
|
// Set the automatic loop interval on init
|
||||||
|
// If left undefined, a call to ui.loop() will be required after init
|
||||||
|
loopInterval?: number;
|
||||||
|
// Set if anything is rendered at all
|
||||||
|
renderingEnabled: boolean;
|
||||||
|
// Ignore the ctrl+c key combo to quit the UI
|
||||||
|
ignoreCtrlC: boolean;
|
||||||
|
// Initially hide the cursor
|
||||||
|
hideCursor: boolean;
|
||||||
|
// Palette of colors that will be used by the UI
|
||||||
|
palette: UIPalette;
|
||||||
|
// Callback to receive key strokes
|
||||||
|
onKeyStroke: (key: string) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export const UI_CONFIG_DEFAULTS: UIConfig = {
|
||||||
|
renderingEnabled: true,
|
||||||
|
ignoreCtrlC: false,
|
||||||
|
hideCursor: true,
|
||||||
|
palette: [],
|
||||||
|
onKeyStroke: () => {},
|
||||||
|
};
|
24
demo.ts
24
demo.ts
|
@ -1,16 +1,24 @@
|
||||||
#!./run
|
#!./run
|
||||||
|
|
||||||
import { AnsiTerminalDisplay } from "./ansi.ts";
|
import { AnsiTerminalDisplay } from "./ansi.ts";
|
||||||
|
import { UIConfig } from "./config.ts";
|
||||||
import { TextUI } from "./ui.ts";
|
import { TextUI } from "./ui.ts";
|
||||||
|
|
||||||
const display = new AnsiTerminalDisplay();
|
const display = new AnsiTerminalDisplay();
|
||||||
const ui = new TextUI(display);
|
let x = 0;
|
||||||
await ui.init([
|
const config: Partial<UIConfig> = {
|
||||||
{ r: 0, g: 0, b: 0 },
|
palette: [
|
||||||
{ r: 1, g: 1, b: 1 },
|
{ r: 0, g: 0, b: 0 },
|
||||||
{ r: 0, g: 1, b: 1 },
|
{ r: 1, g: 1, b: 1 },
|
||||||
]);
|
{ r: 0, g: 1, b: 1 },
|
||||||
|
],
|
||||||
|
onKeyStroke: (key) => {
|
||||||
|
ui.drawing.color(1, 0).text(key, { x, y: 7 });
|
||||||
|
x += key.length + 1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const ui = new TextUI(display, config);
|
||||||
|
await ui.init();
|
||||||
ui.drawing.color(2, 0).text("hello", { x: 10, y: 3 });
|
ui.drawing.color(2, 0).text("hello", { x: 10, y: 3 });
|
||||||
ui.drawing.color(0, 1).text("world", { x: 10, y: 5 });
|
ui.drawing.color(0, 1).text("world", { x: 10, y: 5 });
|
||||||
await ui.flush();
|
await ui.loop();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
||||||
|
|
1
deps.ts
1
deps.ts
|
@ -1 +1,2 @@
|
||||||
export * from "https://code.thunderk.net/typescript/functional/raw/1.0.0/all.ts";
|
export * from "https://code.thunderk.net/typescript/functional/raw/1.0.0/all.ts";
|
||||||
|
export { readKeypress } from "https://deno.land/x/keypress@0.0.7/mod.ts";
|
||||||
|
|
29
display.ts
29
display.ts
|
@ -3,11 +3,13 @@ import { BufferLocation, BufferSize, Char, Color } from "./base.ts";
|
||||||
/**
|
/**
|
||||||
* Display protocol, to allow the UI to draw things on "screen"
|
* Display protocol, to allow the UI to draw things on "screen"
|
||||||
*/
|
*/
|
||||||
export interface Display {
|
export class Display {
|
||||||
/**
|
/**
|
||||||
* Get the displayable grid size
|
* Get the displayable grid size
|
||||||
*/
|
*/
|
||||||
getSize(): Promise<BufferSize>;
|
async getSize(): Promise<BufferSize> {
|
||||||
|
return { w: 1, h: 1 };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup the palette for color display
|
* Setup the palette for color display
|
||||||
|
@ -17,15 +19,32 @@ export interface Display {
|
||||||
*
|
*
|
||||||
* From this call forward, colors will be received by numbered index in the returned array.
|
* From this call forward, colors will be received by numbered index in the returned array.
|
||||||
*/
|
*/
|
||||||
setupPalette(colors: readonly Color[]): Promise<readonly Color[]>;
|
async setupPalette(colors: readonly Color[]): Promise<readonly Color[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the whole screen
|
* Clear the whole screen
|
||||||
*/
|
*/
|
||||||
clear(): Promise<void>;
|
async clear(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cursor visibility
|
||||||
|
*/
|
||||||
|
async setCursorVisibility(visible: boolean): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw a single character on screen
|
* Draw a single character on screen
|
||||||
*/
|
*/
|
||||||
setChar(at: BufferLocation, char: Char): Promise<void>;
|
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the keys pressed since last call
|
||||||
|
*/
|
||||||
|
async getKeyStrokes(): Promise<string[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
31
mod.ts
31
mod.ts
|
@ -1,15 +1,28 @@
|
||||||
import { AnsiTerminalDisplay } from "./ansi.ts";
|
import { AnsiTerminalDisplay } from "./ansi.ts";
|
||||||
import { TextUI, UIPalette } from "./ui.ts";
|
import { UIConfig } from "./config.ts";
|
||||||
|
import { Display } from "./display.ts";
|
||||||
|
import { TextUI } from "./ui.ts";
|
||||||
export { TextUI } from "./ui.ts";
|
export { TextUI } from "./ui.ts";
|
||||||
|
|
||||||
export type UIConfig = {
|
export const UI_DISPLAY_TYPES = {
|
||||||
palette: UIPalette;
|
autodetect: undefined,
|
||||||
};
|
ansi: AnsiTerminalDisplay,
|
||||||
|
dummy: Display,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export async function createTextUI(
|
||||||
|
config: Partial<UIConfig>,
|
||||||
|
display_type: keyof typeof UI_DISPLAY_TYPES = "autodetect",
|
||||||
|
): Promise<TextUI> {
|
||||||
|
if (display_type == "autodetect") {
|
||||||
|
// TODO detect platform
|
||||||
|
display_type = "ansi";
|
||||||
|
}
|
||||||
|
|
||||||
|
var display = new UI_DISPLAY_TYPES[display_type]();
|
||||||
|
|
||||||
|
var ui = new TextUI(display, config);
|
||||||
|
await ui.init();
|
||||||
|
|
||||||
export async function createTextUI(config: UIConfig): Promise<TextUI> {
|
|
||||||
// TODO detect platform
|
|
||||||
var display = new AnsiTerminalDisplay();
|
|
||||||
var ui = new TextUI(display);
|
|
||||||
await ui.init(config.palette);
|
|
||||||
return ui;
|
return ui;
|
||||||
}
|
}
|
||||||
|
|
18
ui.test.ts
18
ui.test.ts
|
@ -10,8 +10,8 @@ describe(TextUI, () => {
|
||||||
{ r: 1.0, g: 0.0, b: 0.0 },
|
{ r: 1.0, g: 0.0, b: 0.0 },
|
||||||
];
|
];
|
||||||
const display = new PaletteTestDisplay(palette);
|
const display = new PaletteTestDisplay(palette);
|
||||||
const ui = new TextUI(display);
|
const ui = new TextUI(display, { palette });
|
||||||
await ui.init(palette);
|
await ui.init();
|
||||||
ui.drawing.color(1, 0).text("x", { x: 0, y: 0 });
|
ui.drawing.color(1, 0).text("x", { x: 0, y: 0 });
|
||||||
await ui.flush();
|
await ui.flush();
|
||||||
expect(display.last).toEqual({ ch: "x", bg: 0, fg: 1 });
|
expect(display.last).toEqual({ ch: "x", bg: 0, fg: 1 });
|
||||||
|
@ -28,31 +28,25 @@ describe(TextUI, () => {
|
||||||
{ r: 0.1, g: 0.2, b: 0.1 },
|
{ r: 0.1, g: 0.2, b: 0.1 },
|
||||||
];
|
];
|
||||||
const display = new PaletteTestDisplay(display_palette);
|
const display = new PaletteTestDisplay(display_palette);
|
||||||
const ui = new TextUI(display);
|
const ui = new TextUI(display, { palette: app_palette });
|
||||||
await ui.init(app_palette);
|
await ui.init();
|
||||||
ui.drawing.color(0, 1).text("x", { x: 0, y: 0 });
|
ui.drawing.color(0, 1).text("x", { x: 0, y: 0 });
|
||||||
await ui.flush();
|
await ui.flush();
|
||||||
expect(display.last).toEqual({ ch: "x", bg: 2, fg: 1 });
|
expect(display.last).toEqual({ ch: "x", bg: 2, fg: 1 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class PaletteTestDisplay implements Display {
|
class PaletteTestDisplay extends Display {
|
||||||
last = SPACE;
|
last = SPACE;
|
||||||
|
|
||||||
constructor(public palette: Color[]) {
|
constructor(public palette: Color[]) {
|
||||||
}
|
super();
|
||||||
|
|
||||||
async getSize(): Promise<BufferSize> {
|
|
||||||
return { w: 1, h: 1 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setupPalette(colors: readonly Color[]): Promise<readonly Color[]> {
|
async setupPalette(colors: readonly Color[]): Promise<readonly Color[]> {
|
||||||
return this.palette;
|
return this.palette;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(): Promise<void> {
|
|
||||||
}
|
|
||||||
|
|
||||||
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
async setChar(at: BufferLocation, char: Char): Promise<void> {
|
||||||
this.last = char;
|
this.last = char;
|
||||||
}
|
}
|
||||||
|
|
183
ui.ts
183
ui.ts
|
@ -5,6 +5,7 @@ import {
|
||||||
Color,
|
Color,
|
||||||
PaletteMap,
|
PaletteMap,
|
||||||
} from "./base.ts";
|
} from "./base.ts";
|
||||||
|
import { UI_CONFIG_DEFAULTS, UIConfig, UIPalette } from "./config.ts";
|
||||||
import { cmp } from "./deps.ts";
|
import { cmp } from "./deps.ts";
|
||||||
import { Display } from "./display.ts";
|
import { Display } from "./display.ts";
|
||||||
|
|
||||||
|
@ -12,10 +13,13 @@ import { Display } from "./display.ts";
|
||||||
* Common abstraction for a textual UI
|
* Common abstraction for a textual UI
|
||||||
*/
|
*/
|
||||||
export class TextUI {
|
export class TextUI {
|
||||||
|
private config: UIConfig;
|
||||||
private screen = new CharBuffer({ w: 1, h: 1 });
|
private screen = new CharBuffer({ w: 1, h: 1 });
|
||||||
private palettemap: PaletteMap = [];
|
private palettemap: PaletteMap = [];
|
||||||
|
private quitting = false;
|
||||||
|
|
||||||
constructor(private display: Display) {
|
constructor(private display: Display, config: Partial<UIConfig>) {
|
||||||
|
this.config = { ...UI_CONFIG_DEFAULTS, ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
get drawing(): BufferDrawing {
|
get drawing(): BufferDrawing {
|
||||||
|
@ -23,13 +27,42 @@ export class TextUI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the display
|
* Initializes the UI and display
|
||||||
|
*
|
||||||
|
* If config.loopInterval is defined, the UI loop is
|
||||||
|
* started but not awaited.
|
||||||
*/
|
*/
|
||||||
async init(palette: UIPalette): Promise<void> {
|
async init(): Promise<void> {
|
||||||
var size = await this.display.getSize();
|
var size = await this.display.getSize();
|
||||||
this.screen = new CharBuffer(size);
|
this.screen = new CharBuffer(size);
|
||||||
this.palettemap = await this.getPaletteMapping(palette);
|
this.palettemap = await getPaletteMapping(
|
||||||
await this.display.clear();
|
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<void> {
|
||||||
|
this.quitting = true;
|
||||||
|
if (this.config.renderingEnabled) {
|
||||||
|
await this.display.clear();
|
||||||
|
await this.display.setCursorVisibility(true);
|
||||||
|
}
|
||||||
|
if (typeof Deno != "undefined") {
|
||||||
|
Deno.exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,6 +76,10 @@ export class TextUI {
|
||||||
* Flush the internal buffer to the display
|
* Flush the internal buffer to the display
|
||||||
*/
|
*/
|
||||||
async flush(): Promise<void> {
|
async flush(): Promise<void> {
|
||||||
|
if (!this.config.renderingEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO only dirty chars
|
// TODO only dirty chars
|
||||||
const { w, h } = this.screen.getSize();
|
const { w, h } = this.screen.getSize();
|
||||||
for (let y = 0; y < h; y++) {
|
for (let y = 0; y < h; y++) {
|
||||||
|
@ -56,82 +93,78 @@ export class TextUI {
|
||||||
* Start the event loop, waiting for input
|
* Start the event loop, waiting for input
|
||||||
*/
|
*/
|
||||||
async loop(refresh = 1000): Promise<void> {
|
async loop(refresh = 1000): Promise<void> {
|
||||||
while (true) {
|
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 this.flush();
|
||||||
await new Promise((resolve) => setTimeout(resolve, refresh));
|
await new Promise((resolve) => setTimeout(resolve, refresh));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getPaletteMapping(
|
|
||||||
palette: UIPalette,
|
|
||||||
): Promise<PaletteMap> {
|
|
||||||
// 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 this.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function getPaletteMapping(
|
||||||
* Color palette requirements.
|
palette: UIPalette,
|
||||||
*
|
display: Display,
|
||||||
* The array represents the "ideal" colors desired by the application.
|
): Promise<PaletteMap> {
|
||||||
* When drawing things, *bg* and *fg* color information should be an index
|
// get the colors supported by display
|
||||||
* in this palette.
|
const app_colors = palette.map((c): Color[] => Array.isArray(c) ? c : [c]);
|
||||||
*
|
const all_colors = app_colors.reduce((acc, val) => acc.concat(val), []);
|
||||||
* For each palette index, a single color can be requested, or an
|
const display_colors = await display.setupPalette(all_colors);
|
||||||
* array of accepted alternatives, with decreasing priority.
|
|
||||||
*/
|
// rank all supported colors by proximity to each app color
|
||||||
export type UIPalette = ReadonlyArray<Color | ReadonlyArray<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 {
|
function colorDistance(e1: Color, e2: Color): number {
|
||||||
/*return (e2.r - e1.r) * (e2.r - e1.r) +
|
/*return (e2.r - e1.r) * (e2.r - e1.r) +
|
||||||
|
|
Loading…
Reference in a new issue