mirror of
https://github.com/remko/waforth
synced 2024-12-26 09:59:09 +01:00
shell: Refactor
This commit is contained in:
parent
b00f426b7f
commit
2a6d7618e6
8 changed files with 428 additions and 398 deletions
66
src/web/notebook/src/CodeCell.css
Normal file
66
src/web/notebook/src/CodeCell.css
Normal 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;
|
||||
}
|
126
src/web/notebook/src/CodeCell.tsx
Normal file
126
src/web/notebook/src/CodeCell.tsx
Normal 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 };
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = () => (
|
||||
<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
|
||||
);
|
||||
}
|
||||
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 {
|
||||
|
|
41
src/web/shell/Console.css
Normal file
41
src/web/shell/Console.css
Normal 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
186
src/web/shell/Console.ts
Normal 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);
|
||||
}
|
||||
})();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 = `<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);
|
||||
}
|
||||
})();
|
||||
renderConsole(document.body);
|
||||
|
|
Loading…
Reference in a new issue