Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
3e0e4fb8aa | |||
bb898ba0a8 |
24 changed files with 162 additions and 177 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
deno.d.ts
|
deno.d.ts
|
||||||
.vscode
|
.vscode
|
||||||
.local
|
.local
|
||||||
|
.output
|
||||||
|
web/*.js
|
||||||
|
|
3
TODO.md
3
TODO.md
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
- Add click events for ansi display
|
- Add click events for ansi display
|
||||||
- Fix resizing on web_div display
|
- Fix resizing on web_div display
|
||||||
- Prevent ctrl+c on web display
|
- Ignore ctrl+c on web display (generally speaking, on displays that does not
|
||||||
|
support proper 'quit')
|
||||||
- Optimize drawing to display, by allowing sequences of characters with the same
|
- Optimize drawing to display, by allowing sequences of characters with the same
|
||||||
colors (if supported by the display)
|
colors (if supported by the display)
|
||||||
- Restore ansi terminal properly after exit (ctrl+c does not work anymore, for
|
- Restore ansi terminal properly after exit (ctrl+c does not work anymore, for
|
||||||
|
|
7
cli.ts
Executable file
7
cli.ts
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
#!./run
|
||||||
|
|
||||||
|
import { runUIDemo } from "./src/demo.ts";
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
await runUIDemo();
|
||||||
|
}
|
34
demo.ts
34
demo.ts
|
@ -1,34 +0,0 @@
|
||||||
#!./run
|
|
||||||
|
|
||||||
import { AnsiTerminalDisplay } from "./ansi.ts";
|
|
||||||
import { UIConfig } from "./config.ts";
|
|
||||||
import { TextUI } from "./ui.ts";
|
|
||||||
|
|
||||||
const display = new AnsiTerminalDisplay();
|
|
||||||
let x = 0;
|
|
||||||
const config: Partial<UIConfig> = {
|
|
||||||
palette: [
|
|
||||||
{ r: 0, g: 0, b: 0 },
|
|
||||||
{ r: 1, g: 1, b: 1 },
|
|
||||||
{ r: 0, g: 1, b: 1 },
|
|
||||||
],
|
|
||||||
onResize: draw,
|
|
||||||
onKeyStroke: (key) => {
|
|
||||||
ui.drawing.color(1, 0).text(key, { x, y: 7 });
|
|
||||||
x += key.length + 1;
|
|
||||||
},
|
|
||||||
onMouseClick: (loc) => {
|
|
||||||
const text = `${loc.x}:${loc.y}`;
|
|
||||||
ui.drawing.color(1, 0).text(text, { x, y: 7 });
|
|
||||||
x += text.length + 1;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const ui = new TextUI(display, config);
|
|
||||||
await ui.init();
|
|
||||||
draw();
|
|
||||||
await ui.loop();
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
ui.drawing.color(2, 0).text("hello", { x: 10, y: 3 });
|
|
||||||
ui.drawing.color(0, 1).text("world", { x: 10, y: 5 });
|
|
||||||
}
|
|
|
@ -2,5 +2,5 @@ export {
|
||||||
describe,
|
describe,
|
||||||
expect,
|
expect,
|
||||||
it,
|
it,
|
||||||
} from "https://js.thunderk.net/devtools@1.3.0/testing.ts";
|
} from "https://js.thunderk.net/testing@1.0.0/mod.ts";
|
||||||
export { Buffer } from "https://deno.land/std@0.106.0/io/buffer.ts";
|
export { Buffer } from "https://deno.land/std@0.106.0/io/buffer.ts";
|
||||||
|
|
45
mod.ts
45
mod.ts
|
@ -1,42 +1,3 @@
|
||||||
import { AnsiTerminalDisplay } from "./ansi.ts";
|
export { TextUI } from "./src/ui.ts";
|
||||||
import { UIConfig } from "./config.ts";
|
export { createTextUI, UI_DISPLAY_TYPES } from "./src/main.ts";
|
||||||
import { Display } from "./display.ts";
|
export { runUIDemo } from "./src/demo.ts";
|
||||||
import { TextUI } from "./ui.ts";
|
|
||||||
import { CanvasTerminalDisplay, DivTerminalDisplay } from "./web.ts";
|
|
||||||
|
|
||||||
export { TextUI };
|
|
||||||
|
|
||||||
export const UI_DISPLAY_TYPES = {
|
|
||||||
autodetect: undefined,
|
|
||||||
ansi: AnsiTerminalDisplay,
|
|
||||||
web_div: DivTerminalDisplay,
|
|
||||||
web_canvas: CanvasTerminalDisplay,
|
|
||||||
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") {
|
|
||||||
if (typeof (window as any).document != "undefined") {
|
|
||||||
display_type = "web_canvas";
|
|
||||||
// TODO if canvas is not available, fall back to div
|
|
||||||
} else if (typeof (Deno as any) != "undefined") {
|
|
||||||
display_type = "ansi";
|
|
||||||
} else {
|
|
||||||
const message = "Cannot initialize display";
|
|
||||||
if (typeof alert == "function") {
|
|
||||||
alert(message);
|
|
||||||
}
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var display = new UI_DISPLAY_TYPES[display_type]();
|
|
||||||
|
|
||||||
var ui = new TextUI(display, config);
|
|
||||||
await ui.init();
|
|
||||||
|
|
||||||
return ui;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { AnsiColorMode, AnsiTerminalDisplay } from "./ansi.ts";
|
import { AnsiColorMode, AnsiTerminalDisplay } from "./ansi.ts";
|
||||||
import { Buffer, describe, expect, it } from "./deps.testing.ts";
|
import { Buffer, describe, expect, it } from "../deps.testing.ts";
|
||||||
|
|
||||||
function createTestDisplay(): {
|
function createTestDisplay(): {
|
||||||
stdout: Buffer;
|
stdout: Buffer;
|
||||||
|
@ -18,7 +18,7 @@ describe(AnsiTerminalDisplay, () => {
|
||||||
await display.init();
|
await display.init();
|
||||||
checkSequence(stdout, "![2J");
|
checkSequence(stdout, "![2J");
|
||||||
await display.uninit();
|
await display.uninit();
|
||||||
checkSequence(stdout, "![2J![2J");
|
checkSequence(stdout, "![2J![2J![?25h!c");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes truecolor characters", async () => {
|
it("writes truecolor characters", async () => {
|
|
@ -1,5 +1,5 @@
|
||||||
import { BufferLocation, BufferSize, Char, Color } from "./base.ts";
|
import { BufferLocation, BufferSize, Char, Color } from "./base.ts";
|
||||||
import { readKeypress } from "./deps.ts";
|
import { readKeypress } from "../deps.ts";
|
||||||
import { Display } from "./display.ts";
|
import { Display } from "./display.ts";
|
||||||
|
|
||||||
export enum AnsiColorMode {
|
export enum AnsiColorMode {
|
||||||
|
@ -34,6 +34,8 @@ export class AnsiTerminalDisplay extends Display {
|
||||||
|
|
||||||
async uninit(): Promise<void> {
|
async uninit(): Promise<void> {
|
||||||
await this.writer.write(CLEAR);
|
await this.writer.write(CLEAR);
|
||||||
|
await this.setCursorVisibility(true);
|
||||||
|
await this.writer.write(RESET);
|
||||||
}
|
}
|
||||||
|
|
||||||
async flush(): Promise<void> {
|
async flush(): Promise<void> {
|
||||||
|
@ -182,6 +184,7 @@ function get256Colors(): readonly Color[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLEAR = escape("[2J");
|
const CLEAR = escape("[2J");
|
||||||
|
const RESET = escape("c");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a reader will be compatible with raw mode
|
* Check if a reader will be compatible with raw mode
|
|
@ -1,5 +1,5 @@
|
||||||
import { BufferDrawing, BufferLocation, CharBuffer } from "./base.ts";
|
import { BufferDrawing, BufferLocation, CharBuffer } from "./base.ts";
|
||||||
import { describe, expect, it } from "./deps.testing.ts";
|
import { describe, expect, it } from "../deps.testing.ts";
|
||||||
|
|
||||||
describe(CharBuffer, () => {
|
describe(CharBuffer, () => {
|
||||||
it("initializes empty, sets and gets characters", () => {
|
it("initializes empty, sets and gets characters", () => {
|
77
src/colors.ts
Normal file
77
src/colors.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { Color, PaletteMap } from "./base.ts";
|
||||||
|
import { UIPalette } from "./config.ts";
|
||||||
|
import { cmp } from "../deps.ts";
|
||||||
|
import { Display } from "./display.ts";
|
||||||
|
|
||||||
|
export async function getPaletteMapping(
|
||||||
|
palette: UIPalette,
|
||||||
|
display: Display,
|
||||||
|
): 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 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),
|
||||||
|
);
|
||||||
|
}
|
2
src/controls.ts
Normal file
2
src/controls.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export interface Control {
|
||||||
|
}
|
18
web-demo/demo.js → src/demo.ts
Normal file → Executable file
18
web-demo/demo.js → src/demo.ts
Normal file → Executable file
|
@ -1,9 +1,11 @@
|
||||||
import { createTextUI } from "./textui.js";
|
import { UIConfig } from "./config.ts";
|
||||||
|
import { createTextUI, UI_DISPLAY_TYPES } from "./main.ts";
|
||||||
|
|
||||||
export async function demo(display_type) {
|
export async function runUIDemo(
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
display_type: keyof typeof UI_DISPLAY_TYPES = "autodetect",
|
||||||
|
): Promise<void> {
|
||||||
let x = 0;
|
let x = 0;
|
||||||
const ui = await createTextUI({
|
const config: Partial<UIConfig> = {
|
||||||
palette: [
|
palette: [
|
||||||
{ r: 0, g: 0, b: 0 },
|
{ r: 0, g: 0, b: 0 },
|
||||||
{ r: 1, g: 1, b: 1 },
|
{ r: 1, g: 1, b: 1 },
|
||||||
|
@ -19,10 +21,14 @@ export async function demo(display_type) {
|
||||||
ui.drawing.color(1, 0).text(text, { x, y: 7 });
|
ui.drawing.color(1, 0).text(text, { x, y: 7 });
|
||||||
x += text.length + 1;
|
x += text.length + 1;
|
||||||
},
|
},
|
||||||
}, display_type);
|
};
|
||||||
|
const ui = await createTextUI(config, display_type);
|
||||||
|
await ui.init();
|
||||||
|
draw();
|
||||||
|
await ui.loop();
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
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.loop();
|
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { Display } from "./display.ts";
|
import { Display } from "./display.ts";
|
||||||
import { describe, expect, it } from "./deps.testing.ts";
|
import { describe, expect, it } from "../deps.testing.ts";
|
||||||
|
|
||||||
describe(Display, () => {
|
describe(Display, () => {
|
||||||
it("buffers unique events", async () => {
|
it("buffers unique events", async () => {
|
40
src/main.ts
Normal file
40
src/main.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { AnsiTerminalDisplay } from "./ansi.ts";
|
||||||
|
import { UIConfig } from "./config.ts";
|
||||||
|
import { Display } from "./display.ts";
|
||||||
|
import { TextUI } from "./ui.ts";
|
||||||
|
import { CanvasTerminalDisplay, DivTerminalDisplay } from "./web.ts";
|
||||||
|
|
||||||
|
export const UI_DISPLAY_TYPES = {
|
||||||
|
autodetect: undefined,
|
||||||
|
ansi: AnsiTerminalDisplay,
|
||||||
|
web_div: DivTerminalDisplay,
|
||||||
|
web_canvas: CanvasTerminalDisplay,
|
||||||
|
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") {
|
||||||
|
if (typeof (window as any).document != "undefined") {
|
||||||
|
display_type = "web_canvas";
|
||||||
|
// TODO if canvas is not available, fall back to div
|
||||||
|
} else if (typeof (Deno as any) != "undefined") {
|
||||||
|
display_type = "ansi";
|
||||||
|
} else {
|
||||||
|
const message = "Cannot initialize display";
|
||||||
|
if (typeof alert == "function") {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var display = new UI_DISPLAY_TYPES[display_type]();
|
||||||
|
|
||||||
|
var ui = new TextUI(display, config);
|
||||||
|
await ui.init();
|
||||||
|
|
||||||
|
return ui;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { BufferLocation, Char, Color, SPACE } from "./base.ts";
|
import { BufferLocation, Char, Color, SPACE } from "./base.ts";
|
||||||
import { describe, expect, it } from "./deps.testing.ts";
|
import { describe, expect, it } from "../deps.testing.ts";
|
||||||
import { Display } from "./display.ts";
|
import { Display } from "./display.ts";
|
||||||
import { TextUI } from "./ui.ts";
|
import { TextUI } from "./ui.ts";
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import {
|
import { BufferDrawing, BufferSize, CharBuffer, PaletteMap } from "./base.ts";
|
||||||
BufferDrawing,
|
import { getPaletteMapping } from "./colors.ts";
|
||||||
BufferSize,
|
import { UI_CONFIG_DEFAULTS, UIConfig } from "./config.ts";
|
||||||
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";
|
import { Display } from "./display.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -136,76 +130,3 @@ export class TextUI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPaletteMapping(
|
|
||||||
palette: UIPalette,
|
|
||||||
display: Display,
|
|
||||||
): 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 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),
|
|
||||||
);
|
|
||||||
}
|
|
1
web-demo/.gitignore
vendored
1
web-demo/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
textui.js
|
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="demo.css">
|
<link rel="stylesheet" href="demo.css">
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { demo } from "./demo.js";
|
import { runUIDemo } from "./mod.js";
|
||||||
demo("web_canvas");
|
runUIDemo("web_canvas");
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="stylesheet" href="demo.css">
|
<link rel="stylesheet" href="demo.css">
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { demo } from "./demo.js";
|
import { runUIDemo } from "./mod.js";
|
||||||
demo("web_div");
|
runUIDemo("web_div");
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
Loading…
Reference in a new issue