571 lines
13 KiB
JavaScript
571 lines
13 KiB
JavaScript
// Copyright 2025 Bradley D. Nelson
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
'use strict';
|
|
|
|
const MAX_BLOCKS = 16 * 1024;
|
|
const SYNC_CHUNK = 64;
|
|
const DIRTY = 1;
|
|
const LOADED = 2;
|
|
var blocks = new Uint8Array(1024 * MAX_BLOCKS);
|
|
var flags = new Uint8Array(MAX_BLOCKS);
|
|
var clipboard = [];
|
|
var screen_history = [];
|
|
var keymap = {};
|
|
var scr = 0;
|
|
var pos = 0;
|
|
var marker = 0;
|
|
var passwd = '';
|
|
var ForthKeyDown = null;
|
|
var ForthKeyPress = null;
|
|
var forth_added = false;
|
|
var ueforth = null;
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
document.body.style.margin = '0';
|
|
document.body.style.border = '0';
|
|
document.body.style.padding = '0';
|
|
var canvas = document.createElement('canvas');
|
|
document.body.appendChild(canvas);
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
function SpaceIt() {
|
|
if (!(flags[scr] & LOADED)) {
|
|
return;
|
|
}
|
|
if (blocks[scr * 1024] == 0) {
|
|
for (var i = 0; i < 1024; ++i) {
|
|
blocks[i + scr * 1024] = 32;
|
|
}
|
|
flags[scr] |= DIRTY;
|
|
}
|
|
}
|
|
|
|
function IsEmpty(n) {
|
|
for (var i = 0; i < 1024; i++) {
|
|
var ch = blocks[i + n * 1024];
|
|
if (ch != 32 && ch != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function FindEnd() {
|
|
var i = pos;
|
|
while (i < 1023 &&
|
|
(blocks[scr * 1024 + i] != 32 ||
|
|
blocks[scr * 1024 + i + 1] != 32)) {
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
function Insert() {
|
|
var end = FindEnd();
|
|
for (var i = end; i >= pos; i--) {
|
|
blocks[scr * 1024 + i + 1] = blocks[scr * 1024 + i];
|
|
}
|
|
blocks[scr * 1024 + pos] = 32;
|
|
flags[scr] |= DIRTY;
|
|
}
|
|
|
|
function Delete() {
|
|
var end = FindEnd();
|
|
for (var i = pos; i < end; i++) {
|
|
blocks[scr * 1024 + i] = blocks[scr * 1024 + i + 1];
|
|
}
|
|
flags[scr] |= DIRTY;
|
|
}
|
|
|
|
function FindSpan() {
|
|
var i = Math.floor(pos / 64) * 64;
|
|
var min = 63;
|
|
var max = 0;
|
|
for (var j = 0; j < 64; j++) {
|
|
if (blocks[scr * 1024 + i + j] != 32) {
|
|
min = Math.min(min, j);
|
|
max = Math.max(max, j);
|
|
}
|
|
}
|
|
if (min > max) {
|
|
return [i, i + 63];
|
|
}
|
|
return [i + min, i + max];
|
|
}
|
|
|
|
function Home() {
|
|
var span = FindSpan();
|
|
var start = Math.floor(pos / 64) * 64;
|
|
if (pos == span[0]) {
|
|
pos = start;
|
|
} else {
|
|
pos = span[0];
|
|
}
|
|
}
|
|
|
|
function End() {
|
|
var span = FindSpan();
|
|
var end = Math.floor(pos / 64) * 64 + 63;
|
|
var span1 = Math.min(span[1] + 1, end);
|
|
if (pos == span1) {
|
|
pos = end;
|
|
} else {
|
|
pos = span1;
|
|
}
|
|
}
|
|
|
|
function Up() {
|
|
if (pos >= 64) {
|
|
pos -= 64;
|
|
}
|
|
}
|
|
|
|
function Down() {
|
|
if (pos + 64 < 1024) {
|
|
pos += 64;
|
|
}
|
|
}
|
|
|
|
function Left() {
|
|
pos = Math.max(0, pos - 1);
|
|
}
|
|
|
|
function Right() {
|
|
pos = Math.min(1023, pos + 1);
|
|
}
|
|
|
|
function Copy() {
|
|
var row = Math.floor(pos / 64) * 64;
|
|
clipboard.push(blocks.slice(scr * 1024 + row, scr * 1024 + row + 64));
|
|
Up();
|
|
}
|
|
|
|
function Cut() {
|
|
var row = Math.floor(pos / 64) * 64;
|
|
clipboard.push(blocks.slice(scr * 1024 + row, scr * 1024 + row + 64));
|
|
for (var j = 0; j < 64; j++) {
|
|
blocks[scr * 1024 + row + j] = 32;
|
|
}
|
|
flags[scr] |= DIRTY;
|
|
Up();
|
|
}
|
|
|
|
function Paste() {
|
|
if (clipboard.length == 0) {
|
|
return;
|
|
}
|
|
var row = Math.floor(pos / 64) * 64;
|
|
var data = clipboard.pop();
|
|
for (var j = 0; j < 64; j++) {
|
|
blocks[scr * 1024 + row + j] = data[j];
|
|
}
|
|
flags[scr] |= DIRTY;
|
|
Down();
|
|
}
|
|
|
|
function Update() {
|
|
SpaceIt();
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
ctx.font = '16px consolas, Monaco, monospace';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.save();
|
|
ctx.scale(canvas.width / 64 , canvas.height / 17);
|
|
ctx.fillStyle = '#750';
|
|
if (window.onkeydown === Login) {
|
|
ctx.fillRect(10, 0, 1, 1);
|
|
} else {
|
|
for (var j = 0; j < 16; ++j) {
|
|
for (var i = 0; i < 64; ++i) {
|
|
if (pos === i + j * 64) {
|
|
ctx.fillRect(i, j, 1, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ctx.save();
|
|
var m = ctx.measureText('W');
|
|
var w = m.width;
|
|
var h = m.fontBoundingBoxAscent + m.fontBoundingBoxDescent;
|
|
ctx.scale(1 / w, 1 / h);
|
|
ctx.fillStyle = '#fb0';
|
|
if (window.onkeydown === Login) {
|
|
ctx.fillText('password:', 0.5 * w * 9, 0.5 * h);
|
|
} else {
|
|
for (var j = 0; j < 16; ++j) {
|
|
for (var i = 0; i < 64; ++i) {
|
|
var ch = String.fromCharCode(blocks[i + j * 64 + scr * 1024]);
|
|
ctx.fillText(ch, (i + 0.5) * w, (j + 0.5) * h);
|
|
}
|
|
}
|
|
}
|
|
ctx.fillStyle = '#750';
|
|
ctx.textAlign = 'right';
|
|
var info = '';
|
|
if (flags[scr] & DIRTY) {
|
|
info += 'D ';
|
|
}
|
|
if (!(flags[scr] & LOADED)) {
|
|
info += 'L ';
|
|
}
|
|
info += scr;
|
|
ctx.fillText(info, 63.5 * w, 16.5 * h);
|
|
ctx.restore();
|
|
ctx.restore();
|
|
}
|
|
|
|
function LoadChunk(n) {
|
|
if ((flags[n * SYNC_CHUNK] & LOADED)) {
|
|
return Promise.resolve();
|
|
} else {
|
|
return LoadBlocks(n * SYNC_CHUNK, (n + 1) * SYNC_CHUNK).then(function() {
|
|
Update();
|
|
});
|
|
}
|
|
}
|
|
|
|
function MaybeLoad() {
|
|
var s = Math.floor(scr / SYNC_CHUNK);
|
|
return LoadChunk(s);
|
|
}
|
|
|
|
function LineString(blk, row) {
|
|
var result = '';
|
|
for (var col = 0; col < 64; col++) {
|
|
result += String.fromCharCode(blocks[blk * 1024 + row * 64 + col]);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function BlockString(blk, linebreaks) {
|
|
var result = '';
|
|
for (var row = 0; row < 16; row++) {
|
|
result += LineString(blk, row);
|
|
if (linebreaks) {
|
|
result += '\n';
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function Eval(n) {
|
|
eval(BlockString(n, true));
|
|
}
|
|
|
|
function Print() {
|
|
var start = Math.min(marker, scr);
|
|
var end = Math.max(marker, scr);
|
|
var content = '';
|
|
content += '<!DOCTYPE html>\n';
|
|
for (var i = start; i <= end; i++) {
|
|
content += '<pre style="border: 1px solid; display: inline-block;">\n';
|
|
content += BlockString(i, true).replaceAll('<', '<');
|
|
content += '<hr/>' + i;
|
|
content += '</pre><br/>\n';
|
|
}
|
|
var blob = new Blob([content], { type: 'text/html' });
|
|
var url = URL.createObjectURL(blob);
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
function Backspace() {
|
|
if (pos > 0) {
|
|
--pos;
|
|
Delete();
|
|
}
|
|
}
|
|
|
|
function Goto(n) {
|
|
scr = n;
|
|
MaybeLoad();
|
|
}
|
|
|
|
function Gosub(n) {
|
|
screen_history.push(scr);
|
|
Goto(n);
|
|
}
|
|
|
|
function Adjust(n) {
|
|
Goto(Math.max(0, Math.min(MAX_BLOCKS - 1, scr + n)));
|
|
}
|
|
|
|
function Enter() {
|
|
pos = Math.floor((pos + 64) / 64) * 64;
|
|
if (pos > 1023) {
|
|
pos -= 64;
|
|
}
|
|
}
|
|
|
|
function ShiftUp() {
|
|
pos = (pos % 64);
|
|
}
|
|
|
|
function ShiftDown() {
|
|
pos = (pos % 64) + 15 * 64;
|
|
}
|
|
|
|
function GetLink() {
|
|
var s = BlockString(scr, false);
|
|
var at = s.indexOf('@', pos);
|
|
var paren = s.indexOf(' )', pos);
|
|
if (at >= 0 && (at < paren || paren < 0)) {
|
|
return s.slice(at).split(' ')[0];
|
|
}
|
|
if (paren >= 0) {
|
|
var p2 = s.lastIndexOf('( ', paren);
|
|
if (p2 >= 0) {
|
|
return s.slice(p2, paren + 2);
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function Find(s) {
|
|
for (var i = 0; i < MAX_BLOCKS; i++) {
|
|
var j = (scr + 1 + i) % MAX_BLOCKS;
|
|
if (BlockString(j, false).indexOf(s) >= 0) {
|
|
return j;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function FollowLink() {
|
|
var link = GetLink();
|
|
if (link.startsWith('@')) {
|
|
var n = parseInt(link.slice(1));
|
|
Gosub(n);
|
|
} else if (link.startsWith('( ')) {
|
|
var n = Find(link.slice(2, -2));
|
|
if (n !== null) {
|
|
Gosub(n);
|
|
}
|
|
}
|
|
}
|
|
|
|
function Type(ch) {
|
|
Insert();
|
|
blocks[pos + scr * 1024] = ch;
|
|
pos = Math.min(1023, pos + 1);
|
|
}
|
|
|
|
function Key(e) {
|
|
if (e.ctrlKey && keymap['^' + e.key]) {
|
|
keymap['^' + e.key](e);
|
|
} else if (e.shiftKey && keymap['+' + e.key]) {
|
|
keymap['+' + e.key](e);
|
|
} else if (keymap[e.key]) {
|
|
keymap[e.key](e);
|
|
} else if (e.key.length == 1 && !e.ctrlKey) {
|
|
Type(e.key.charCodeAt(0));
|
|
} else {
|
|
return true;
|
|
}
|
|
Update();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
function Login(e) {
|
|
if (e.key == 'Backspace') {
|
|
passwd = passwd.slice(0, -1);
|
|
} else if (e.key == 'Enter') {
|
|
window.onkeydown = Key;
|
|
MaybeLoad().then(function() {
|
|
Eval(63);
|
|
});
|
|
} else if (e.key.length == 1) {
|
|
passwd += e.key;
|
|
}
|
|
Update();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
function Resize() {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
Update();
|
|
}
|
|
|
|
function SaveBlock(i) {
|
|
var fd = new FormData();
|
|
fd.append('command', 'write');
|
|
fd.append('passwd', passwd);
|
|
fd.append('index', i);
|
|
fd.append('data', new Blob([blocks.slice(i * 1024, (i + 1) * 1024)],
|
|
{type: "application/octet-stream"}));
|
|
return fetch('/io', {'method': 'POST', body: fd}).then(function() {
|
|
flags[i] &= ~DIRTY;
|
|
Update();
|
|
});
|
|
}
|
|
|
|
function Sync() {
|
|
for (var i = 0; i < MAX_BLOCKS; ++i) {
|
|
if ((flags[i] & LOADED) && (flags[i] & DIRTY)) {
|
|
SaveBlock(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
function FindEmpty() {
|
|
screen_history.push(scr);
|
|
while (!IsEmpty(scr)) {
|
|
Adjust(1);
|
|
}
|
|
}
|
|
|
|
function StampLog() {
|
|
var now = new Date();
|
|
var month = ('0' + (1 + now.getMonth())).slice(-2);
|
|
var day = ('0' + now.getDate()).slice(-2);
|
|
var dt = now.getFullYear() + '-' + month + '-' + day;
|
|
for (var i = 0; i < dt.length; i++) {
|
|
blocks[54 + i + scr * 1024] = dt.charCodeAt(i);
|
|
}
|
|
var mark = 'LOG: ';
|
|
for (var i = 0; i < mark.length; i++) {
|
|
blocks[i + scr * 1024] = mark.charCodeAt(i);
|
|
}
|
|
pos = mark.length;
|
|
flags[scr] |= DIRTY;
|
|
}
|
|
|
|
function Back() {
|
|
if (screen_history.length) {
|
|
Goto(screen_history.pop());
|
|
}
|
|
}
|
|
|
|
function LoadBlocks(start, end) {
|
|
var fd = new FormData();
|
|
fd.append('command', 'read');
|
|
fd.append('passwd', passwd);
|
|
fd.append('start', start);
|
|
fd.append('end', end);
|
|
return fetch('/io', {'method': 'POST', body: fd}).then(function(response) {
|
|
if (!response.ok) {
|
|
throw 'bad fetch';
|
|
}
|
|
return response.arrayBuffer().then(function(data) {
|
|
var u8 = new Uint8Array(data);
|
|
if (u8.length != (end + 1 - start) * 1024) {
|
|
throw 'bad load';
|
|
}
|
|
for (var i = start; i < end; i++) {
|
|
if (flags[i] & LOADED) {
|
|
return;
|
|
}
|
|
var dst = i * 1024;
|
|
var src = (i - start) * 1024;
|
|
for (var j = 0; j < 1024; j++) {
|
|
blocks[j + dst] = u8[j + src];
|
|
}
|
|
flags[i] |= LOADED;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function ForthKeyFilter(e) {
|
|
if (e.key == 'f' && e.ctrlKey) {
|
|
ToggleForth();
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
return ForthKeyDown(e);
|
|
}
|
|
|
|
function ToggleForth() {
|
|
if (!forth_added) {
|
|
forth_added = true;
|
|
var fscript = document.createElement('script');
|
|
fscript.src = 'myforth.fs';
|
|
fscript.type = 'text/forth';
|
|
document.body.appendChild(fscript);
|
|
var script = document.createElement('script');
|
|
script.src = 'https://eforth.appspot.com/ueforth.js';
|
|
document.body.appendChild(script);
|
|
function Loader() {
|
|
if (ueforth !== null) {
|
|
ueforth.Start();
|
|
canvas.style.display = 'none';
|
|
setTimeout(function() {
|
|
ForthKeyDown = window.onkeydown;
|
|
ForthKeyPress = window.onkeypress;
|
|
window.onkeydown = ForthKeyFilter;
|
|
}, 500);
|
|
} else {
|
|
setTimeout(Loader, 10);
|
|
}
|
|
}
|
|
Loader();
|
|
return;
|
|
}
|
|
if (window.onkeydown === Key) {
|
|
window.onkeydown = ForthKeyFilter;
|
|
window.onkeypress = ForthKeyPress;
|
|
canvas.style.display = 'none';
|
|
ueforth.screen.style.display = '';
|
|
} else {
|
|
window.onkeydown = Key;
|
|
window.onkeypress = null;
|
|
ueforth.screen.style.display = 'none';
|
|
canvas.style.display = '';
|
|
Resize();
|
|
}
|
|
}
|
|
|
|
function Init() {
|
|
keymap['Delete'] = Delete;
|
|
keymap['Backspace'] = Backspace;
|
|
keymap['PageUp'] = function() { Adjust(-1); };
|
|
keymap['PageDown'] = function() { Adjust(1); };
|
|
keymap['+PageUp'] = function() { Adjust(-16); };
|
|
keymap['+PageDown'] = function() { Adjust(16); };
|
|
keymap['Home'] = Home;
|
|
keymap['End'] = End;
|
|
keymap['Enter'] = Enter;
|
|
keymap['^Enter'] = FollowLink;
|
|
keymap['ArrowUp'] = Up;
|
|
keymap['ArrowDown'] = Down;
|
|
keymap['ArrowLeft'] = Left;
|
|
keymap['ArrowRight'] = Right;
|
|
keymap['+ArrowUp'] = ShiftUp;
|
|
keymap['+ArrowDown'] = ShiftDown;
|
|
keymap['+ArrowLeft'] = Home;
|
|
keymap['+ArrowRight'] = End;
|
|
keymap['^c'] = Copy;
|
|
keymap['^x'] = Cut;
|
|
keymap['^v'] = Paste;
|
|
keymap['^m'] = function() { marker = scr; };
|
|
keymap['^p'] = Print;
|
|
keymap['^g'] = function() { Eval(scr); };
|
|
keymap['^o'] = FindEmpty;
|
|
keymap['^l'] = StampLog;
|
|
keymap['^b'] = Back;
|
|
keymap['^f'] = ToggleForth;
|
|
keymap['^h'] = function() { Gosub(0); };
|
|
|
|
window.addEventListener('resize', Resize);
|
|
window.onkeydown = Login;
|
|
Resize();
|
|
}
|
|
Init();
|
|
|
|
setInterval(Sync, 3000);
|