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 = ` `;
+ 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 = ` `;
- 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);