From 2a6d7618e63b06fa197c2478aab7afbad92274e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Remko=20Tron=C3=A7on?= Date: Fri, 23 Dec 2022 19:25:18 +0100 Subject: [PATCH] shell: Refactor --- src/web/notebook/src/CodeCell.css | 66 +++++++++++ src/web/notebook/src/CodeCell.tsx | 126 ++++++++++++++++++++ src/web/notebook/src/wafnb.css | 67 ----------- src/web/notebook/src/wafnb.tsx | 120 +------------------ src/web/shell/Console.css | 41 +++++++ src/web/shell/Console.ts | 186 ++++++++++++++++++++++++++++++ src/web/shell/shell.css | 41 +------ src/web/shell/shell.js | 179 +--------------------------- 8 files changed, 428 insertions(+), 398 deletions(-) create mode 100644 src/web/notebook/src/CodeCell.css create mode 100644 src/web/notebook/src/CodeCell.tsx create mode 100644 src/web/shell/Console.css create mode 100644 src/web/shell/Console.ts diff --git a/src/web/notebook/src/CodeCell.css b/src/web/notebook/src/CodeCell.css new file mode 100644 index 0000000..98adfeb --- /dev/null +++ b/src/web/notebook/src/CodeCell.css @@ -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; +} diff --git a/src/web/notebook/src/CodeCell.tsx b/src/web/notebook/src/CodeCell.tsx new file mode 100644 index 0000000..229d012 --- /dev/null +++ b/src/web/notebook/src/CodeCell.tsx @@ -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 = () => ( + + + +); + +export const clearIcon = () => ( + + + +); + +export function renderCodeCells() { + const runs: Array<() => Promise> = []; + 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 =
; + 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 = ( + + ); + const consoleEl: HTMLPreElement =
;
+        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 = (
+      
+    );
+    const clearEl = (
+      
+    );
+    clearEl.style.display = "none";
+    n.insertBefore(
+      
+ {runEl} + {clearEl} +
, + editor.el + ); + } + + return { runs, clears, setEnableds }; +} diff --git a/src/web/notebook/src/wafnb.css b/src/web/notebook/src/wafnb.css index aea3fda..ffb091a 100644 --- a/src/web/notebook/src/wafnb.css +++ b/src/web/notebook/src/wafnb.css @@ -55,76 +55,9 @@ code.raw-code-cell { 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 { float: right; display: flex; gap: 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; -} diff --git a/src/web/notebook/src/wafnb.tsx b/src/web/notebook/src/wafnb.tsx index 65c43dc..e4e4997 100644 --- a/src/web/notebook/src/wafnb.tsx +++ b/src/web/notebook/src/wafnb.tsx @@ -1,123 +1,8 @@ import "./wafnb.css"; -import Editor from "../../thurtle/Editor"; import * as jsx from "../../thurtle/jsx"; -import draw from "../../thurtle/draw"; -import { isSuccess, withLineBuffer } from "waforth"; +import { runIcon, clearIcon, renderCodeCells } from "./CodeCell"; -const runIcon = () => ( - - - -); - -const clearIcon = () => ( - - - -); - -const runs: Array<() => Promise> = []; -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 =
; - 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 = ; - const consoleEl: HTMLPreElement =
;
-      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 = (
-    
-  );
-  const clearEl = (
-    
-  );
-  clearEl.style.display = "none";
-  n.insertBefore(
-    
- {runEl} - {clearEl} -
, - editor.el - ); -} +const { setEnableds, runs, clears } = renderCodeCells(); function setAllEnabled(v: boolean) { for (const setEnabled of setEnableds) { @@ -126,6 +11,7 @@ function setAllEnabled(v: boolean) { runAllButtonEl.disabled = !v; clearAllButtonEl.disabled = !v; } + async function runAll() { setAllEnabled(false); try { diff --git a/src/web/shell/Console.css b/src/web/shell/Console.css new file mode 100644 index 0000000..a22aa5e --- /dev/null +++ b/src/web/shell/Console.css @@ -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; +} diff --git a/src/web/shell/Console.ts b/src/web/shell/Console.ts new file mode 100644 index 0000000..77a35ef --- /dev/null +++ b/src/web/shell/Console.ts @@ -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 = `WAForth${ + version != null ? ` (${version})` : "" + }\n `; + 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 = {}; + 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); + } + })(); +} diff --git a/src/web/shell/shell.css b/src/web/shell/shell.css index 5402849..a81b324 100644 --- a/src/web/shell/shell.css +++ b/src/web/shell/shell.css @@ -11,52 +11,19 @@ html { body { height: 100vh; margin: 0; - background-color: black; + background-color: #111; color: gray; } -.console { +.container { 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; left: 0; right: 0; bottom: 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; -} - -input { +body .Console { + height: 100%; width: 100%; - opacity: 0; - visibility: hidden; } diff --git a/src/web/shell/shell.js b/src/web/shell/shell.js index c5881f5..fa5a07f 100644 --- a/src/web/shell/shell.js +++ b/src/web/shell/shell.js @@ -1,179 +1,4 @@ -/* global WAFORTH_VERSION */ - -import WAForth, { withCharacterBuffer } from "../waforth"; import "./shell.css"; +import { renderConsole } from "./Console"; -const version = - 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 = `WAForth (${version})\n `; - 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); - } -})(); +renderConsole(document.body);