From 9864b196b405231b00d74b3d4c78e4fbfe9136ae Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:13:27 -0800 Subject: [PATCH 01/35] Don't add confusing params to config example, tweak params too --- setup.folk.default | 4 ++-- virtual-programs/camera-usb.folk | 2 +- virtual-programs/display.folk | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.folk.default b/setup.folk.default index b43adab4..1ef1dfb1 100644 --- a/setup.folk.default +++ b/setup.folk.default @@ -1,6 +1,6 @@ # Copy this file to ~/folk-live/setup.folk and edit it to make # changes. -Assert $this wishes $::thisNode uses camera "/dev/video0" with width 1280 height 720 bufferCount 4 +Assert $this wishes $::thisNode uses camera "/dev/video0" with width 1280 height 720 -Assert $this wishes $::thisNode uses display 0 with swapchainPadding 1 +Assert $this wishes $::thisNode uses display 0 diff --git a/virtual-programs/camera-usb.folk b/virtual-programs/camera-usb.folk index a17f3ce3..f14ad8b0 100644 --- a/virtual-programs/camera-usb.folk +++ b/virtual-programs/camera-usb.folk @@ -301,7 +301,7 @@ When /someone/ wishes $::thisNode uses camera /cameraPath/ with /...options/ { set width [dict get $options width] set height [dict get $options height] - set bufferCount [dict_getdef $options bufferCount 4] + set bufferCount [dict_getdef $options bufferCount 2] if {[dict exists $options crop]} { set crop [dict get $options crop] diff --git a/virtual-programs/display.folk b/virtual-programs/display.folk index 5e6bdebb..0f1f0747 100644 --- a/virtual-programs/display.folk +++ b/virtual-programs/display.folk @@ -1640,9 +1640,9 @@ Start-display-process { Wish $::thisProcess shares statements like \ [list /someone/ has error /err/ with info /errorInfo/] - # for backwards compatibility + # If swapchain padding isn't indicated, default to this When /someone/ wishes $::thisNode uses display /displayIdx/ { - Wish $::thisNode uses display $displayIdx with swapchainPadding 1 + Wish $::thisNode uses display $displayIdx with swapchainPadding 0 } When /someone/ wishes $::thisNode uses display /displayIdx/ with /...options/ { From 8d21f8137dc7f85ea1b7580d0349be3aa90629a9 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Mon, 10 Feb 2025 20:23:53 -0800 Subject: [PATCH 02/35] Add bidirectional editor support --- lib/folk.js | 93 +++++++++++----- virtual-programs/editor-control.folk | 152 +++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 25 deletions(-) create mode 100644 virtual-programs/editor-control.folk diff --git a/lib/folk.js b/lib/folk.js index b64e4f46..3575f77f 100644 --- a/lib/folk.js +++ b/lib/folk.js @@ -24,8 +24,8 @@ const eatBrace = (str) => { if (bc === 0) { return [ - str.slice(0, i+1), - str.slice(i+1), + str.slice(0, i + 1), + str.slice(i + 1), ]; } } @@ -118,8 +118,8 @@ const loadDict = (str) => { if (list.length % 2 !== 0) throw new Error('uneven element count in dict'); const obj = {}; - for (let i = 0; i < list.length; i+= 2) - obj[list[i]] = list[i+1]; + for (let i = 0; i < list.length; i += 2) + obj[list[i]] = list[i + 1]; return obj; }; @@ -141,7 +141,7 @@ const dump = (val) => { return dumpString(val.toString()); } else if (typeof val == 'object') { const core = Object.entries(val) - .map(([k, v]) => dump(k) + ' ' + dump(v)); + .map(([k, v]) => dump(k) + ' ' + dump(v)); return '{' + core.join(' ') + '}'; } @@ -170,18 +170,21 @@ class FolkWSChannel { this.prefix = prefix; this.callback = callback; this.ws.channels[prefix] = this; + this.retractKey = null; } stop() { + this.ws.ws.send(`Retract ${this.retractKey}`); if (this.ws.channels[this.prefix] !== this) return; delete this.ws.channels[this.prefix]; } } class FolkWS { - constructor(statusEl=null, url=null) { + constructor(statusEl = null, url = null) { this.channels = {}; this.i = 0; + this.assertSeq = 0; this._connect(statusEl, url); } @@ -247,12 +250,21 @@ class FolkWS { } // Evaluates inside a match context dependent on the WS connection: async send(message) { - await this.evaluate(tcl`Assert when websocket $chan is connected [list {this __seq} ${message}] with environment [list $chan [incr ::__seq]]`); + const seqNumber = this.assertSeq++; + const assertBody = tcl`when websocket $chan is connected [list {this __seq} ${message}] with environment [list $chan ${seqNumber}]`; + await this.evaluate(`Assert ${assertBody}`); + + const retractKey = assertBody; + return retractKey; } // Evaluates inside a persistent match context that replaces any // previous hold with same key: async hold(key, program, on = '$chan') { - await this.evaluate(tcl`Hold (non-capturing) (on ${on}) ${key} ${program}`); + if (on !== null) { + await this.evaluate(tcl`Hold (non-capturing) (on ${on}) ${key} ${program}`); + } else { + await this.evaluate(tcl`Hold (non-capturing) ${key} ${program}`); + } } async watchCollected(statement, onChange) { @@ -270,36 +282,67 @@ class FolkWS { async watch(statement, callbacks) { const channel = this.createChannel((message) => { - const [ action, match, matchId ] = loadList(message); + const [action, match, matchId] = loadList(message); const callback = callbacks[action]; callback && callback(loadDict(match), matchId); }); - await this.send(tcl` - set varNamesWillBeBound [list] - foreach word ${statement} { - if {[set varName [trie scanVariable $word]] != "false"} { + const retractKey = await this.send(tcl` + # for the top level Say + set outerVarNames [list] + # for inner When's + set innerVarNames [list] + set statement ${statement} + set body { + set matches [dict create] + foreach varName $names { + set val [set $varName] + dict append matches $varName $val + } + emit ${channel.prefix} [list add $matches $::matchId] + On unmatch { + emit ${channel.prefix} [list remove $matches $::matchId] + } + } + for {set i 0} {$i < [llength $statement]} {incr i} { + set word [lindex $statement $i] + if {$word eq "&"} { + # Desugar this join into nested Whens. + set remainingStatement [lrange $statement $i+1 end] + set statement [lrange $statement 0 $i-1] + for {set j 0} {$j < [llength $remainingStatement]} {incr j} { + set remainingWord [lindex $remainingStatement $j] + if {[set varName [trie scanVariable $remainingWord]] != "false"} { + if {$varName ni $statement::blanks && $varName ni $statement::negations} { + if {[string range $varName 0 2] eq "..."} { + set varName [string range $varName 3 end] + } + lappend innerVarNames $varName + } + } + if {[regexp {^/([^/ ]+)/$} $remainingWord -> remainingVarName] && + $remainingVarName in $outerVarNames} { + lset remainingStatement $j \\$$remainingVarName + } + } + set body [list When {*}$remainingStatement $body] + break + } elseif {[set varName [trie scanVariable $word]] != "false"} { if {$varName ni $statement::blanks && $varName ni $statement::negations} { if {[string range $varName 0 2] eq "..."} { set varName [string range $varName 3 end] } - lappend varNamesWillBeBound $varName + lappend outerVarNames $varName } } } - Say when {*}${statement} {{this names args} { - set matches [dict create] - foreach varName $names val $args { - dict append matches $varName $val - } - - emit ${channel.prefix} [list add $matches $::matchId] - On unmatch { - emit ${channel.prefix} [list remove $matches $::matchId] - } - }} with environment [list $this $varNamesWillBeBound] + # join both variable names to be sent along the websocket in the innermost When + set combinedVarNames [concat $outerVarNames $innerVarNames] + Say when {*}$statement [list [list this names {*}$outerVarNames] $body] with environment [list $this $combinedVarNames] `); + channel.retractKey = retractKey; + return channel; } } diff --git a/virtual-programs/editor-control.folk b/virtual-programs/editor-control.folk new file mode 100644 index 00000000..4f8f1504 --- /dev/null +++ b/virtual-programs/editor-control.folk @@ -0,0 +1,152 @@ +When /page/ has editor code /editorCode/ & /page/ has program code /programCode/ { + Claim $page has base64 editor code [binary encode base64 $editorCode] \ + program code [binary encode base64 $programCode] +} + +Wish the web server handles route "/editor-control" with handler { + html { + + +
+ ++ Select a keyboard: +
+ + + + + } +} \ No newline at end of file From 89dfad07cfe26d4e4b28a97e875d4d5da7f919c5 Mon Sep 17 00:00:00 2001 From: Mason Jones <20848827+smj-edison@users.noreply.github.com> Date: Mon, 10 Feb 2025 20:53:45 -0800 Subject: [PATCH 03/35] Make saving work from webpage --- virtual-programs/editor-control.folk | 52 +++++++++++++++++++--------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/virtual-programs/editor-control.folk b/virtual-programs/editor-control.folk index 4f8f1504..cb27992c 100644 --- a/virtual-programs/editor-control.folk +++ b/virtual-programs/editor-control.folk @@ -25,6 +25,7 @@ const textarea = document.querySelector("#code"); var currentKeyboard = null; var programCode = ""; // not the same as editor code +var cursorPosition = [0, 0]; // temporarily disable event processing after sending new code to prevent recursive event sends var allowLocalEventsToProcess = true; @@ -49,9 +50,29 @@ function disableLocalEventProcessing(durationMs) { }, durationMs); } +function updateProgramCode() { + disableRemoteEventProcessing(500); + + const { page, kbPath } = currentKeyboard; + + const currentCode = textarea.value; + programCode = currentCode; + + const id = page + kbPath; + ws.evaluate(tcl` + Hold (non-capturing) (on virtual-programs/editor.folk) ${"cursor" + kbPath} { + Claim the ${kbPath} cursor is [list ${cursorPosition[0]} ${cursorPosition[1]}] + Hold (on virtual-programs/editor.folk) ${"code" + kbPath} { + Claim ${id} has program code [binary decode base64 ${btoa(currentCode)}] + Claim ${id} has editor code [binary decode base64 ${btoa(currentCode)}] + } + } + `); +} + function updateCursorAndCode(ev) { if (!allowLocalEventsToProcess) return; - disableRemoteEventProcessing(100); + disableRemoteEventProcessing(500); const { page, kbPath } = currentKeyboard; @@ -63,6 +84,8 @@ function updateCursorAndCode(ev) { const y = linesBefore.length - 1; const x = linesBefore[linesBefore.length - 1].length; + cursorPosition = [x, y]; + const id = page + kbPath; ws.evaluate(tcl` Hold (non-capturing) (on virtual-programs/editor.folk) ${"cursor" + kbPath} { @@ -77,8 +100,14 @@ function updateCursorAndCode(ev) { textarea.addEventListener("input", updateCursorAndCode); textarea.addEventListener("selectionchange", updateCursorAndCode); +textarea.addEventListener("keydown", ev => { + if(ev.keyCode === 83 /* s */ && (navigator.platform.match("Mac") ? ev.metaKey : ev.ctrlKey)) { + ev.preventDefault(); + updateProgramCode(); + } +}); -var lastKeyboard; +var lastKeyboard; // to clean up the previous keyboard when another is picked async function selectKeyboard({ page, kbPath }) { if (lastKeyboard) lastKeyboard.stop(); @@ -88,7 +117,7 @@ async function selectKeyboard({ page, kbPath }) { lastKeyboard = await ws.watch(`${id} has base64 editor code /editorCode/ program code /programCode/ & the ${kbPath} cursor is /cursor/`, { add: ({ editorCode, programCode: _programCode, cursor }) => { if (!allowRemoteEventsToProcess) return; - disableLocalEventProcessing(100); + disableLocalEventProcessing(500); programCode = atob(_programCode); @@ -99,6 +128,8 @@ async function selectKeyboard({ page, kbPath }) { let [x, y] = loadList(cursor); x = parseInt(x); y = parseInt(y); + cursorPosition = [x, y]; + const lines = editorCode.split("\n"); let pos = 0; @@ -132,21 +163,8 @@ ws.watchCollected("/page/ is an editor & /page/ is a keyboard with path /kbPath/ keyboardSelect.addEventListener("input", (ev) => { selectKeyboard(JSON.parse(ev.target.value)); }); - -var editorMatches; -var selectedKeyboard; - -function updateTextarea() { - if (!editorMatches) return; - const editor = editorMatches.find(x => x.editor === selectedKeyboard); - - if (editor) { - code.innerText = editor.code; - console.log(editor.code); - } -}