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;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
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 {
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
Loading…
Reference in a new issue