shell: Refactor

This commit is contained in:
Remko Tronçon 2022-12-23 19:25:18 +01:00
parent b00f426b7f
commit 2a6d7618e6
8 changed files with 428 additions and 398 deletions

View file

@ -0,0 +1,66 @@
.code-cell {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}
.code-cell .output {
flex: 1 1 15em;
gap: 0.125em;
display: flex;
flex-direction: column;
}
.code-cell .console {
font-family: monospace;
background-color: #111;
color: white;
padding: 0.5em 1em;
border-radius: 0.25em;
margin: 0;
white-space: pre-wrap;
}
.code-cell .editor {
flex: 1 1 15em;
background-color: #eee;
box-sizing: content-box;
}
.code-cell .world {
border: thin solid rgb(171, 208, 166);
background-color: rgb(221, 248, 221);
border-radius: 10px;
width: 100%;
max-height: 15em;
}
.code-cell .controls {
float: left;
margin-left: -28px;
display: flex;
flex-direction: column;
}
.bi {
display: inline-block;
vertical-align: -0.125em;
fill: currentcolor;
}
.toolbutton {
display: inline-block;
border: none;
border-radius: 0.5em;
background: none;
color: #888;
height: 24px;
width: 24px;
padding: 0;
cursor: pointer;
}
.toolbutton:hover {
background-color: rgba(0, 0, 0, 0.1);
color: inherit;
}

View file

@ -0,0 +1,126 @@
import * as jsx from "../../thurtle/jsx";
import Editor from "../../thurtle/Editor";
import draw from "../../thurtle/draw";
import { isSuccess, withLineBuffer } from "waforth";
import "./CodeCell.css";
export const runIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi"
viewBox="0 0 16 16"
>
<path
xmlns="http://www.w3.org/2000/svg"
d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"
/>
</svg>
);
export const clearIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi"
viewBox="0 0 16 16"
>
<path
xmlns="http://www.w3.org/2000/svg"
d="M8.086 2.207a2 2 0 0 1 2.828 0l3.879 3.879a2 2 0 0 1 0 2.828l-5.5 5.5A2 2 0 0 1 7.879 15H5.12a2 2 0 0 1-1.414-.586l-2.5-2.5a2 2 0 0 1 0-2.828l6.879-6.879zm2.121.707a1 1 0 0 0-1.414 0L4.16 7.547l5.293 5.293 4.633-4.633a1 1 0 0 0 0-1.414l-3.879-3.879zM8.746 13.547 3.453 8.254 1.914 9.793a1 1 0 0 0 0 1.414l2.5 2.5a1 1 0 0 0 .707.293H7.88a1 1 0 0 0 .707-.293l.16-.16z"
/>
</svg>
);
export function renderCodeCells() {
const runs: Array<() => Promise<void>> = [];
const clears: Array<() => void> = [];
const setEnableds: Array<(v: boolean) => void> = [];
for (const n of document.querySelectorAll("[data-hook=code-cell")) {
n.className = "code-cell";
const program = n.textContent ?? "";
const editor = new Editor(true);
editor.setValue(program);
n.innerHTML = "";
n.appendChild(editor.el);
const outputEl = <div class="output" />;
outputEl.style.display = "none";
n.appendChild(outputEl);
const setEnabled = (v: boolean) => {
runEl.disabled = !v;
};
setEnableds.push(setEnabled);
const clear = () => {
outputEl.style.display = "none";
outputEl.innerHTML = "";
clearEl.style.display = "none";
editor.el.style.borderColor = "#ced4da";
};
clears.push(clear);
const run = async () => {
setEnabled(false);
try {
clear();
const worldEl = (
<svg class="world" xmlns="http://www.w3.org/2000/svg" />
);
const consoleEl: HTMLPreElement = <pre class="console" />;
const result = await draw({
program: editor.getValue(),
drawEl: worldEl,
onEmit: (c: string) => {
consoleEl.appendChild(document.createTextNode(c));
},
jsx,
});
if (!result.isEmpty) {
outputEl.appendChild(worldEl);
outputEl.style.display = "flex";
}
if (consoleEl.childNodes.length > 0) {
outputEl.appendChild(consoleEl);
outputEl.style.display = "flex";
}
clearEl.style.display = "block";
editor.el.style.borderColor = isSuccess(result.result)
? "rgb(60, 166, 60)"
: "rgb(208, 49, 49)";
} catch (e) {
alert((e as any).message);
} finally {
setEnabled(true);
}
};
runs.push(run);
const runEl = (
<button title="Run" class="toolbutton" onclick={run}>
{runIcon()}
</button>
);
const clearEl = (
<button title="Clear" class="toolbutton" onclick={clear}>
{clearIcon()}
</button>
);
clearEl.style.display = "none";
n.insertBefore(
<div class="controls">
{runEl}
{clearEl}
</div>,
editor.el
);
}
return { runs, clears, setEnableds };
}

View file

@ -55,76 +55,9 @@ code.raw-code-cell {
background-color: #eee; background-color: #eee;
} }
.code-cell {
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}
.code-cell .output {
flex: 1 1 15em;
gap: 0.125em;
display: flex;
flex-direction: column;
}
.console {
font-family: monospace;
background-color: #111;
color: white;
padding: 0.5em 1em;
border-radius: 0.25em;
margin: 0;
white-space: pre-wrap;
}
.code-cell .editor {
flex: 1 1 15em;
background-color: #eee;
box-sizing: content-box;
}
.world {
border: thin solid rgb(171, 208, 166);
background-color: rgb(221, 248, 221);
border-radius: 10px;
width: 100%;
max-height: 15em;
}
.all-controls { .all-controls {
float: right; float: right;
display: flex; display: flex;
gap: 4px; gap: 4px;
margin-top: 4px; margin-top: 4px;
} }
.controls {
float: left;
margin-left: -28px;
display: flex;
flex-direction: column;
}
.bi {
display: inline-block;
vertical-align: -0.125em;
fill: currentcolor;
}
.toolbutton {
display: inline-block;
border: none;
border-radius: 0.5em;
background: none;
color: #888;
height: 24px;
width: 24px;
padding: 0;
cursor: pointer;
}
.toolbutton:hover {
background-color: rgba(0, 0, 0, 0.1);
color: inherit;
}

View file

@ -1,123 +1,8 @@
import "./wafnb.css"; import "./wafnb.css";
import Editor from "../../thurtle/Editor";
import * as jsx from "../../thurtle/jsx"; import * as jsx from "../../thurtle/jsx";
import draw from "../../thurtle/draw"; import { runIcon, clearIcon, renderCodeCells } from "./CodeCell";
import { isSuccess, withLineBuffer } from "waforth";
const runIcon = () => ( const { setEnableds, runs, clears } = renderCodeCells();
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi"
viewBox="0 0 16 16"
>
<path
xmlns="http://www.w3.org/2000/svg"
d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"
/>
</svg>
);
const clearIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi"
viewBox="0 0 16 16"
>
<path
xmlns="http://www.w3.org/2000/svg"
d="M8.086 2.207a2 2 0 0 1 2.828 0l3.879 3.879a2 2 0 0 1 0 2.828l-5.5 5.5A2 2 0 0 1 7.879 15H5.12a2 2 0 0 1-1.414-.586l-2.5-2.5a2 2 0 0 1 0-2.828l6.879-6.879zm2.121.707a1 1 0 0 0-1.414 0L4.16 7.547l5.293 5.293 4.633-4.633a1 1 0 0 0 0-1.414l-3.879-3.879zM8.746 13.547 3.453 8.254 1.914 9.793a1 1 0 0 0 0 1.414l2.5 2.5a1 1 0 0 0 .707.293H7.88a1 1 0 0 0 .707-.293l.16-.16z"
/>
</svg>
);
const runs: Array<() => Promise<void>> = [];
const clears: Array<() => void> = [];
const setEnableds: Array<(v: boolean) => void> = [];
for (const n of document.querySelectorAll("[data-hook=code-cell")) {
n.className = "code-cell";
const program = n.textContent ?? "";
const editor = new Editor(true);
editor.setValue(program);
n.innerHTML = "";
n.appendChild(editor.el);
const outputEl = <div class="output" />;
outputEl.style.display = "none";
n.appendChild(outputEl);
const setEnabled = (v: boolean) => {
runEl.disabled = !v;
};
setEnableds.push(setEnabled);
const clear = () => {
outputEl.style.display = "none";
outputEl.innerHTML = "";
clearEl.style.display = "none";
editor.el.style.borderColor = "#ced4da";
};
clears.push(clear);
const run = async () => {
setEnabled(false);
try {
clear();
const worldEl = <svg class="world" xmlns="http://www.w3.org/2000/svg" />;
const consoleEl: HTMLPreElement = <pre class="console" />;
const result = await draw({
program: editor.getValue(),
drawEl: worldEl,
onEmit: (c: string) => {
consoleEl.appendChild(document.createTextNode(c));
},
jsx,
});
if (!result.isEmpty) {
outputEl.appendChild(worldEl);
outputEl.style.display = "flex";
}
if (consoleEl.childNodes.length > 0) {
outputEl.appendChild(consoleEl);
outputEl.style.display = "flex";
}
clearEl.style.display = "block";
editor.el.style.borderColor = isSuccess(result.result)
? "rgb(60, 166, 60)"
: "rgb(208, 49, 49)";
} catch (e) {
alert((e as any).message);
} finally {
setEnabled(true);
}
};
runs.push(run);
const runEl = (
<button title="Run" class="toolbutton" onclick={run}>
{runIcon()}
</button>
);
const clearEl = (
<button title="Clear" class="toolbutton" onclick={clear}>
{clearIcon()}
</button>
);
clearEl.style.display = "none";
n.insertBefore(
<div class="controls">
{runEl}
{clearEl}
</div>,
editor.el
);
}
function setAllEnabled(v: boolean) { function setAllEnabled(v: boolean) {
for (const setEnabled of setEnableds) { for (const setEnabled of setEnableds) {
@ -126,6 +11,7 @@ function setAllEnabled(v: boolean) {
runAllButtonEl.disabled = !v; runAllButtonEl.disabled = !v;
clearAllButtonEl.disabled = !v; clearAllButtonEl.disabled = !v;
} }
async function runAll() { async function runAll() {
setAllEnabled(false); setAllEnabled(false);
try { try {

41
src/web/shell/Console.css Normal file
View file

@ -0,0 +1,41 @@
.Console {
overflow-y: scroll;
background-color: #111;
font-family: monospace;
padding: 1em;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
}
.Console .header {
color: #00ff00;
}
.Console .header a {
color: inherit;
text-decoration: none;
}
.Console .cursor {
background-color: gray;
}
.Console .in {
color: #bb0;
}
.Console .out {
color: white;
}
.Console .error {
color: red;
}
.Console input {
width: 100%;
height: 0;
opacity: 0;
visibility: hidden;
}

186
src/web/shell/Console.ts Normal file
View file

@ -0,0 +1,186 @@
import WAForth, { withCharacterBuffer } from "../waforth";
import "./Console.css";
declare let WAFORTH_VERSION: string;
const version = typeof WAFORTH_VERSION !== "undefined" ? WAFORTH_VERSION : "";
export function renderConsole(parentEl: HTMLElement) {
const forth = new WAForth();
const consoleEl = document.createElement("pre");
consoleEl.className = "Console";
parentEl.appendChild(consoleEl);
let inputEl: HTMLElement;
let cursorEl: HTMLElement;
consoleEl.addEventListener("click", () => {
inputEl.style.visibility = "visible";
inputEl.focus();
inputEl.style.visibility = "hidden";
});
let currentConsoleEl: HTMLElement;
let currentConsoleElIsInput = false;
let outputBuffer: string[] = [];
function flush() {
if (outputBuffer.length == 0) {
return;
}
currentConsoleEl.appendChild(
document.createTextNode(outputBuffer.join(""))
);
outputBuffer = [];
parentEl.querySelector(".cursor")!.scrollIntoView(false);
}
function output(s: string, isInput: boolean, forceFlush = false) {
if (currentConsoleEl != null && currentConsoleElIsInput !== isInput) {
flush();
}
if (currentConsoleEl == null || currentConsoleElIsInput !== isInput) {
currentConsoleEl = document.createElement("span");
currentConsoleEl.className = isInput ? "in" : "out";
currentConsoleElIsInput = isInput;
consoleEl.insertBefore(currentConsoleEl, cursorEl);
}
outputBuffer.push(s);
if (forceFlush || isInput || s.endsWith("\n")) {
flush();
}
}
function unoutput(isInput: boolean) {
if (
currentConsoleElIsInput !== isInput ||
currentConsoleEl.lastChild == null
) {
console.log("not erasing character");
return;
}
currentConsoleEl.lastChild.remove();
}
function startConsole() {
let inputbuffer: string[] = [];
function load(s: string) {
const commands = s.split("\n");
const newInputBuffer: string[] = [];
if (commands.length > 0) {
newInputBuffer.push(commands.pop()!);
}
for (const command of commands) {
output(command, true);
output(" ", true);
forth.interpret(inputbuffer.join("") + command);
inputbuffer = [];
}
if (newInputBuffer.length > 0) {
output(newInputBuffer.join(""), true);
flush();
}
inputbuffer = newInputBuffer;
}
parentEl.addEventListener("keydown", (ev) => {
// console.log("keydown", ev);
if (ev.key === "Enter") {
output(" ", true);
forth.interpret(inputbuffer.join(""));
inputbuffer = [];
} else if (ev.key === "Backspace") {
if (inputbuffer.length > 0) {
inputbuffer = inputbuffer.slice(0, inputbuffer.length - 1);
unoutput(true);
}
} else if (ev.key.length === 1 && !ev.metaKey && !ev.ctrlKey) {
output(ev.key, true);
inputbuffer.push(ev.key);
} else if (ev.key === "o" && (ev.metaKey || ev.ctrlKey)) {
if (!(window as any).showOpenFilePicker) {
window.alert("File loading not supported on this browser");
return;
}
(async () => {
const [fh] = await (window as any).showOpenFilePicker({
types: [
{
description: "Forth source files",
accept: {
"text/plain": [".fs", ".f", ".fth", ".f4th", ".fr"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
});
load(await (await fh.getFile()).text());
})();
} else {
console.log("ignoring key %s", ev.key);
}
if (ev.key === " ") {
ev.preventDefault();
}
});
parentEl.addEventListener("paste", (event) => {
load(
(event.clipboardData || (window as any).clipboardData).getData("text")
);
});
}
function clearConsole() {
consoleEl.innerHTML = `<span class='header'><a target='_blank' href='https://github.com/remko/waforth'>WAForth${
version != null ? ` (${version})` : ""
}</a>\n</span><span class="cursor"> </span><input type="text">`;
inputEl = parentEl.querySelector("input")!;
cursorEl = parentEl.querySelector(".cursor")!;
}
forth.onEmit = withCharacterBuffer((c) => {
output(c, false);
});
clearConsole();
(async () => {
output("Loading core ... ", false, true);
try {
await forth.load();
clearConsole();
startConsole();
// Parse query string
const qs: Record<string, string> = {};
for (const p of window.location.search
.substring(window.location.search.indexOf("?") + 1)
.replace(/\+/, " ")
.split("&")) {
const j = p.indexOf("=");
if (j > 0) {
qs[decodeURIComponent(p.substring(0, j))] = decodeURIComponent(
p.substring(j + 1)
);
}
}
if (qs.p != null) {
for (const command of qs.p.split("\n")) {
output(command, true);
output(" ", true);
forth.interpret(command);
}
}
} catch (e) {
console.error(e);
const errorEl = document.createElement("span");
errorEl.className = "error";
errorEl.innerText = "error";
cursorEl!.remove();
inputEl!.remove();
consoleEl.appendChild(errorEl);
}
})();
}

View file

@ -11,52 +11,19 @@ html {
body { body {
height: 100vh; height: 100vh;
margin: 0; margin: 0;
background-color: black; background-color: #111;
color: gray; color: gray;
} }
.console { .container {
position: absolute; position: absolute;
overflow-y: scroll;
background-color: black;
font-family: monospace;
padding: 1em;
white-space: pre-wrap;
word-wrap: break-word;
margin: 0;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
} }
.console .header { body .Console {
color: #00ff00; height: 100%;
}
.console .header a {
color: inherit;
text-decoration: none;
}
.console .cursor {
background-color: gray;
}
.console .in {
color: #bb0;
}
.console .out {
color: white;
}
.console .error {
color: red;
}
input {
width: 100%; width: 100%;
opacity: 0;
visibility: hidden;
} }

View file

@ -1,179 +1,4 @@
/* global WAFORTH_VERSION */
import WAForth, { withCharacterBuffer } from "../waforth";
import "./shell.css"; import "./shell.css";
import { renderConsole } from "./Console";
const version = renderConsole(document.body);
typeof WAFORTH_VERSION !== "undefined" ? WAFORTH_VERSION : "dev";
const forth = new WAForth();
const consoleEl = document.createElement("pre");
consoleEl.className = "console";
document.body.appendChild(consoleEl);
let inputEl;
let cursorEl;
consoleEl.addEventListener("click", () => {
inputEl.style.visibility = "visible";
inputEl.focus();
inputEl.style.visibility = "hidden";
});
let currentConsoleEl;
let currentConsoleElIsInput = false;
let outputBuffer = [];
function flush() {
if (outputBuffer.length == 0) {
return;
}
currentConsoleEl.appendChild(document.createTextNode(outputBuffer.join("")));
outputBuffer = [];
document.querySelector(".cursor").scrollIntoView(false);
}
function output(s, isInput, forceFlush = false) {
if (currentConsoleEl != null && currentConsoleElIsInput !== isInput) {
flush();
}
if (currentConsoleEl == null || currentConsoleElIsInput !== isInput) {
currentConsoleEl = document.createElement("span");
currentConsoleEl.className = isInput ? "in" : "out";
currentConsoleElIsInput = isInput;
consoleEl.insertBefore(currentConsoleEl, cursorEl);
}
outputBuffer.push(s);
if (forceFlush || isInput || s.endsWith("\n")) {
flush();
}
}
function unoutput(isInput) {
if (
currentConsoleElIsInput !== isInput ||
currentConsoleEl.lastChild == null
) {
console.log("not erasing character");
return;
}
currentConsoleEl.lastChild.remove();
}
function startConsole() {
let inputbuffer = [];
function load(s) {
const commands = s.split("\n");
let newInputBuffer = [];
if (commands.length > 0) {
newInputBuffer.push(commands.pop());
}
for (const command of commands) {
output(command, true);
output(" ", true);
forth.interpret(inputbuffer.join("") + command);
inputbuffer = [];
}
if (newInputBuffer.length > 0) {
output(newInputBuffer.join(""), true);
flush();
}
inputbuffer = newInputBuffer;
}
document.addEventListener("keydown", (ev) => {
// console.log("keydown", ev);
if (ev.key === "Enter") {
output(" ", true);
forth.interpret(inputbuffer.join(""));
inputbuffer = [];
} else if (ev.key === "Backspace") {
if (inputbuffer.length > 0) {
inputbuffer = inputbuffer.slice(0, inputbuffer.length - 1);
unoutput(true);
}
} else if (ev.key.length === 1 && !ev.metaKey && !ev.ctrlKey) {
output(ev.key, true);
inputbuffer.push(ev.key);
} else if (ev.key === "o" && (ev.metaKey || ev.ctrlKey)) {
if (!window.showOpenFilePicker) {
window.alert("File loading not supported on this browser");
return;
}
(async () => {
const [fh] = await window.showOpenFilePicker({
types: [
{
description: "Forth source files",
accept: {
"text/plain": [".fs", ".f", ".fth", ".f4th", ".fr"],
},
},
],
excludeAcceptAllOption: true,
multiple: false,
});
load(await (await fh.getFile()).text());
})();
} else {
console.log("ignoring key %s", ev.key);
}
if (ev.key === " ") {
ev.preventDefault();
}
});
document.addEventListener("paste", (event) => {
load(event.clipboardData || window.clipboardData).getData("text");
});
}
function clearConsole() {
consoleEl.innerHTML = `<span class='header'><a target='_blank' href='https://github.com/remko/waforth'>WAForth (${version})</a>\n</span><span class="cursor"> </span><input type="text">`;
inputEl = document.querySelector("input");
cursorEl = document.querySelector(".cursor");
}
forth.onEmit = withCharacterBuffer((c) => {
output(c, false);
});
clearConsole();
(async () => {
output("Loading core ... ", false, true);
try {
await forth.load();
clearConsole();
startConsole();
// Parse query string
const qs = {};
for (const p of window.location.search
.substring(window.location.search.indexOf("?") + 1)
.replace(/\+/, " ")
.split("&")) {
const j = p.indexOf("=");
if (j > 0) {
qs[decodeURIComponent(p.substring(0, j))] = decodeURIComponent(
p.substring(j + 1)
);
}
}
if (qs.p != null) {
for (const command of qs.p.split("\n")) {
output(command, true);
output(" ", true);
forth.interpret(command);
}
}
} catch (e) {
console.error(e);
const errorEl = document.createElement("span");
errorEl.className = "error";
errorEl.innerText = "error";
cursorEl.remove();
inputEl.remove();
consoleEl.appendChild(errorEl);
}
})();