From 3a5daad8af298ebd0612eb21595d44fb15650a21 Mon Sep 17 00:00:00 2001 From: Brad Nelson Date: Sat, 10 May 2025 12:41:18 -0700 Subject: [PATCH] Adding jotblk sample. A web editor that embeds uEforth. --- .gitignore | 1 + attic/jotblk/Makefile | 12 + attic/jotblk/README.md | 20 ++ attic/jotblk/app.yaml | 17 + attic/jotblk/dstore.sh | 2 + attic/jotblk/main.py | 81 +++++ attic/jotblk/requirements.txt | 2 + attic/jotblk/run.sh | 8 + attic/jotblk/static/index.html | 3 + attic/jotblk/static/jotblk.js | 562 +++++++++++++++++++++++++++++++++ attic/jotblk/static/myforth.fs | 43 +++ 11 files changed, 751 insertions(+) create mode 100755 attic/jotblk/Makefile create mode 100644 attic/jotblk/README.md create mode 100644 attic/jotblk/app.yaml create mode 100644 attic/jotblk/dstore.sh create mode 100644 attic/jotblk/main.py create mode 100644 attic/jotblk/requirements.txt create mode 100755 attic/jotblk/run.sh create mode 100644 attic/jotblk/static/index.html create mode 100644 attic/jotblk/static/jotblk.js create mode 100644 attic/jotblk/static/myforth.fs diff --git a/.gitignore b/.gitignore index ac12813..39e403e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ out build.ninja +attic/jotblk/env diff --git a/attic/jotblk/Makefile b/attic/jotblk/Makefile new file mode 100755 index 0000000..ef8aaa3 --- /dev/null +++ b/attic/jotblk/Makefile @@ -0,0 +1,12 @@ +all: + ./run.sh + +deploy: + gcloud app deploy -q --project jotblk *.yaml + +setup: + python3 -m venv env + env/bin/pip install -r requirements.txt + +datastore: + echo "Run: . dstore.sh" diff --git a/attic/jotblk/README.md b/attic/jotblk/README.md new file mode 100644 index 0000000..bcf92f5 --- /dev/null +++ b/attic/jotblk/README.md @@ -0,0 +1,20 @@ +# jotblk + +A web based block based editor to run on AppEngine. + +To run you need the AppEngine SDK. + +To setup run: +``` +make setup +``` + +In parallel run (for the datastore): +``` +. dstore.sh +``` + +And separately (to run a webserver on http://localhost:8080): +``` +./run.sh +``` diff --git a/attic/jotblk/app.yaml b/attic/jotblk/app.yaml new file mode 100644 index 0000000..0b6195f --- /dev/null +++ b/attic/jotblk/app.yaml @@ -0,0 +1,17 @@ +runtime: python39 + +default_expiration: "10m" + +handlers: +- url: / + static_files: static/index.html + upload: static/index.html + secure: always + +- url: /static + static_dir: static + secure: always + +- url: /io + script: main.app + secure: always diff --git a/attic/jotblk/dstore.sh b/attic/jotblk/dstore.sh new file mode 100644 index 0000000..c37bab5 --- /dev/null +++ b/attic/jotblk/dstore.sh @@ -0,0 +1,2 @@ +# Run this to start an datastore emulator. +gcloud emulators firestore start --database-mode=datastore-mode --project=jotblk --host-port=127.0.0.1:8099 diff --git a/attic/jotblk/main.py b/attic/jotblk/main.py new file mode 100644 index 0000000..3521040 --- /dev/null +++ b/attic/jotblk/main.py @@ -0,0 +1,81 @@ +# 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. + +import base64 +from flask import Flask, request, send_from_directory +from google.cloud import datastore + +client = datastore.Client() + +root_passwd_key = client.key('passwd', 'root') +root_passwd_entity = client.get(root_passwd_key) +if root_passwd_entity is None: + root_passwd = 'xyzzy' +else: + root_passwd = root_passwd_entity['secret'] + +def SaveBlock(index, data): + assert index >= 0 + assert len(data) == 1024, len(data) + entity = datastore.Entity(key=client.key('block', index+1)) + entity.update({'data': data}) + client.put(entity) + return '' + +def LoadBlocks(start, end): + assert end >= end + assert start >= 0 + assert end - start <= 128 + query = client.query(kind='block') + first_key = client.key('block', start+1) + last_key = client.key('block', end+1) + query.key_filter(first_key, '>=') + query.key_filter(last_key, '<') + entities = query.fetch() + blks = {} + for i in entities: + blks[i.key.path[0]['id']-1] = i['data'] + result = [] + for i in range(start, end+1): + if i in blks: + result.append(blks[i]) + else: + result.append(b' ' * 1024) + return b''.join(result) + +app = Flask(__name__) + +@app.route('/') +def canned(filename): + return send_from_directory('static', filename) + +@app.route('/') +def root(): + return canned('index.html') + +@app.route('/io', methods=['POST']) +def io(): + if root_passwd != request.form['passwd']: + return 'deny', 403 + if request.form['command'] == 'read': + start = int(request.form['start']) + end = int(request.form['end']) + return LoadBlocks(start, end) + elif request.form['command'] == 'write': + index = int(request.form['index']) + data = request.files['data'].read() + return SaveBlock(index, data) + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) diff --git a/attic/jotblk/requirements.txt b/attic/jotblk/requirements.txt new file mode 100644 index 0000000..13f13f5 --- /dev/null +++ b/attic/jotblk/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.0.0 +google-cloud-datastore==2.15.1 diff --git a/attic/jotblk/run.sh b/attic/jotblk/run.sh new file mode 100755 index 0000000..b9bbc2e --- /dev/null +++ b/attic/jotblk/run.sh @@ -0,0 +1,8 @@ +#! /bin/bash + +export DATASTORE_DATASET=jotblk +export DATASTORE_EMULATOR_HOST=127.0.0.1:8099 +export DATASTORE_EMULATOR_HOST_PATH=127.0.0.1:8099/datastore +export DATASTORE_HOST=http://127.0.0.1:8099 +export DATASTORE_PROJECT_ID=jotblk +env/bin/python3 main.py diff --git a/attic/jotblk/static/index.html b/attic/jotblk/static/index.html new file mode 100644 index 0000000..28144c2 --- /dev/null +++ b/attic/jotblk/static/index.html @@ -0,0 +1,3 @@ + + + diff --git a/attic/jotblk/static/jotblk.js b/attic/jotblk/static/jotblk.js new file mode 100644 index 0000000..708a897 --- /dev/null +++ b/attic/jotblk/static/jotblk.js @@ -0,0 +1,562 @@ +// 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); + for (var j = 0; j < 16; ++j) { + for (var i = 0; i < 64; ++i) { + if (pos === i + j * 64) { + ctx.fillStyle = '#750'; + 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); + 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.fillStyle = '#fb0'; + 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 += '\n'; + for (var i = start; i <= end; i++) { + content += '
\n';
+    content += BlockString(i, true);
+    content += '
' + i; + content += '

\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); diff --git a/attic/jotblk/static/myforth.fs b/attic/jotblk/static/myforth.fs new file mode 100644 index 0000000..9eb97df --- /dev/null +++ b/attic/jotblk/static/myforth.fs @@ -0,0 +1,43 @@ +\ Copyright 2021 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. + +web definitions + +JSWORD: block-read { n a -- } + for (var i = 0; i < 1024; i++) { + u8[a + i] = blocks[n * 1024 + i]; + } +~ + +also internals + +create block-buffer 1024 allot + +also forth definitions + +variable scr +: block ( n -- a ) block-buffer block-read block-buffer ; +: buffer ( n -- a ) block ; + +only forth definitions + +( Loading ) +: load ( n -- ) block 1024 evaluate ; +: thru ( a b -- ) over - 1+ for aft dup >r load r> 1+ then next drop ; + +( Listing ) +: list ( n -- ) scr ! ." Block " scr @ . cr scr @ block + 15 for dup 63 type [char] | emit space 15 r@ - . cr 64 + next drop ; + +only forth definitions