diff options
| author | Omar Rizwan <omar@omar.website> | 2023-09-08 22:30:02 +0000 |
|---|---|---|
| committer | Omar Rizwan <omar@omar.website> | 2023-09-08 22:30:02 +0000 |
| commit | 4af0fc0e8aa6d89912e0516626fcd7c995b4abda (patch) | |
| tree | 80df9a2ed12b3235af67dd427dcf31fd3de0773e | |
| parent | c: Scope getters so fields w/ same name across structs don't collide (diff) | |
| parent | Some peering cleanup (receive before Step); build in FPS counting (diff) | |
| download | folk-4af0fc0e8aa6d89912e0516626fcd7c995b4abda.tar.gz folk-4af0fc0e8aa6d89912e0516626fcd7c995b4abda.zip | |
Merge branch 'main' into osnr/vulkan-display
39 files changed, 2565 insertions, 559 deletions
@@ -38,8 +38,10 @@ flamegraph: ~/FlameGraph/stackcollapse-perf.pl out.perf > out.folded ~/FlameGraph/flamegraph.pl out.folded > out.svg +# You can use the Web server to check the pid of display.folk, +# apriltags.folk, camera.folk, etc. remote-flamegraph: - ssh -t folk@$(FOLK_SHARE_NODE) -- make -C /home/folk/folk flamegraph + ssh -t folk@$(FOLK_SHARE_NODE) -- make -C /home/folk/folk flamegraph $(if $(REMOTE_FLAMEGRAPH_TID),FLAMEGRAPH_TID=$(REMOTE_FLAMEGRAPH_TID),) scp folk@$(FOLK_SHARE_NODE):~/folk/out.svg . scp folk@$(FOLK_SHARE_NODE):~/folk/out.perf . @@ -452,6 +452,26 @@ retrigger, and so on. in its body will run again unless the boop goes away and an entirely new boop appears. +### Animation + +#### Getting time + +Get the global clock time with: + +``` +When the clock time is /t/ { + Wish $this is labelled $t +} +``` + +Use it in an animation: + +``` +When the clock time is /t/ { + Wish $this draws a circle offset [list [expr {sin($t) * 50}] 0] +} +``` + ### You usually won't need these #### When when diff --git a/calibrate.tcl b/calibrate.tcl index cf2471f4..48cf0b8e 100644 --- a/calibrate.tcl +++ b/calibrate.tcl @@ -1,3 +1,4 @@ +source "lib/language.tcl" source "lib/c.tcl" exec sudo systemctl stop folk @@ -34,8 +34,10 @@ if {[info exists ::env(FOLK_SHARE_NODE)]} { set ::shareNode "folk-arc.local" } elseif {$wifi eq "The Windfish"} { set ::shareNode "folk-dpip.local" - } elseif {$wifi eq "Moxie"} { + } elseif {$wifi eq "interact residency"} { set ::shareNode "folk-interact.local" + } elseif {$wifi eq "Fios-gLwY5" } { + set ::shareNode "folk-charles.local" } else { # there's no default. } @@ -47,7 +47,7 @@ namespace eval Display { uplevel [list Wish display runs [list .display create line {*}[join $points] -fill $color -width $width]] } - proc text {fb x y scale text {radians 0}} { + proc text {x y scale text {radians 0}} { uplevel [list Wish display runs [list .display create text $x $y -text $text -font "Helvetica [expr {$scale * 12}]" -fill white -anchor center -angle [expr {$radians/3.14159*180}]]] } diff --git a/lib/environment.tcl b/lib/environment.tcl index 28dd33fb..6a83802e 100644 --- a/lib/environment.tcl +++ b/lib/environment.tcl @@ -19,20 +19,21 @@ proc runInSerializedEnvironment {lambda env} { if {![dict exists $::Evaluator::totalTimesMap $lambda]} { dict set ::Evaluator::totalTimesMap $lambda [dict create loadTime 0 runTime 0 unloadTime 0] } - set loadTime_ [time {}] + set loadTime_ [baretime {}] try { - set runTime_ [time {set ret [apply $lambda {*}$env]}] + set runTime_ [baretime {set ret [apply $lambda {*}$env]}] + set ::stepRunTime [+ $::stepRunTime $runTime_] set ret } finally { - set unloadTime_ [time {}] + set unloadTime_ [baretime {}] dict with ::Evaluator::totalTimesMap $lambda { - incr loadTime [string map {" microseconds per iteration" ""} $loadTime_] + incr loadTime $loadTime_ if {[info exists runTime_]} { - incr runTime [string map {" microseconds per iteration" ""} $runTime_] + incr runTime $runTime_ } - incr unloadTime [string map {" microseconds per iteration" ""} $unloadTime_] + incr unloadTime $unloadTime_ } } } diff --git a/lib/evaluator.tcl b/lib/evaluator.tcl index a411169d..daff44b3 100644 --- a/lib/evaluator.tcl +++ b/lib/evaluator.tcl @@ -749,6 +749,15 @@ namespace eval Statements { ;# singleton Statement store } return "digraph { rankdir=LR; [join $dot "\n"] }" } + proc saveDotToPdf {filename} { + exec dot -Tpdf >$filename <<[Statements::dot] + } + + proc print {} { + dict for {id stmt} [Statements::all] { + puts [statement short $stmt] + } + } # these are kind of arbitrary/temporary bridge $cc proc matchRemoveFirstDestructor {match_handle_t matchId} void { @@ -875,6 +884,7 @@ namespace eval Evaluator { } } static void LogWriteRecollect(statement_handle_t collectId); + static void LogWriteUnmatch(match_handle_t matchId); void reactToStatementAdditionThatMatchesCollect(Tcl_Interp* interp, statement_handle_t collectId, Tcl_Obj* collectPattern, @@ -1000,8 +1010,7 @@ namespace eval Evaluator { match_handle_t matchId = edge->match; if (!matchExists(matchId)) continue; // if was removed earlier - reactToMatchRemoval(interp, matchId); - matchRemove(matchId); + LogWriteUnmatch(matchId); } } } @@ -1014,6 +1023,7 @@ namespace eval Evaluator { if (unmatch->edges[j].type == PARENT) { statement_handle_t unmatchWhenId = unmatch->edges[j].statement; statement_t* unmatchWhen = get(unmatchWhenId); + if (unmatchWhen == NULL) continue; for (int k = 0; k < unmatchWhen->n_edges; k++) { if (unmatchWhen->edges[k].type == PARENT) { unmatchId = unmatchWhen->edges[k].match; @@ -1093,8 +1103,7 @@ namespace eval Evaluator { if (edge->type == CHILD && !matchHandleIsEqual(edge->match, matchId)) { match_handle_t childMatchId = edge->match; matchGet(childMatchId)->recollectOnDestruction = false; - reactToMatchRemoval(interp, childMatchId); - matchRemove(childMatchId); + LogWriteUnmatch(childMatchId); break; } } @@ -1103,7 +1112,7 @@ namespace eval Evaluator { $cc code { typedef enum { - NONE, ASSERT, RETRACT, SAY, RECOLLECT + NONE, ASSERT, RETRACT, SAY, UNMATCH, RECOLLECT } log_entry_op_t; typedef struct log_entry_t { log_entry_op_t op; @@ -1114,21 +1123,24 @@ namespace eval Evaluator { match_handle_t parentMatchId; Tcl_Obj* clause; } say; + struct { match_handle_t matchId; } unmatch; struct { statement_handle_t collectId; } recollect; }; } log_entry_t; - log_entry_t evaluatorLog[1024] = {0}; + log_entry_t evaluatorLog[4096] = {0}; #define EVALUATOR_LOG_CAPACITY (sizeof(evaluatorLog)/sizeof(evaluatorLog[1])) int evaluatorLogReadIndex = EVALUATOR_LOG_CAPACITY - 1; int evaluatorLogWriteIndex = 0; } $cc proc Evaluate {Tcl_Interp* interp} void { + /* printf("Evaluate==========\n"); */ while (evaluatorLogReadIndex != evaluatorLogWriteIndex) { log_entry_t entry = evaluatorLog[evaluatorLogReadIndex]; evaluatorLogReadIndex = (evaluatorLogReadIndex + 1) % EVALUATOR_LOG_CAPACITY; if (entry.op == ASSERT) { + /* printf("Assert (%s)\n", Tcl_GetString(entry.assert.clause)); */ statement_handle_t id; bool isNewStatement; addImpl(interp, entry.assert.clause, 0, NULL, &id, &isNewStatement); @@ -1138,6 +1150,7 @@ namespace eval Evaluator { Tcl_DecrRefCount(entry.assert.clause); } else if (entry.op == RETRACT) { + /* printf("Retract (%s)\n", Tcl_GetString(entry.retract.pattern)); */ environment_t* results[1000]; int resultsCount = searchByPattern(entry.retract.pattern, 1000, results); @@ -1150,6 +1163,7 @@ namespace eval Evaluator { Tcl_DecrRefCount(entry.retract.pattern); } else if (entry.op == SAY) { + /* printf("Say (%s)\n", Tcl_GetString(entry.say.clause)); */ if (matchExists(entry.say.parentMatchId)) { statement_handle_t id; bool isNewStatement; addImpl(interp, entry.say.clause, 1, &entry.say.parentMatchId, @@ -1160,7 +1174,15 @@ namespace eval Evaluator { } Tcl_DecrRefCount(entry.say.clause); + } else if (entry.op == UNMATCH) { + /* printf("Unmatch (m%d:%d)\n", entry.unmatch.matchId.idx, entry.unmatch.matchId.gen); */ + if (matchExists(entry.unmatch.matchId)) { + reactToMatchRemoval(interp, entry.unmatch.matchId); + matchRemove(entry.unmatch.matchId); + } + } else if (entry.op == RECOLLECT) { + /* printf("Recollect (s%d:%d)\n", entry.recollect.collectId.idx, entry.recollect.collectId.gen); */ if (exists(entry.recollect.collectId)) { recollect(interp, entry.recollect.collectId); } @@ -1191,8 +1213,11 @@ namespace eval Evaluator { Tcl_IncrRefCount(clause); LogWriteFront((log_entry_t) { .op = SAY, .say = {.parentMatchId=parentMatchId, .clause=clause} }); } + $cc proc LogWriteUnmatch {match_handle_t matchId} void { + LogWriteBack((log_entry_t) { .op = UNMATCH, .unmatch = {.matchId=matchId} }); + } $cc proc LogWriteRecollect {statement_handle_t collectId} void { - LogWriteBack((log_entry_t) { .op = RECOLLECT, .recollect = {.collectId=collectId} }); + LogWriteFront((log_entry_t) { .op = RECOLLECT, .recollect = {.collectId=collectId} }); } $cc proc LogIsEmpty {} bool { return evaluatorLogReadIndex == evaluatorLogWriteIndex; diff --git a/lib/language.tcl b/lib/language.tcl index b265b5a6..2b9fbc01 100644 --- a/lib/language.tcl +++ b/lib/language.tcl @@ -70,6 +70,8 @@ proc assert condition { } } +proc baretime body { string map {" microseconds per iteration" ""} [uplevel [list time $body]] } + # forever { ... } is sort of like while true { ... }, but it yields to # the event loop after each iteration. proc forever {body} { diff --git a/lib/math.tcl b/lib/math.tcl index 9fc6839a..23efc57b 100644 --- a/lib/math.tcl +++ b/lib/math.tcl @@ -139,6 +139,18 @@ namespace eval ::region { set bottomEdgeIndex [lindex [lsort -indices -real -index 1 $edgeMidpoints] end] vec2 midpoint {*}[edgeToLineSegment $r [lindex [edges $r] $bottomEdgeIndex]] } + proc bottomleft {r} { + lindex [vertices $r] 0 + } + proc bottomright {r} { + lindex [vertices $r] 1 + } + proc topright {r} { + lindex [vertices $r] 2 + } + proc topleft {r} { + lindex [vertices $r] 3 + } proc mapVertices {varname r body} { lreplace $r 0 0 [uplevel [list lmap $varname [vertices $r] $body]] @@ -262,7 +274,7 @@ namespace eval ::region { error "region move: Invalid distance $distance" } if {$unit eq "%"} { - set distance [* distance 0.01] + set distance [* $distance 0.01] set unit "" } if {$unit eq ""} { diff --git a/lib/peer.tcl b/lib/peer.tcl index 274f9f07..ef8e1e1c 100644 --- a/lib/peer.tcl +++ b/lib/peer.tcl @@ -24,73 +24,64 @@ namespace eval clauseset { namespace eval ::Peers {} set ::peersBlacklist [dict create] +proc ::addMatchesToShareStatements {shareStatementsVar matches} { + upvar $shareStatementsVar shareStatements + foreach m $matches { + set pattern [dict get $m pattern] + foreach match [Statements::findMatches $pattern] { + set id [lindex [dict get $match __matcheeIds] 0] + set clause [statement clause [Statements::get $id]] + clauseset add shareStatements $clause + } + } +} + proc ::peer {process {dieOnDisconnect false}} { - package require websocket namespace eval ::Peers::$process { - variable connected false + variable connected true proc log {s} { variable process puts "$::thisProcess -> $process: $s" } - proc setupSock {} { + + # TODO: Handle die on disconnect (?) + + proc send {statements} { variable process - log "Trying to connect to: ws://$process:4273/ws" - variable chan [::websocket::open "ws://$process:4273/ws" [namespace code handleWs]] + Mailbox::share $::thisProcess $process $statements } - proc handleWs {chan type msg} { - if {$type eq "connect"} { - log "Connected" - variable connected true - - # Establish a peering on their end, in the reverse - # direction, so they can send stuff back to us. - # It'll implicitly run in a ::Peers::X namespace on their end - # (because of how `run` is implemented below) - run { - set name [namespace tail [namespace current]] - variable chan [uplevel {set chan}] - - # First, check if this side has us blacklisted. - if {[dict exists $::peersBlacklist $name]} { - ::websocket::close $chan - return - } - - variable connected true - proc run {msg} { - variable chan - ::websocket::send $chan text $msg - } - } - } elseif {$type eq "disconnect"} { - log "Disconnected" - - variable dieOnDisconnect - if {$dieOnDisconnect} { exit 0 } - - variable connected false - after 2000 [namespace code setupSock] - } elseif {$type eq "error"} { - log "WebSocket error: $type $msg" - after 2000 [namespace code setupSock] - } elseif {$type eq "text"} { - eval $msg - } elseif {$type eq "ping" || $type eq "pong"} { - } else { - error "Unknown WebSocket event: $type $msg" - } + proc receive {} { + variable process + Mailbox::receive $process $::thisProcess } - proc run {msg} { - variable chan - ::websocket::send $chan text [list namespace eval ::Peers::$::thisProcess $msg] + proc share {shareStatements} { + variable process + variable prevShareStatements + + variable connected + if {!$connected} { return } + + # Share. + ::addMatchesToShareStatements shareStatements \ + [Statements::findMatches [list /someone/ wishes $process receives statements like /pattern/]] + if {![info exists prevShareStatements] || + ([clauseset size $prevShareStatements] > 0 || + [clauseset size $shareStatements] > 0)} { + + send [clauseset clauses $shareStatements] + + set prevShareStatements $shareStatements + } } proc init {n shouldDieOnDisconnect} { - variable process $n; setupSock + variable process $n variable dieOnDisconnect $shouldDieOnDisconnect - vwait ::Peers::${n}::connected + + Mailbox::create $::thisProcess $process + Mailbox::create $process $::thisProcess } init } $process $dieOnDisconnect diff --git a/lib/process.tcl b/lib/process.tcl index a4b8ed48..059c0b89 100644 --- a/lib/process.tcl +++ b/lib/process.tcl @@ -58,19 +58,22 @@ namespace eval ::Zygote { proc On-process {name body} { set this [uplevel {expr {[info exists this] ? $this : "<unknown>"}}] - set processCode [list apply {{__name __body} { + set processCode [list apply {{__parentProcess __name __body} { set ::thisProcess $__name Assert <lib/process.tcl> wishes $::thisProcess shares all wishes Assert <lib/process.tcl> wishes $::thisProcess shares all claims - ::peer "localhost" true + ::peer $__parentProcess true Assert <lib/process.tcl> claims $::thisProcess has pid [pid] Assert when $::thisProcess has pid /something/ [list {} $__body] - Step - vwait forever - }} $name $body] + while true { + Step + } + }} $::thisProcess $name $body] + + ::peer $name false Zygote::spawn [list apply {{processCode} { # A supervisor that wraps the subprocess. diff --git a/lib/terminal.tcl b/lib/terminal.tcl new file mode 100644 index 00000000..16e553ca --- /dev/null +++ b/lib/terminal.tcl @@ -0,0 +1,193 @@ +# terminal.tcl -- +# +# Implements a virtual terminal with basic read/write procs. +# + +namespace eval Terminal { + # From `man console_codes` + variable keymap [dict create \ + BACKSPACE "\x08" \ + TAB "\x09" \ + ENTER "\x0d" \ + DELETE "\x7f" \ + ESC "\x1b" \ + UP "\x1b\[A" \ + DOWN "\x1b\[B" \ + RIGHT "\x1b\[C" \ + LEFT "\x1b\[D" \ + ] + + proc _remap {key ctrlPressed} { + variable keymap + if {[string length $key] == 1} { + # Convert ctrl-A through ctrl-Z and others to terminal control characters + if {$ctrlPressed} { + set charCode [scan [string toupper $key] %c] + if {$charCode >= 64 && $charCode <= 95} { + set charCode [expr {$charCode - 64}] + return [format %c $charCode] + } + } + # All other single char keys can be passed through + return $key + } + if {[dict exists $keymap $key]} { + return [dict get $keymap $key] + } + return "" + } + + proc create {rows cols cmd} { + termCreate $rows $cols [list bash -c $cmd ""] + } + + proc destroy {term} { + termDestroy $term + } + + # Writes a keyboard key to the terminal, handling control codes + proc write {term key ctrlPressed} { + set key [_remap $key $ctrlPressed] + if {[string length $key] > 0} { + termWrite $term $key + } + } + + # Returns a newline separated string of terminal lines + proc read {term} { + termRead $term + } +} + +set cc [c create] +$cc cflags -I./vendor/libtmt ./vendor/libtmt/tmt.c + +c loadlib [lindex [exec /usr/sbin/ldconfig -p | grep libutil.so | head -1] end] +$cc cflags -lutil + +$cc include <sys/types.h> +$cc include <stdlib.h> +$cc include <unistd.h> +$cc include <pty.h> +$cc include <fcntl.h> +$cc include <string.h> +$cc include <sys/time.h> +$cc include <signal.h> +$cc include "tmt.h" + +$cc struct VTerminal { + TMT* tmt; + int pty_fd; + int pid; + + // Note: display has 1 more column than tmt screen to hold newlines between each line + char* display; + int curs_r; + int curs_c; + int ncols; +}; + +$cc code { + #define PTYBUF 4096 + char iobuf[PTYBUF]; + + char* charAt(VTerminal *vt, int r, int c) { + int i = r * (vt->ncols + 1) + c; + return &vt->display[i]; + } + + void tmtEvent(tmt_msg_t m, TMT *tmt, const void *a, void *p) { + VTerminal *vt = (VTerminal*)p; + const TMTSCREEN *s = tmt_screen(tmt); + + if (m == TMT_MSG_UPDATE) { + for (size_t r = 0; r < s->nline; r++){ + if (s->lines[r]->dirty){ + for (size_t c = 0; c < s->ncol; c++){ + *charAt(vt, r, c) = s->lines[r]->chars[c].c; + } + } + } + tmt_clean(tmt); + } + } + + void blinkCursor(VTerminal *vt) { + // Restore char under old cursor + const TMTSCREEN *s = tmt_screen(vt->tmt); + *charAt(vt, vt->curs_r, vt->curs_c) = s->lines[vt->curs_r]->chars[vt->curs_c].c; + + // Update new cursor + const TMTPOINT *c = tmt_cursor(vt->tmt); + vt->curs_r = c->r; + vt->curs_c = c->c; + + // Replace char with cursor every other second + struct timeval tv; + gettimeofday(&tv, NULL); + if (tv.tv_sec % 2 == 0) { + *charAt(vt, vt->curs_r, vt->curs_c) = 0xDB; // block char: █ + } + } +} + +$cc proc termCreate {int rows int cols char* cmd[]} VTerminal* { + int i = 0; + while (true) { + // execvp requires cmd array to be terminated by null pointer + if (strlen(cmd[i]) == 0) { cmd[i] = NULL; break; } + i++; + } + + VTerminal *vt = malloc(sizeof(VTerminal)); + vt->curs_r = 0; + vt->curs_c = 0; + vt->ncols = cols; + + vt->display = malloc(sizeof(char[rows][cols + 1])); + for (int r = 0; r < rows - 1; r++) { + *charAt(vt, r, cols) = '\n'; + } + *charAt(vt, rows - 1, cols) = '\0'; + + vt->tmt = tmt_open(rows, cols, tmtEvent, vt, NULL); + + struct winsize ws = {.ws_row = rows, .ws_col = cols}; + pid_t pid = forkpty(&vt->pty_fd, NULL, NULL, &ws); + if (pid < 0){ + return NULL; + } else if (pid == 0){ + setenv("TERM", "ansi", 1); + if (execvp(cmd[0], cmd) == -1) { + fprintf(stderr, "execvp(%s, ...) failed: %m\n", cmd[0]); + } + return NULL; + } + + vt->pid = pid; + fcntl(vt->pty_fd, F_SETFL, O_NONBLOCK); + return vt; +} + +$cc proc termDestroy {VTerminal* vt} void { + kill(vt->pid, SIGTERM); + close(vt->pty_fd); + free(vt->display); + free(vt); +} + +$cc proc termRead {VTerminal* vt} char* { + ssize_t r = read(vt->pty_fd, iobuf, PTYBUF); + if (r > 0) { + tmt_write(vt->tmt, iobuf, r); + } + + blinkCursor(vt); + return vt->display; +} + +$cc proc termWrite {VTerminal* vt char* key} void { + write(vt->pty_fd, key, strlen(key)); +} + +$cc compile diff --git a/lib/trie.tcl b/lib/trie.tcl index e5c2c0be..1f9bac8d 100644 --- a/lib/trie.tcl +++ b/lib/trie.tcl @@ -33,8 +33,8 @@ namespace eval ctrie { // We generally store a pointer (for example, to a // reaction thunk) or a generational handle (for example, - // for a statement) in this 64-bit value slot. Only used - // in leaf nodes of the trie. + // for a statement) in this 64-bit value slot. + bool hasValue; uint64_t value; size_t nbranches; @@ -47,6 +47,7 @@ namespace eval ctrie { trie_t* ret = (trie_t *) ckalloc(size); memset(ret, 0, size); *ret = (trie_t) { .key = NULL, + .hasValue = false, .value = 0, .nbranches = 10 }; @@ -83,6 +84,7 @@ namespace eval ctrie { $cc proc addImpl {trie_t** trie int wordc Tcl_Obj** wordv uint64_t value} void { if (wordc == 0) { (*trie)->value = value; + (*trie)->hasValue = true; return; } @@ -116,6 +118,7 @@ namespace eval ctrie { branch->key = word; Tcl_IncrRefCount(branch->key); branch->value = 0; + branch->hasValue = false; branch->nbranches = 10; (*trie)->branches[j] = branch; @@ -178,7 +181,7 @@ namespace eval ctrie { uint64_t* results int* resultsidx size_t maxresults trie_t* trie int wordc Tcl_Obj** wordv} void { if (wordc == 0) { - if (trie->value != 0) { + if (trie->hasValue) { if (*resultsidx < maxresults) { results[(*resultsidx)++] = trie->value; } @@ -1,5 +1,14 @@ if {$tcl_version eq 8.5} { error "Don't use Tcl 8.5 / macOS system Tcl. Quitting." } +# TODO: Fix this hack. +set thisPid [pid] +foreach pid [try { exec pgrep tclsh8.6 } on error e { list }] { + if {$pid ne $thisPid} { + exec kill -9 $pid + } +} +exec sleep 1 + if {[info exists ::argv0] && $::argv0 eq [info script]} { set ::isLaptop [expr {$tcl_platform(os) eq "Darwin" || ([info exists ::env(XDG_SESSION_TYPE)] && @@ -133,6 +142,7 @@ proc On {event args} { # send that to the subprocess. lassign [uplevel Evaluator::serializeEnvironment] argNames argValues uplevel [list On-process $name [list apply [list $argNames $body] {*}$argValues]] + set name ;# Return the name to the caller in case they want it. } elseif {$event eq "unmatch"} { set body [lindex $args 0] @@ -156,8 +166,9 @@ proc After {n unit body} { set ::committed [dict create] set ::toCommit [dict create] proc Commit {args} { + upvar this this set body [lindex $args end] - set key [list Commit [uplevel {expr {[info exists this] ? $this : "<unknown>"}}] {*}[lreplace $args end end]] + set key [list Commit [expr {[info exists this] ? $this : "<unknown>"}] {*}[lreplace $args end end]] if {$body eq ""} { dict set ::toCommit $key $body } else { @@ -165,18 +176,22 @@ proc Commit {args} { set lambda [list {this} [list apply [list $argNames $body] {*}$argValues]] dict set ::toCommit $key $lambda } - - after idle Step } set ::stepCount 0 -set ::stepTime "none" +set ::stepTime -1 source "lib/peer.tcl" proc StepImpl {} { incr ::stepCount Assert $::thisProcess has step count $::stepCount Retract $::thisProcess has step count [expr {$::stepCount - 1}] + # Receive statements from all peers. + foreach peerNs [namespace children ::Peers] { + upvar ${peerNs}::process peer + Commit $peer [list Say $peer is sharing statements [${peerNs}::receive]] + } + while {[dict size $::toCommit] > 0 || ![Evaluator::LogIsEmpty]} { dict for {key lambda} $::toCommit { if {$lambda ne ""} { @@ -193,65 +208,44 @@ proc StepImpl {} { Evaluator::Evaluate } - if {[namespace exists Display]} { - Display::commit ;# TODO: this is weird, not right level - } - - set shareStatements [clauseset create] - set shareAllWishes [expr {[llength [Statements::findMatches [list /someone/ wishes $::thisProcess shares all wishes]]] > 0}] - set shareAllClaims [expr {[llength [Statements::findMatches [list /someone/ wishes $::thisProcess shares all claims]]] > 0}] - dict for {_ stmt} [Statements::all] { - if {($shareAllWishes && [lindex [statement clause $stmt] 1] eq "wishes") || - ($shareAllClaims && [lindex [statement clause $stmt] 1] eq "claims")} { - clauseset add shareStatements [statement clause $stmt] - } - } - - set matches [Statements::findMatches [list /someone/ wishes $::thisProcess shares statements like /pattern/]] - proc ::addMatchesToShareStatements {shareStatementsVar matches} { - upvar $shareStatementsVar shareStatements - foreach m $matches { - set pattern [dict get $m pattern] - foreach match [Statements::findMatches $pattern] { - set id [lindex [dict get $match __matcheeIds] 0] - set clause [statement clause [Statements::get $id]] - clauseset add shareStatements $clause + # Share statements to all peers. + set ::peerTime [baretime { + # This takes 2 ms. + set shareStatements [clauseset create] + set shareAllWishes [expr {[llength [Statements::findMatches [list /someone/ wishes $::thisProcess shares all wishes]]] > 0}] + set shareAllClaims [expr {[llength [Statements::findMatches [list /someone/ wishes $::thisProcess shares all claims]]] > 0}] + dict for {_ stmt} [Statements::all] { + if {($shareAllWishes && [lindex [statement clause $stmt] 1] eq "wishes") || + ($shareAllClaims && [lindex [statement clause $stmt] 1] eq "claims")} { + clauseset add shareStatements [statement clause $stmt] } } - } - ::addMatchesToShareStatements shareStatements $matches - - foreach peerNs [namespace children ::Peers] { - apply [list {peer shareStatements} { - variable prevShareStatements - - variable connected - if {!$connected} { return } - - ::addMatchesToShareStatements shareStatements \ - [Statements::findMatches [list /someone/ wishes $peer receives statements like /pattern/]] - if {![info exists prevShareStatements] || - ([clauseset size $prevShareStatements] > 0 || - [clauseset size $shareStatements] > 0)} { + set matches [Statements::findMatches [list /someone/ wishes $::thisProcess shares statements like /pattern/]] + ::addMatchesToShareStatements shareStatements $matches - run [list apply {{process receivedStatements} { - upvar chan chan - Commit $chan statements { - Claim $process is sharing statements $receivedStatements - } - }} $::thisProcess [clauseset clauses $shareStatements]] - - set prevShareStatements $shareStatements - } - - } $peerNs] [namespace tail $peerNs] $shareStatements - } + foreach peerNs [namespace children ::Peers] { + ${peerNs}::share $shareStatements + } + }] } + +set ::frames [list] proc Step {} { - if {[dict size $::toCommit] > 0 || ![Evaluator::LogIsEmpty]} { - set ::stepTime [time StepImpl] + set ::stepRunTime 0 + set stepTime [baretime StepImpl] + + set framesInLastSecond 0 + set now [clock milliseconds] + lappend ::frames $now + foreach frame $::frames { + if {$frame > $now - 1000} { + incr framesInLastSecond + } } + set ::frames [lreplace $::frames 0 end-$framesInLastSecond] + + set ::stepTime "$stepTime us (peer $::peerTime us, run $::stepRunTime us) ($framesInLastSecond fps)" } source "lib/math.tcl" @@ -288,30 +282,35 @@ namespace eval ::Heap { $cc include <fcntl.h> $cc include <unistd.h> $cc include <stdlib.h> + $cc include <string.h> + $cc include <errno.h> $cc code { - size_t folkHeapSize = 100000000; // 100MB + size_t folkHeapSize = 400000000; // 400MB uint8_t* folkHeapBase; - uint8_t* _Atomic folkHeapPointer; + uint8_t** _Atomic folkHeapPointer; } # The memory mapping of the heap will be inherited by all # subprocesses, since it's established before the creation of # the zygote. $cc proc folkHeapMount {} void { + shm_unlink("/folk-heap"); int fd = shm_open("/folk-heap", O_RDWR | O_CREAT, S_IROTH | S_IWOTH | S_IRUSR | S_IWUSR); - ftruncate(fd, folkHeapSize); + if (fd == -1) { fprintf(stderr, "folkHeapMount: shm_open failed\n"); exit(1); } + if (ftruncate(fd, folkHeapSize) == -1) { fprintf(stderr, "folkHeapMount: ftruncate failed\n"); exit(1); } folkHeapBase = (uint8_t*) mmap(0, folkHeapSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - if (folkHeapBase == NULL) { - fprintf(stderr, "heapMount: failed"); exit(1); + if (folkHeapBase == NULL || folkHeapBase == (void *) -1) { + fprintf(stderr, "folkHeapMount: mmap failed: '%s'\n", strerror(errno)); exit(1); } - folkHeapPointer = folkHeapBase; + folkHeapPointer = (uint8_t**) folkHeapBase; + *folkHeapPointer = folkHeapBase + sizeof(*folkHeapPointer); } $cc proc folkHeapAlloc {size_t sz} void* { - if (folkHeapPointer + sz > folkHeapBase + folkHeapSize) { - fprintf(stderr, "heapAlloc: out of memory"); exit(1); + if (*folkHeapPointer + sz >= folkHeapBase + folkHeapSize) { + fprintf(stderr, "folkHeapAlloc: out of memory\n"); exit(1); } - void* ptr = folkHeapPointer; - folkHeapPointer = folkHeapPointer + sz; + void* ptr = *folkHeapPointer; + *folkHeapPointer += sz; return (void*) ptr; } if {$::tcl_platform(os) eq "Linux"} { @@ -324,6 +323,90 @@ namespace eval ::Heap { } Heap::init +namespace eval ::Mailbox { + set cc [c create] + $cc include <stdlib.h> + $cc include <string.h> + $cc include <pthread.h> + $cc import ::Heap::cc folkHeapAlloc as folkHeapAlloc + $cc code { + typedef struct mailbox_t { + bool active; + + pthread_mutex_t mutex; + + char from[100]; + char to[100]; + + int mailLen; + char mail[1000000]; + } mailbox_t; + + #define NMAILBOXES 100 + mailbox_t* mailboxes; + } + $cc proc init {} void { + fprintf(stderr, "Before: mailboxes = %p\n", mailboxes); + mailboxes = folkHeapAlloc(sizeof(mailbox_t) * NMAILBOXES); + memset(mailboxes, 0, sizeof(mailbox_t) * NMAILBOXES); + fprintf(stderr, "After: mailboxes = %p\n", mailboxes); + } + $cc proc create {char* from char* to} void { + if (find(from, to) != NULL) return; + fprintf(stderr, "Mailbox create %s -> %s\n", from, to); + for (int i = 0; i < NMAILBOXES; i++) { + if (!mailboxes[i].active) { + mailboxes[i].active = true; + + pthread_mutexattr_t mattr; + pthread_mutexattr_init(&mattr); + pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED); + pthread_mutex_init(&mailboxes[i].mutex, &mattr); + + snprintf(mailboxes[i].from, 100, "%s", from); + snprintf(mailboxes[i].to, 100, "%s", to); + mailboxes[i].mail[0] = '\0'; + return; + } + } + fprintf(stderr, "Out of available mailboxes.\n"); + exit(1); + } + $cc code { + mailbox_t* find(char* from, char* to) { + for (int i = 0; i < NMAILBOXES; i++) { + if (mailboxes[i].active && + strcmp(mailboxes[i].from, from) == 0 && + strcmp(mailboxes[i].to, to) == 0) { + return &mailboxes[i]; + } + } + return NULL; + } + } + $cc proc share {char* from char* to char* statements} void { + mailbox_t* mailbox = find(from, to); + if (!mailbox) { + fprintf(stderr, "Could not find mailbox for '%s -> %s'.\n", from, to); + exit(1); + } + pthread_mutex_lock(&mailbox->mutex); { + mailbox->mailLen = snprintf(mailbox->mail, sizeof(mailbox->mail), "%s", statements); + } pthread_mutex_unlock(&mailbox->mutex); + } + $cc proc receive {char* from char* to} Tcl_Obj* { + mailbox_t* mailbox = find(from, to); + if (!mailbox) { return Tcl_NewStringObj("", -1); } + Tcl_Obj* ret; + pthread_mutex_lock(&mailbox->mutex); { + ret = Tcl_NewStringObj(mailbox->mail, mailbox->mailLen); + } pthread_mutex_unlock(&mailbox->mutex); + return ret; + } + $cc compile + init +} + if {[info exists ::entry]} { source "lib/process.tcl" Zygote::init diff --git a/pi/AprilTags.tcl b/pi/AprilTags.tcl index a489a1f4..69079813 100644 --- a/pi/AprilTags.tcl +++ b/pi/AprilTags.tcl @@ -17,12 +17,12 @@ namespace eval AprilTags { td = apriltag_detector_create(); tf = tagStandard52h13_create(); apriltag_detector_add_family_bits(td, tf, 1); - td->nthreads = 2; + td->nthreads = 1; } apc proc detect {image_t gray} Tcl_Obj* { assert(gray.components == 1); - image_u8_t im = (image_u8_t) { .width = gray.width, .height = gray.height, .stride = gray.width, .buf = gray.data }; + image_u8_t im = (image_u8_t) { .width = gray.width, .height = gray.height, .stride = gray.bytesPerRow, .buf = gray.data }; zarray_t *detections = apriltag_detector_detect(td, &im); int detectionCount = zarray_size(detections); diff --git a/pi/Display.tcl b/pi/Display.tcl index 1ecc8e69..3983d664 100644 --- a/pi/Display.tcl +++ b/pi/Display.tcl @@ -1,5 +1,3 @@ -source "lib/language.tcl" -source "lib/c.tcl" source "pi/cUtils.tcl" namespace eval Display {} @@ -56,12 +54,12 @@ dc code { static void commitThenClearStaging(); pixel_t* staging; + pixel_t* fbmem; struct { pixel_t* mem; uint32_t id; } fbs[2]; - int currentFbIndex; int fbwidth; int fbheight; @@ -130,10 +128,11 @@ dc proc setupGpu {} void { fprintf(stderr, "Display: cannot open '%s': %m\n", card); exit(1); } - if (drmSetMaster(gpuFd) != 0) { + while (drmSetMaster(gpuFd) != 0) { fprintf(stderr, "Display: cannot become DRM master on '%s': %m\n", card); - exit(1); + fprintf(stderr, "Display: waiting 1 s...\n"); sleep(1); } + fprintf(stderr, "Display: successfully became DRM master on '%s'\n", card); uint64_t hasDumb; if (drmGetCap(gpuFd, DRM_CAP_DUMB_BUFFER, &hasDumb) < 0 || !hasDumb) { fprintf(stderr, "Display: drm device '%s' does not support dumb buffers\n", card); @@ -198,12 +197,18 @@ dc proc setupGpu {} void { } setupFb(0); - setupFb(1); + fbmem = fbs[0].mem; + staging = ckalloc(fbwidth * fbheight * sizeof(pixel_t)); - // Can't drop master if we're going to flip buffers at runtime. + int ret = drmModeSetCrtc(gpuFd, gpuEnc->crtc_id, fbs[0].id, 0, 0, + &gpuConn->connector_id, 1, &gpuConn->modes[0]); + if (ret) { + fprintf(stderr, "Display: cannot flip CRTC to %d for connector %u (%d): %m\n", + 0, gpuConn->connector_id, errno); + exit(1); + } + // drmDropMaster(gpuFd); - - commitThenClearStaging(); } dc proc setupFb {int idx} void [csubst { struct drm_mode_create_dumb dumb; @@ -239,19 +244,11 @@ dc proc setupFb {int idx} void [csubst { }] # Hack to support old stuff that uses framebuffer directly and doesn't commit. dc proc getFbPointer {} pixel_t* { - return fbs[currentFbIndex].mem; + return fbmem; } dc proc commitThenClearStaging {} void { - currentFbIndex = !currentFbIndex; - int ret = drmModeSetCrtc(gpuFd, gpuEnc->crtc_id, fbs[currentFbIndex].id, 0, 0, - &gpuConn->connector_id, 1, &gpuConn->modes[0]); - if (ret) { - fprintf(stderr, "Display: cannot flip CRTC to %d for connector %u (%d): %m\n", - currentFbIndex, - gpuConn->connector_id, errno); - exit(1); - } - staging = fbs[!currentFbIndex].mem; + memcpy(fbmem, staging, fbwidth * fbheight * sizeof(pixel_t)); + // This memset takes ~2ms on 1080p on a Pi 4. memset(staging, 0, fbwidth * fbheight * sizeof(pixel_t)); } @@ -349,15 +346,23 @@ dc proc drawCircle {int x0 int y0 int radius int color} void { defineImageType dc dc proc drawImageTransparent {int x0 int y0 image_t image int transparentTone int scale} void { - if (image.components != 1) { exit(1); } for (int y = 0; y < image.height; y++) { for (int x = 0; x < image.width; x++) { - // Index into image to get color - int i = y*image.bytesPerRow + x*image.components; + // index into image to get color + int i = y*image.bytesPerRow + x*image.components; (void)i; uint8_t r; uint8_t g; uint8_t b; - if (image.data[i] == transparentTone) { continue; } - r = image.data[i]; g = image.data[i]; b = image.data[i]; + if (image.components == 1) { + if (image.data[i] == transparentTone) { continue; } + r = image.data[i]; g = image.data[i]; b = image.data[i]; + } else if (image.components == 3) { + if (image.data[i] == transparentTone && + image.data[i + 1] == transparentTone && + image.data[i + 2] == transparentTone) { + continue; + } + r = image.data[i]; g = image.data[i + 1]; b = image.data[i + 2]; + } // Write repeatedly to framebuffer to scale up image for (int dy = 0; dy < scale; dy++) { @@ -368,44 +373,58 @@ dc proc drawImageTransparent {int x0 int y0 image_t image int transparentTone in if (sx < 0 || fbwidth <= sx || sy < 0 || fbheight <= sy) continue; staging[sy*fbwidth + sx] = PIXEL(r, g, b); - } } } } } -dc proc drawImage {int x0 int y0 image_t image int scale} void { - for (int y = 0; y < image.height; y++) { - for (int x = 0; x < image.width; x++) { - - // Index into image to get color - int i = y*image.bytesPerRow + x*image.components; - uint8_t r; uint8_t g; uint8_t b; - if (image.components == 3) { - r = image.data[i]; g = image.data[i+1]; b = image.data[i+2]; - } else if (image.components == 1) { - r = image.data[i]; g = image.data[i]; b = image.data[i]; - } else { - exit(1); - } +source "pi/rotate.tcl" +dc proc drawImage {int x0 int y0 image_t image double radians int scale} void { + double radiansNormalized = fmod(radians, 2.0 * M_PI); + if (radiansNormalized > M_PI) { + radiansNormalized -= 2.0 * M_PI; + } else if (radiansNormalized < -M_PI) { + radiansNormalized += 2.0 * M_PI; + } + int imageX; int imageY; + image_t temp = rotateMakeImage(image.width, image.height, image.components, + radiansNormalized, + &imageX, &imageY); - // Write repeatedly to framebuffer to scale up image - for (int dy = 0; dy < scale; dy++) { - for (int dx = 0; dx < scale; dx++) { + // Draw the image into the temp image. + for (int y = 0; y < image.height; y++) { + memcpy(&temp.data[(y + imageY) * temp.bytesPerRow + imageX * temp.components], + &image.data[y * image.bytesPerRow], + image.width * image.components); + } + + rotate(temp, imageX, imageY, image.width, image.height, radiansNormalized); - int sx = x0 + scale * x + dx; - int sy = y0 + scale * y + dy; - if (sx < 0 || fbwidth <= sx || sy < 0 || fbheight <= sy) continue; + // Find corners of rotated rectangle + Vec2i topLeft = Vec2i_rotate((Vec2i) {-(int)image.width/2, -(int)image.height/2}, radiansNormalized); + Vec2i topRight = Vec2i_rotate((Vec2i) {image.width/2, -(int)image.height/2}, radiansNormalized); + Vec2i bottomLeft = Vec2i_rotate((Vec2i) {-(int)image.width/2, image.height/2}, radiansNormalized); + Vec2i bottomRight = Vec2i_rotate((Vec2i) {image.width/2, image.height/2}, radiansNormalized); - staging[sy*fbwidth + sx] = PIXEL(r, g, b); + // Now blit the offscreen buffer to the screen. + image_t rotatedImage = { + .width = max4(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x) - + min4(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x), + .height = max4(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y) - + min4(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y), + .components = temp.components, + .bytesPerRow = temp.bytesPerRow + }; + int rotatedImageX0 = (temp.width - rotatedImage.width) / 2; + int rotatedImageY0 = (temp.height - rotatedImage.height) / 2; + rotatedImage.data = &temp.data[rotatedImageY0*temp.bytesPerRow + rotatedImageX0*temp.components]; - } - } - } - } + drawImageTransparent(x0 - rotatedImage.width*scale/2, + y0 - rotatedImage.height*scale/2, + rotatedImage, 0x00, scale); + ckfree(temp.data); } -source "pi/rotate.tcl" dc proc drawText {int x0 int y0 double radians int scale char* text} void { // Draws text (breaking at linebreaks), with the center of the // text at (x0, y0). Rotates counterclockwise up from the @@ -628,8 +647,8 @@ namespace eval Display { } } - proc image {x y im {scale 1.0}} { - drawImage [expr {int($x)}] [expr {int($y)}] $im [expr {int($scale)}] + proc image {x y im {radians 0} {scale 1.0}} { + drawImage [expr {int($x)}] [expr {int($y)}] $im $radians [expr {int($scale)}] } # for debugging diff --git a/pi/KeyCodes.tcl b/pi/KeyCodes.tcl index 4ca8d4ad..87a59500 100644 --- a/pi/KeyCodes.tcl +++ b/pi/KeyCodes.tcl @@ -1,129 +1,143 @@ set KeyCodes [dict create] -proc define {name code} { - upvar KeyCodes KeyCodes - dict set KeyCodes $code $name + +proc keydef {code val {shiftVal ""}} { + upvar KeyCodes KeyCodes + if {$shiftVal == ""} { + set shiftVal $val + } + dict set KeyCodes $code [list $val $shiftVal] +} + +proc keyFromCode {code {shift false}} { + upvar KeyCodes KeyCodes + if {[dict exists $KeyCodes $code]} { + set vals [dict get $KeyCodes $code] + return [lindex $vals [expr {$shift ? 1 : 0}]] + } + puts "WARNING: unknown key code \"$code\"" + return "?" } -# from https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h -define KEY_RESERVED 0 -define KEY_ESC 1 -define KEY_1 2 -define KEY_2 3 -define KEY_3 4 -define KEY_4 5 -define KEY_5 6 -define KEY_6 7 -define KEY_7 8 -define KEY_8 9 -define KEY_9 10 -define KEY_0 11 -define KEY_MINUS 12 -define KEY_EQUAL 13 -define KEY_BACKSPACE 14 -define KEY_TAB 15 -define KEY_Q 16 -define KEY_W 17 -define KEY_E 18 -define KEY_R 19 -define KEY_T 20 -define KEY_Y 21 -define KEY_U 22 -define KEY_I 23 -define KEY_O 24 -define KEY_P 25 -define KEY_LEFTBRACE 26 -define KEY_RIGHTBRACE 27 -define KEY_ENTER 28 -define KEY_LEFTCTRL 29 -define KEY_A 30 -define KEY_S 31 -define KEY_D 32 -define KEY_F 33 -define KEY_G 34 -define KEY_H 35 -define KEY_J 36 -define KEY_K 37 -define KEY_L 38 -define KEY_SEMICOLON 39 -define KEY_APOSTROPHE 40 -define KEY_GRAVE 41 -define KEY_LEFTSHIFT 42 -define KEY_BACKSLASH 43 -define KEY_Z 44 -define KEY_X 45 -define KEY_C 46 -define KEY_V 47 -define KEY_B 48 -define KEY_N 49 -define KEY_M 50 -define KEY_COMMA 51 -define KEY_DOT 52 -define KEY_SLASH 53 -define KEY_RIGHTSHIFT 54 -define KEY_KPASTERISK 55 -define KEY_LEFTALT 56 -define KEY_SPACE 57 -define KEY_CAPSLOCK 58 -define KEY_F1 59 -define KEY_F2 60 -define KEY_F3 61 -define KEY_F4 62 -define KEY_F5 63 -define KEY_F6 64 -define KEY_F7 65 -define KEY_F8 66 -define KEY_F9 67 -define KEY_F10 68 -define KEY_NUMLOCK 69 -define KEY_SCROLLLOCK 70 -define KEY_KP7 71 -define KEY_KP8 72 -define KEY_KP9 73 -define KEY_KPMINUS 74 -define KEY_KP4 75 -define KEY_KP5 76 -define KEY_KP6 77 -define KEY_KPPLUS 78 -define KEY_KP1 79 -define KEY_KP2 80 -define KEY_KP3 81 -define KEY_KP0 82 -define KEY_KPDOT 83 +# Keycodes from https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h +keydef 0 {RESERVED} +keydef 1 {ESC} +keydef 2 {1} {!} +keydef 3 {2} {@} +keydef 4 {3} {#} +keydef 5 {4} {$} +keydef 6 {5} {%} +keydef 7 {6} {^} +keydef 8 {7} {&} +keydef 9 {8} {*} +keydef 10 {9} {(} +keydef 11 {0} {)} +keydef 12 {-} {_} +keydef 13 {=} {+} +keydef 14 {BACKSPACE} +keydef 15 {TAB} +keydef 16 {q} {Q} +keydef 17 {w} {W} +keydef 18 {e} {E} +keydef 19 {r} {R} +keydef 20 {t} {T} +keydef 21 {y} {Y} +keydef 22 {u} {U} +keydef 23 {i} {I} +keydef 24 {o} {O} +keydef 25 {p} {P} +keydef 26 {[} "\{" +keydef 27 {]} "\}" +keydef 28 {ENTER} +keydef 29 {LEFTCTRL} +keydef 30 {a} {A} +keydef 31 {s} {S} +keydef 32 {d} {D} +keydef 33 {f} {F} +keydef 34 {g} {G} +keydef 35 {h} {H} +keydef 36 {j} {J} +keydef 37 {k} {K} +keydef 38 {l} {L} +keydef 39 {;} {:} +keydef 40 {'} "\"" +keydef 41 {`} {~} +keydef 42 {LEFTSHIFT} +keydef 43 "\\" {|} +keydef 44 {z} {Z} +keydef 45 {x} {X} +keydef 46 {c} {C} +keydef 47 {v} {V} +keydef 48 {b} {B} +keydef 49 {n} {N} +keydef 50 {m} {M} +keydef 51 {,} {<} +keydef 52 {.} {>} +keydef 53 {/} {?} +keydef 54 {RIGHTSHIFT} +keydef 55 {KPASTERISK} +keydef 56 {LEFTALT} +keydef 57 { } ;# SPACE +keydef 58 {CAPSLOCK} +keydef 59 {F1} +keydef 60 {F2} +keydef 61 {F3} +keydef 62 {F4} +keydef 63 {F5} +keydef 64 {F6} +keydef 65 {F7} +keydef 66 {F8} +keydef 67 {F9} +keydef 68 {F10} +keydef 69 {NUMLOCK} +keydef 70 {SCROLLLOCK} +keydef 71 {KP7} +keydef 72 {KP8} +keydef 73 {KP9} +keydef 74 {KPMINUS} +keydef 75 {KP4} +keydef 76 {KP5} +keydef 77 {KP6} +keydef 78 {KPPLUS} +keydef 79 {KP1} +keydef 80 {KP2} +keydef 81 {KP3} +keydef 82 {KP0} +keydef 83 {KPDOT} -define KEY_ZENKAKUHANKAKU 85 -define KEY_102ND 86 -define KEY_F11 87 -define KEY_F12 88 -define KEY_RO 89 -define KEY_KATAKANA 90 -define KEY_HIRAGANA 91 -define KEY_HENKAN 92 -define KEY_KATAKANAHIRAGANA 93 -define KEY_MUHENKAN 94 -define KEY_KPJPCOMMA 95 -define KEY_KPENTER 96 -define KEY_RIGHTCTRL 97 -define KEY_KPSLASH 98 -define KEY_SYSRQ 99 -define KEY_RIGHTALT 100 -define KEY_LINEFEED 101 -define KEY_HOME 102 -define KEY_UP 103 -define KEY_PAGEUP 104 -define KEY_LEFT 105 -define KEY_RIGHT 106 -define KEY_END 107 -define KEY_DOWN 108 -define KEY_PAGEDOWN 109 -define KEY_INSERT 110 -define KEY_DELETE 111 -define KEY_MACRO 112 -define KEY_MUTE 113 -define KEY_VOLUMEDOWN 114 -define KEY_VOLUMEUP 115 -define KEY_POWER 116 -define KEY_KPEQUAL 117 -define KEY_KPPLUSMINUS 118 -define KEY_PAUSE 119 -define KEY_SCALE 120 +keydef 85 {ZENKAKUHANKAKU} +keydef 86 {102ND} +keydef 87 {F11} +keydef 88 {F12} +keydef 89 {RO} +keydef 90 {KATAKANA} +keydef 91 {HIRAGANA} +keydef 92 {HENKAN} +keydef 93 {KATAKANAHIRAGANA} +keydef 94 {MUHENKAN} +keydef 95 {KPJPCOMMA} +keydef 96 {KPENTER} +keydef 97 {RIGHTCTRL} +keydef 98 {KPSLASH} +keydef 99 {SYSRQ} +keydef 100 {RIGHTALT} +keydef 101 {LINEFEED} +keydef 102 {HOME} +keydef 103 {UP} +keydef 104 {PAGEUP} +keydef 105 {LEFT} +keydef 106 {RIGHT} +keydef 107 {END} +keydef 108 {DOWN} +keydef 109 {PAGEDOWN} +keydef 110 {INSERT} +keydef 111 {DELETE} +keydef 112 {MACRO} +keydef 113 {MUTE} +keydef 114 {VOLUMEDOWN} +keydef 115 {VOLUMEUP} +keydef 116 {POWER} +keydef 117 {KPEQUAL} +keydef 118 {KPPLUSMINUS} +keydef 119 {PAUSE} +keydef 120 {SCALE} diff --git a/pi/Keyboard.tcl b/pi/Keyboard.tcl index d578e0b5..b393aaf1 100644 --- a/pi/Keyboard.tcl +++ b/pi/Keyboard.tcl @@ -1,6 +1,4 @@ namespace eval Keyboard { - source "pi/KeyCodes.tcl" - variable kb proc init {} { @@ -33,7 +31,18 @@ namespace eval Keyboard { fconfigure $kb -translation binary } - proc getChar {} { + # Event size depends on sizeof(long). Default to 32-bit longs + variable evtBytes 16 + variable evtFormat iissi + if {[exec getconf LONG_BIT] == 64} { + set evtBytes 24 + set evtFormat wwssi + } + + proc getKeyEvent {} { + variable evtBytes + variable evtFormat + # See https://www.kernel.org/doc/Documentation/input/input.txt # https://www.kernel.org/doc/Documentation/input/event-codes.txt # https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h @@ -42,27 +51,14 @@ namespace eval Keyboard { # struct timeval time; # unsigned short type; (should be EV_KEY = 0x01) # unsigned short code; (scancode; for example, 16 = q) - # unsigned int value; (should be 1 for keypress) + # unsigned int value; (0 for key release, 1 for press, 2 for repeat) # }; # while 1 { - binary scan [read $Keyboard::kb 16] nntutunu tvSec tvUsec type code value - if {$type == 0x01 && $value == 1} break + binary scan [read $Keyboard::kb $evtBytes] $evtFormat tvSec tvUsec type code value + if {$type == 0x01} { + return [list $code $value] + } } - # TODO: Should properly catch this error - # e.g. Jan 28 04:11:28 folk0 make[1991]: Thread error: tid0x7f45fd2b0640 can't\ read\ \"name\":\ no\ such\ variable ... - - # scancode name, like KEY_A - catch { set name [dict get $Keyboard::KeyCodes $code] } err - set ch [string tolower [string range $name 4 end]] - # puts "type $type code $code value $value ($ch)" - return $ch } } - -if {[info exists ::argv0] && $::argv0 eq [info script]} { - Keyboard::init - puts [Keyboard::getChar] - puts [Keyboard::getChar] - puts [Keyboard::getChar] -} @@ -2,92 +2,46 @@ package require Thread proc errorproc {id errorInfo} {puts "Thread error in $id: $errorInfo"} thread::errorproc errorproc -namespace eval Display { - variable WIDTH - variable HEIGHT - regexp {mode "(\d+)x(\d+)"} [exec fbset] -> WIDTH HEIGHT - - variable displayThread [thread::create { - source pi/Display.tcl - Display::init - puts "Display tid: [getTid]" - - set ::displayCount 0 - thread::wait - }] - puts "Display thread id: $displayThread" - - proc stroke {points width color} { - uplevel [list Wish display runs [list Display::stroke $points $width $color]] - } - - proc circle {x y radius thickness color} { - uplevel [list Wish display runs [list Display::circle $x $y $radius $thickness $color]] - } - - proc text args { - uplevel [list Wish display runs [list Display::text {*}$args]] - } - - proc fillTriangle args { - uplevel [list Wish display runs [list Display::fillTriangle {*}$args]] - } - - proc fillQuad args { - uplevel [list Wish display runs [list Display::fillQuad {*}$args]] - } - - proc fillPolygon args { - uplevel [list Wish display runs [list Display::fillPolygon {*}$args]] - } - - variable displayTime none - proc commit {} { - set displayList [list] - foreach match [Statements::findMatches {/someone/ wishes display runs /command/}] { - lappend displayList [dict get $match command] - } - - proc lcomp {a b} {expr {[lindex $a 0] == "Display::text"}} - incr ::displayCount - thread::send -head -async $Display::displayThread [format { - set newDisplayCount %d - if {$::displayCount > $newDisplayCount} { - # we've already displayed a newer frame - return - } else { - set ::displayCount $newDisplayCount - } - - # Draw the display list - set displayTime [time { - %s - commitThenClearStaging - }] - thread::send -async "%s" [subst { - set Display::displayTime "$displayTime" - }] - } $::displayCount \ - [join [lsort -command lcomp $displayList] "\n"] \ - [thread::id]] - } -} - try { set keyboardThread [thread::create [format { source "pi/Keyboard.tcl" + source "pi/KeyCodes.tcl" source "lib/c.tcl" source "pi/cUtils.tcl" Keyboard::init puts "Keyboard tid: [getTid]" - set chs [list] + set keyStates [list up down repeat] + set modifiers [dict create \ + shift 0 \ + ctrl 0 \ + alt 0 \ + ] + while true { - lappend chs [Keyboard::getChar] + lassign [Keyboard::getKeyEvent] keyCode eventType + + set shift [dict get $modifiers shift] + set key [keyFromCode $keyCode $shift] + set keyState [lindex $keyStates $eventType] + + set isDown [expr {$keyState != "up"}] + if {[string match *SHIFT $key]} { + dict set modifiers shift $isDown + } + if {[string match *CTRL $key]} { + dict set modifiers ctrl $isDown + } + if {[string match *ALT $key]} { + dict set modifiers alt $isDown + } + + set heldModifiers [dict keys [dict filter $modifiers value 1]] + # Use `list` to escape special chars (brackets, quotes, whitespace) thread::send -async "%s" [subst { - Retract keyboard claims the keyboard character log is /something/ - Assert keyboard claims the keyboard character log is "$chs" + Retract keyboard claims key /k/ is /t/ with modifiers /m/ + Assert keyboard claims key [list $key] is [list $keyState] with modifiers [list $heldModifiers] }] } } [thread::id]]] diff --git a/pi/rotate.tcl b/pi/rotate.tcl index 028ffc35..8a196471 100644 --- a/pi/rotate.tcl +++ b/pi/rotate.tcl @@ -30,9 +30,9 @@ dc proc shearY {image_t sprite int x0 int y0 int width int height double sy} voi int shear = sy * (x - x0); int from = y*sprite.bytesPerRow + x*sprite.components; int to = (y + shear)*sprite.bytesPerRow + x*sprite.components; - sprite.data[to] = sprite.data[from]; + memmove(&sprite.data[to], &sprite.data[from], sprite.components); // Blot out the unsheared part - if (from != to) { sprite.data[from] = 0x00; } + if (from != to) { memset(&sprite.data[from], 0x00, sprite.components); } } } } else if (sy < 0) { @@ -41,9 +41,9 @@ dc proc shearY {image_t sprite int x0 int y0 int width int height double sy} voi int shear = sy * (x - x0); // Is negative. int from = y*sprite.bytesPerRow + x*sprite.components; int to = (y + shear)*sprite.bytesPerRow + x*sprite.components; - sprite.data[to] = sprite.data[from]; + memmove(&sprite.data[to], &sprite.data[from], sprite.components); // Blot out the unsheared part - if (from != to) { sprite.data[from] = 0x00; } + if (from != to) { memset(&sprite.data[from], 0x00, sprite.components); } } } } @@ -56,9 +56,9 @@ dc proc rotate180 {image_t sprite int x0 int y0 int width int height} void { int icenter = imin + (imax - imin)/2; for (int i = imin; i < icenter; i += sprite.components) { int j = imax - (i - imin); - uint8_t temp = sprite.data[i]; - sprite.data[i] = sprite.data[j]; - sprite.data[j] = temp; + uint8_t temp[sprite.components]; memcpy(temp, &sprite.data[i], sprite.components); + memmove(&sprite.data[i], &sprite.data[j], sprite.components); + memcpy(&sprite.data[j], temp, sprite.components); } } dc proc rotateMakeImage {int width int height int components double radians diff --git a/test/stale.tcl b/test/stale.tcl new file mode 100644 index 00000000..7b2fcd70 --- /dev/null +++ b/test/stale.tcl @@ -0,0 +1,6 @@ +for {set i 0} {$i < 30000} {incr i} { + Commit [list Claim the iteration count is $i] + Step +} + +assert {[llength [Statements::findMatches [list /someone/ claims the iteration count is /i/]]] == 1} diff --git a/test/survival.tcl b/test/survival.tcl new file mode 100644 index 00000000..914ab45d --- /dev/null +++ b/test/survival.tcl @@ -0,0 +1,25 @@ +Assert when we are running {{} { + When the collected matches for [list tag /k/ was seen by /x/ at /p/] are /matches/ { + set tagsSeen [dict create] + foreach m $matches { + dict set tagsSeen [dict get $m k] true + } + dict for {k _} $tagsSeen { Claim tag $k is a tag } + } + When tag /k/ is a tag { + puts "Saw tag $k" + On unmatch { error "Should never unmatch" } + } +}} +Assert we are running +Step + +Commit Omar { Claim tag 1 was seen by Omar at home } +Commit Mom { Claim tag 1 was seen by Mom at restaurant } +Step + +Commit Omar { Claim tag 1 was seen by Omar at work } +Step + +# Statements::print + diff --git a/user-programs/haippi7/web-image.folk b/user-programs/haippi7/web-image.folk index 8c437ce9..d06df1b3 100644 --- a/user-programs/haippi7/web-image.folk +++ b/user-programs/haippi7/web-image.folk @@ -1,91 +1,9 @@ -set cc [c create] -$cc cflags -L[lindex [exec /usr/sbin/ldconfig -p | grep libjpeg] end] - -# defineImageType $cc -# for some reason defineImageType doesn't work here so we do it manually -$cc code { - typedef struct { - uint32_t width; - uint32_t height; - int components; - uint32_t bytesPerRow; - - uint8_t *data; - } image_t; -} - -$cc argtype image_t { - image_t $argname; sscanf(Tcl_GetString($obj), "width %u height %u components %d bytesPerRow %u data 0x%p", &$argname.width, &$argname.height, &$argname.components, &$argname.bytesPerRow, &$argname.data); -} -$cc rtype image_t { - $robj = Tcl_ObjPrintf("width %u height %u components %d bytesPerRow %u data 0x%" PRIxPTR, $rvalue.width, $rvalue.height, $rvalue.components, $rvalue.bytesPerRow, (uintptr_t) $rvalue.data); -} - -$cc include <stdlib.h> -$cc include <string.h> -$cc include <jpeglib.h> - -$cc code { - #include <jpeglib.h> - #include <stdint.h> - #include <unistd.h> - - void -jpeg(FILE* dest, uint8_t* rgb, uint32_t width, uint32_t height, int quality) -{ - JSAMPARRAY image; - image = calloc(height, sizeof (JSAMPROW)); - for (size_t i = 0; i < height; i++) { - image[i] = calloc(width * 3, sizeof (JSAMPLE)); - for (size_t j = 0; j < width; j++) { - image[i][j * 3 + 0] = rgb[(i * width + j)]; - image[i][j * 3 + 1] = rgb[(i * width + j)]; - image[i][j * 3 + 2] = rgb[(i * width + j)]; - } - } - - struct jpeg_compress_struct compress; - struct jpeg_error_mgr error; - compress.err = jpeg_std_error(&error); - jpeg_create_compress(&compress); - jpeg_stdio_dest(&compress, dest); - - compress.image_width = width; - compress.image_height = height; - compress.input_components = 3; - compress.in_color_space = JCS_RGB; - jpeg_set_defaults(&compress); - jpeg_set_quality(&compress, quality, TRUE); - jpeg_start_compress(&compress, TRUE); - jpeg_write_scanlines(&compress, image, height); - jpeg_finish_compress(&compress); - jpeg_destroy_compress(&compress); - - for (size_t i = 0; i < height; i++) { - free(image[i]); - } - free(image); -} - -} - - -$cc proc saveTempImage {image_t im char* filename} void { - // write capture to jpeg - // char filename[100] = "web-image-test.jpg"; - FILE* out = fopen(filename, "w"); - jpeg(out, im.data, im.width, im.height, 100); - fclose(out); -} -c loadlib [lindex [exec /usr/sbin/ldconfig -p | grep libjpeg] end] -$cc compile - When the camera frame is /im/ { Wish the web server handles route "/frame-image/$" with handler [list apply {{im} { # set width [dict get $im width] # set height [dict get $im height] set filename "/tmp/web-image-frame.jpg" - saveTempImage $im $filename + image saveAsJpeg $im $filename set fsize [file size $filename] set fd [open $filename r] fconfigure $fd -encoding binary -translation binary @@ -93,4 +11,4 @@ When the camera frame is /im/ { close $fd dict create statusAndHeaders "HTTP/1.1 200 OK\nConnection: close\nContent-Type: image/jpeg\nContent-Length: $fsize\n\n" body $body }} $im] -}
\ No newline at end of file +} diff --git a/vendor/libtmt/README.rst b/vendor/libtmt/README.rst new file mode 100644 index 00000000..f3819df9 --- /dev/null +++ b/vendor/libtmt/README.rst @@ -0,0 +1,637 @@ + +============================================ +libtmt - a simple terminal emulation library +============================================ + +libtmt is the Tiny Mock Terminal Library. It provides emulation of a classic +smart text terminal, by maintaining an in-memory screen image. Sending text +and command sequences to libtmt causes it to update this in-memory image, +which can then be examined and rendered however the user sees fit. + +The imagined primary goal for libtmt is to for terminal emulators and +multiplexers; it provides the terminal emulation layer for the `mtm`_ +terminal multiplexer, for example. Other uses include screen-scraping and +automated test harnesses. + +libtmt is similar in purpose to `libtsm`_, but considerably smaller (500 +lines versus 6500 lines). libtmt is also, in this author's humble opinion, +considerably easier to use. + +.. _`mtm`: https://github.com/deadpixi/mtm +.. _`libtsm`: https://www.freedesktop.org/wiki/Software/kmscon/libtsm/ + +Major Features and Advantages +============================= + +Works Out-of-the-Box + libtmt emulates a well-known terminal type (`ansi`), the definition of + which has been in the terminfo database since at least 1995. There's no + need to install a custom terminfo entry. There's no claiming to be an + xterm but only emulating a small subset of its features. Any program + using terminfo works automatically: this includes vim, emacs, mc, + cmus, nano, nethack, ... + +Portable + Written in pure C99. + Optionally, the POSIX-mandated `wcwidth` function can be used, which + provides minimal support for combining characters. + +Small + Less than 500 lines of C, including comments and whitespace. + +Free + Released under a BSD-style license, free for commercial and + non-commerical use, with no restrictions on source code release or + redistribution. + +Simple + Only 8 functions to learn, and really you can get by with 6! + +International + libtmt internally uses wide characters exclusively, and uses your C + library's multibyte encoding functions. + This means that the library automatically supports any encoding that + your operating system does. + +How to Use libtmt +================= + +libtmt is a single C file and a single header. Just include these files +in your project and you should be good to go. + +By default, libtmt uses only ISO standard C99 features, +but see `Compile-Time Options`_ below. + +Example Code +------------ + +Below is a simple program fragment giving the flavor of libtmt. +Note that another good example is the `mtm`_ terminal multiplexer: + +.. _`mtm`: https://github.com/deadpixi/mtm + +.. code:: c + + #include <stdio.h> + #include <stdlib.h> + #include "tmt.h" + + /* Forward declaration of a callback. + * libtmt will call this function when the terminal's state changes. + */ + void callback(tmt_msg_t m, TMT *vt, const void *a, void *p); + + int + main(void) + { + /* Open a virtual terminal with 2 lines and 10 columns. + * The first NULL is just a pointer that will be provided to the + * callback; it can be anything. The second NULL specifies that + * we want to use the default Alternate Character Set; this + * could be a pointer to a wide string that has the desired + * characters to be displayed when in ACS mode. + */ + TMT *vt = tmt_open(2, 10, callback, NULL, NULL); + if (!vt) + return perror("could not allocate terminal"), EXIT_FAILURE; + + /* Write some text to the terminal, using escape sequences to + * use a bold rendition. + * + * The final argument is the length of the input; 0 means that + * libtmt will determine the length dynamically using strlen. + */ + tmt_write(vt, "\033[1mhello, world (in bold!)\033[0m", 0); + + /* Writing input to the virtual terminal can (and in this case, did) + * call the callback letting us know the screen was updated. See the + * callback below to see how that works. + */ + tmt_close(vt); + return EXIT_SUCCESS; + } + + void + callback(tmt_msg_t m, TMT *vt, const void *a, void *p) + { + /* grab a pointer to the virtual screen */ + const TMTSCREEN *s = tmt_screen(vt); + const TMTPOINT *c = tmt_cursor(vt); + + switch (m){ + case TMT_MSG_BELL: + /* the terminal is requesting that we ring the bell/flash the + * screen/do whatever ^G is supposed to do; a is NULL + */ + printf("bing!\n"); + break; + + case TMT_MSG_UPDATE: + /* the screen image changed; a is a pointer to the TMTSCREEN */ + for (size_t r = 0; r < s->nline; r++){ + if (s->lines[r]->dirty){ + for (size_t c = 0; c < s->ncol; c++){ + printf("contents of %zd,%zd: %lc (%s bold)\n", r, c, + s->lines[r]->chars[c].c, + s->lines[r]->chars[c].a.bold? "is" : "is not"); + } + } + } + + /* let tmt know we've redrawn the screen */ + tmt_clean(vt); + break; + + case TMT_MSG_ANSWER: + /* the terminal has a response to give to the program; a is a + * pointer to a string */ + printf("terminal answered %s\n", (const char *)a); + break; + + case TMT_MSG_MOVED: + /* the cursor moved; a is a pointer to the cursor's TMTPOINT */ + printf("cursor is now at %zd,%zd\n", c->r, c->c); + break; + } + } + +Data Types and Enumerations +--------------------------- + +.. code:: c + + /* an opaque structure */ + typedef struct TMT TMT; + + /* possible messages sent to the callback */ + typedef enum{ + TMT_MSG_MOVED, /* the cursor changed position */ + TMT_MSG_UPDATE, /* the screen image changed */ + TMT_MSG_ANSWER, /* the terminal responded to a query */ + TMT_MSG_BELL /* the terminal bell was rung */ + } tmt_msg_T; + + /* a callback for the library + * m is one of the message constants above + * vt is a pointer to the vt structure + * r is NULL for TMT_MSG_BELL + * is a pointer to the cursor's TMTPOINT for TMT_MSG_MOVED + * is a pointer to the terminal's TMTSCREEN for TMT_MSG_UPDATE + * is a pointer to a string for TMT_MSG_ANSWER + * p is whatever was passed to tmt_open (see below). + */ + typedef void (*TMTCALLBACK)(tmt_msg_t m, struct TMT *vt, + const void *r, void *p); + + /* color definitions */ + typedef enum{ + TMT_COLOR_BLACK, + TMT_COLOR_RED, + TMT_COLOR_GREEN, + TMT_COLOR_YELLOW, + TMT_COLOR_BLUE, + TMT_COLOR_MAGENTA, + TMT_COLOR_CYAN, + TMT_COLOR_WHITE, + TMT_COLOR_DEFAULT /* whatever the host terminal wants it to mean */ + } tmt_color_t; + + /* graphical rendition */ + typedef struct TMTATTRS TMTATTRS; + struct TMTATTRS{ + bool bold; /* character is bold */ + bool dim; /* character is half-bright */ + bool underline; /* character is underlined */ + bool blink; /* character is blinking */ + bool reverse; /* character is in reverse video */ + bool invisible; /* character is invisible */ + tmt_color_t fg; /* character foreground color */ + tmt_color_t bg; /* character background color */ + }; + + /* characters */ + typedef struct TMTCHAR TMTCHAR; + struct TMTCHAR{ + wchar_t c; /* the character */ + TMTATTRS a; /* its rendition */ + }; + + /* a position on the screen; upper left corner is 0,0 */ + typedef struct TMTPOINT TMTPOINT; + struct TMTPOINT{ + size_t r; /* row */ + size_t c; /* column */ + }; + + /* a line of characters on the screen; + * every line is always as wide as the screen + */ + typedef struct TMTLINE TMTLINE; + struct TMTLINE{ + bool dirty; /* line has changed since it was last drawn */ + TMTCHAR chars; /* the contents of the line */ + }; + + /* a virtual terminal screen image */ + typedef struct TMTSCREEN TMTSCREEN; + struct TMTSCREEN{ + size_t nline; /* number of rows */ + size_t ncol; /* number of columns */ + TMTLINE **lines; /* the lines on the screen */ + }; + +Functions +--------- + +`TMT *tmt_open(size_t nrows, size_t ncols, TMTCALLBACK cb, VOID *p, const wchar *acs);` + Creates a new virtual terminal, with `nrows` rows and `ncols` columns. + The callback `cb` will be called on updates, and passed `p` as a final + argument. See the definition of `tmt_msg_t` above for possible values + of each argument to the callback. + + Terminals must have a size of at least two rows and two columns. + + `acs` specifies the characters to use when in Alternate Character Set + (ACS) mode. The default string (used if `NULL` is specified) is:: + + L"><^v#+:o##+++++~---_++++|<>*!fo" + + See `Alternate Character Set`_ for more information. + + Note that the callback must be ready to be called immediately, as + it will be called after initialization of the terminal is done, but + before the call to `tmt_open` returns. + +`void tmt_close(TMT *vt)` + Close and free all resources associated with `vt`. + +`bool tmt_resize(TMT *vt, size_t nrows, size_t ncols)` + Resize the virtual terminal to have `nrows` rows and `ncols` columns. + The contents of the area in common between the two sizes will be preserved. + + Terminals must have a size of at least two rows and two columns. + + If this function returns false, the resize failed (only possible in + out-of-memory conditions or invalid sizes). If this happens, the terminal + is trashed and the only valid operation is the close the terminal. + +`void tmt_write(TMT *vt, const char *s, size_t n);` + Write the provided string to the terminal, interpreting any escape + sequences contained threin, and update the screen image. The last + argument is the length of the input. If set to 0, the length is + determined using `strlen`. + + The terminal's callback function may be invoked one or more times before + a call to this function returns. + + The string is converted internally to a wide-character string using the + system's current multibyte encoding. Each terminal maintains a private + multibyte decoding state, and correctly handles mulitbyte characters that + span multiple calls to this function (that is, the final byte(s) of `s` + may be a partial mulitbyte character to be completed on the next call). + +`const TMTSCREEN *tmt_screen(const TMT *vt);` + Returns a pointer to the terminal's screen image. + +`const TMTPOINT *tmt_cursor(cosnt TMT *vt);` + Returns a pointer to the terminal's cursor position. + +`void tmt_clean(TMT *vt);` + Call this after receiving a `TMT_MSG_UPDATE` or `TMT_MSG_MOVED` callback + to let the library know that the program has handled all reported changes + to the screen image. + +`void tmt_reset(TMT *vt);` + Resets the virtual terminal to its default state (colors, multibyte + decoding state, rendition, etc). + +Special Keys +------------ + +To send special keys to a program that is using libtmt for its display, +write one of the `TMT_KEY_*` strings to that program's standard input +(*not* to libtmt; it makes no sense to send any of these constants to +libtmt itself). + +The following macros are defined, and are all constant strings: + +- TMT_KEY_UP +- TMT_KEY_DOWN +- TMT_KEY_RIGHT +- TMT_KEY_LEFT +- TMT_KEY_HOME +- TMT_KEY_END +- TMT_KEY_INSERT +- TMT_KEY_BACKSPACE +- TMT_KEY_ESCAPE +- TMT_KEY_BACK_TAB +- TMT_KEY_PAGE_UP +- TMT_KEY_PAGE_DOWN +- TMT_KEY_F1 through TMT_KEY_F10 + +Note also that the classic PC console sent the enter key as +a carriage return, not a linefeed. Many programs don't care, +but some do. + +Compile-Time Options +-------------------- + +There are two preprocessor macros that affect libtmt: + +`TMT_INVALID_CHAR` + Define this to a wide-character. This character will be added to + the virtual display when an invalid multibyte character sequence + is encountered. + + By default (if you don't define it as something else before compiling), + this is `((wchar_t)0xfffd)`, which is the codepoint for the Unicode + 'REPLACEMENT CHARACTER'. Note that your system might not use Unicode, + and its wide-character type might not be able to store a constant as + large as `0xfffd`, in which case you'll want to use an alternative. + +`TMT_HAS_WCWIDTH` + By default, libtmt uses only standard C99 features. If you define + TMT_HAS_WCWIDTH before compiling, libtmt will use the POSIX `wcwidth` + function to detect combining characters. + + Note that combining characters are still not handled particularly + well, regardless of whether this was defined. Also note that what + your C library's `wcwidth` considers a combining character and what + the written language in question considers one could be different. + +Alternate Character Set +----------------------- + +The terminal can be switched to and from its "Alternate Character Set" (ACS) +using escape sequences. The ACS traditionally contained box-drawing and other +semigraphic characters. + +The characters in the ACS are configurable at runtime, by passing a wide string +to `tmt_open`. The default if none is provided (i.e. the argument is `NULL`) +uses ASCII characters to approximate the traditional characters. + +The string passed to `tmt_open` must be 31 characters long. The characters, +and their default ASCII-safe values, are in order: + +- RIGHT ARROW ">" +- LEFT ARROW "<" +- UP ARROW "^" +- DOWN ARROW "v" +- BLOCK "#" +- DIAMOND "+" +- CHECKERBOARD "#" +- DEGREE "o" +- PLUS/MINUS "+" +- BOARD ":" +- LOWER RIGHT CORNER "+" +- UPPER RIGHT CORNER "+" +- UPPER LEFT CORNER "+" +- LOWER LEFT CORNER "+" +- CROSS "+" +- SCAN LINE 1 "~" +- SCAN LINE 3 "-" +- HORIZONTAL LINE "-" +- SCAN LINE 7 "-" +- SCAN LINE 9 "_" +- LEFT TEE "+" +- RIGHT TEE "+" +- BOTTOM TEE "+" +- TOP TEE "+" +- VERTICAL LINE "|" +- LESS THAN OR EQUAL "<" +- GREATER THAN OR EQUAL ">" +- PI "*" +- NOT EQUAL "!" +- POUND STERLING "f" +- BULLET "o" + +If your system's wide character type's character set corresponds to the +Universal Character Set (UCS/Unicode), the following wide string is a +good option to use:: + + L"→←↑↓■◆▒°±▒┘┐┌└┼⎺───⎽├┤┴┬│≤≥π≠£•" + +**Note that multibyte decoding is disabled in ACS mode.** The traditional +implementations of the "ansi" terminal type (i.e. IBM PCs and compatibles) +had no concept of multibyte encodings and used the character codes +outside the ASCII range for various special semigraphic characters. +(Technically they had an entire alternate character set as well via the +code page mechanism, but that's beyond the scope of this explanation.) + +The end result is that the terminfo definition of "ansi" sends characters +with the high bit set when in ACS mode. This breaks several multibyte +encoding schemes (including, most importantly, UTF-8). + +As a result, libtmt does not attempt to decode multibyte characters in +ACS mode, since that would break the multibyte encoding, the semigraphic +characters, or both. + +In general this isn't a problem, since programs explicitly switch to and +from ACS mode using escape sequences. + +When in ACS mode, bytes that are not special members of the alternate +character set (that is, bytes not mapped to the string provided to +`tmt_open`) are passed unchanged to the terminal. + +Supported Input and Escape Sequences +==================================== + +Internally libtmt uses your C library's/compiler's idea of a wide character +for all characters, so you should be able to use whatever characters you want +when writing to the virtual terminal (but see `Alternate Character Set`_). + +The following escape sequences are recognized and will be processed +specially. + +In the descriptions below, "ESC" means a literal escape character and "Ps" +means zero or more decimal numeric arguments separated by semicolons. +In descriptions "P1", "P2", etc, refer to the first parameter, second +parameter, and so on. If a required parameter is omitted, it defaults +to the smallest meaningful value (zero if the command accepts zero as +an argument, one otherwise). Any number of parameters may be passed, +but any after the first eight are ignored. + +Unless explicitly stated below, cursor motions past the edges of the screen +are ignored and do not result in scrolling. When characters are moved, +the spaces left behind are filled with blanks and any characters moved +off the edges of the screen are lost. + +====================== ====================================================================== +Sequence Action +====================== ====================================================================== +0x07 (Bell) Callback with TMT_MSG_BELL +0x08 (Backspace) Cursor left one cell +0x09 (Tab) Cursor to next tab stop or end of line +0x0a (Carriage Return) Cursor to first cell on this line +0x0d (Linefeed) Cursor to same column one line down, scroll if needed +ESC H Set a tabstop in this column +ESC 7 Save cursor position and current graphical state +ESC 8 Restore saved cursor position and current graphical state +ESC c Reset terminal to default state +ESC [ Ps A Cursor up P1 rows +ESC [ Ps B Cursor down P1 rows +ESC [ Ps C Cursor right P1 columns +ESC [ Ps D Cursor left P1 columns +ESC [ Ps E Cursor to first column of line P1 rows down from current +ESC [ Ps F Cursor to first column of line P1 rows up from current +ESC [ Ps G Cursor to column P1 +ESC [ Ps d Cursor to row P1 +ESC [ Ps H Cursor to row P1, column P2 +ESC [ Ps f Alias for ESC [ Ps H +ESC [ Ps I Cursor to next tab stop +ESC [ Ps J Clear screen + P1 == 0: from cursor to end of screen + P1 == 1: from beginning of screen to cursor + P1 == 2: entire screen +ESC [ Ps K Clear line + P1 == 0: from cursor to end of line + P1 == 1: from beginning of line to cursor + P1 == 2: entire line +ESC [ Ps L Insert P1 lines at cursor, scrolling lines below down +ESC [ Ps M Delete P1 lines at cursor, scrolling lines below up +ESC [ Ps P Delete P1 characters at cursor, moving characters to the right over +ESC [ Ps S Scroll screen up P1 lines +ESC [ Ps T Scroll screen down P1 lines +ESC [ Ps X Erase P1 characters at cursor (overwrite with spaces) +ESC [ Ps Z Go to previous tab stop +ESC [ Ps b Repeat previous character P1 times +ESC [ Ps c Callback with TMT_MSG_ANSWER "\033[?6c" +ESC [ Ps g If P1 == 3, clear all tabstops +ESC [ Ps h If P1 == 25, show the cursor (if it was hidden) +ESC [ Ps m Change graphical rendition state; see below +ESC [ Ps l If P1 == 25, hide the cursor +ESC [ Ps n If P1 == 6, callback with TMT_MSG_ANSWER "\033[%d;%dR" + with cursor row, column +ESC [ Ps s Alias for ESC 7 +ESC [ Ps u Alias for ESC 8 +ESC [ Ps @ Insert P1 blank spaces at cursor, moving characters to the right over +====================== ====================================================================== + +For the `ESC [ Ps m` escape sequence above ("Set Graphic Rendition"), +up to eight parameters may be passed; the results are cumulative: + +============== ================================================= +Rendition Code Meaning +============== ================================================= +0 Reset all graphic rendition attributes to default +1 Bold +2 Dim (half bright) +4 Underline +5 Blink +7 Reverse video +8 Invisible +10 Leave ACS mode +11 Enter ACS mode +22 Bold off +23 Dim (half bright) off +24 Underline off +25 Blink off +27 Reverse video off +28 Invisible off +30 Foreground black +31 Foreground red +32 Foreground green +33 Foreground yellow +34 Foreground blue +35 Foreground magenta +36 Foreground cyan +37 Foreground white +39 Foreground default color +40 Background black +41 Background red +42 Background green +43 Background yellow +44 Background blue +45 Background magenta +46 Background cyan +47 Background white +49 Background default color +============== ================================================= + +Other escape sequences are recognized but ignored. This includes escape +sequences for switching out codesets (officially, all code sets are defined +as equivalent in libtmt), and the various "Media Copy" escape sequences +used to print output on paper (officially, there is no printer attached +to libtmt). + +Additionally, "?" characters are stripped out of escape sequence parameter +lists for compatibility purposes. + +Known Issues +============ + +- Combining characters are "handled" by ignoring them + (when compiled with `TMT_HAS_WCWIDTH`) or by printing them separately. +- Double-width characters are rendered as single-width invalid + characters. +- The documentation and error messages are available only in English. + +Frequently Asked Questions +========================== + +What programs work with libtmt? +------------------------------- + +Pretty much all of them. Any program that doesn't assume what terminal +it's running under should work without problem; this includes any program +that uses the terminfo, termcap, or (pd|n)?curses libraries. Any program +that assumes it's running under some specific terminal might fail if its +assumption is wrong, and not just under libtmt. + +I've tested quite a few applications in libtmt and they've worked flawlessly: +vim, GNU emacs, nano, cmus, mc (Midnight Commander), and others just work +with no changes. + +What programs don't work with libtmt? +------------------------------------- + +Breakage with libtmt is of two kinds: breakage due to assuming a terminal +type, and reduced functionality. + +In all my testing, I only found one program that didn't work correctly by +default with libtmt: recent versions of Debian's `apt`_ assume a terminal +with definable scrolling regions to draw a fancy progress bar during +package installation. Using apt in its default configuration in libtmt will +result in a corrupted display (that can be fixed by clearing the screen). + +.. _`apt`: https://wiki.debian.org/Apt + +In my honest opinion, this is a bug in apt: it shouldn't assume the type +of terminal it's running in. + +The second kind of breakage is when not all of a program's features are +available. The biggest missing feature here is mouse support: libtmt +doesn't, and probably never will, support mouse tracking. I know of many +programs that *can* use mouse tracking in a terminal, but I don't know +of any that *require* it. Most (if not all?) programs of this kind would +still be completely usable in libtmt. + +License +------- + +Copyright (c) 2017 Rob King +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +- Neither the name of the copyright holder nor the + names of contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS, +COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/libtmt/tmt.c b/vendor/libtmt/tmt.c new file mode 100644 index 00000000..73be670d --- /dev/null +++ b/vendor/libtmt/tmt.c @@ -0,0 +1,506 @@ +/* Copyright (c) 2017 Rob King + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the copyright holder nor the + * names of contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS, + * COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "tmt.h" + +#define BUF_MAX 100 +#define PAR_MAX 8 +#define TAB 8 +#define MAX(x, y) (((size_t)(x) > (size_t)(y)) ? (size_t)(x) : (size_t)(y)) +#define MIN(x, y) (((size_t)(x) < (size_t)(y)) ? (size_t)(x) : (size_t)(y)) +#define CLINE(vt) (vt)->screen.lines[MIN((vt)->curs.r, (vt)->screen.nline - 1)] + +#define P0(x) (vt->pars[x]) +#define P1(x) (vt->pars[x]? vt->pars[x] : 1) +#define CB(vt, m, a) ((vt)->cb? (vt)->cb(m, vt, a, (vt)->p) : (void)0) +#define INESC ((vt)->state) + +#define COMMON_VARS \ + TMTSCREEN *s = &vt->screen; \ + TMTPOINT *c = &vt->curs; \ + TMTLINE *l = CLINE(vt); \ + TMTCHAR *t = vt->tabs->chars; \ + /* ignore -Wunused-variable */ (void)t;(void)l;(void)c;(void)s + +#define HANDLER(name) static void name (TMT *vt) { COMMON_VARS; + +struct TMT{ + TMTPOINT curs, oldcurs; + TMTATTRS attrs, oldattrs; + + bool dirty, acs, ignored; + TMTSCREEN screen; + TMTLINE *tabs; + + TMTCALLBACK cb; + void *p; + const wchar_t *acschars; + + mbstate_t ms; + size_t nmb; + char mb[BUF_MAX + 1]; + + size_t pars[PAR_MAX]; + size_t npar; + size_t arg; + enum {S_NUL, S_ESC, S_ARG} state; +}; + +static TMTATTRS defattrs = {.fg = TMT_COLOR_DEFAULT, .bg = TMT_COLOR_DEFAULT}; +static void writecharatcurs(TMT *vt, wchar_t w); + +static wchar_t +tacs(const TMT *vt, unsigned char c) +{ + /* The terminfo alternate character set for ANSI. */ + static unsigned char map[] = {0020U, 0021U, 0030U, 0031U, 0333U, 0004U, + 0261U, 0370U, 0361U, 0260U, 0331U, 0277U, + 0332U, 0300U, 0305U, 0176U, 0304U, 0304U, + 0304U, 0137U, 0303U, 0264U, 0301U, 0302U, + 0263U, 0363U, 0362U, 0343U, 0330U, 0234U, + 0376U}; + for (size_t i = 0; i < sizeof(map); i++) if (map[i] == c) + return vt->acschars[i]; + return (wchar_t)c; +} + +static void +dirtylines(TMT *vt, size_t s, size_t e) +{ + vt->dirty = true; + for (size_t i = s; i < e; i++) + vt->screen.lines[i]->dirty = true; +} + +static void +clearline(TMT *vt, TMTLINE *l, size_t s, size_t e) +{ + vt->dirty = l->dirty = true; + for (size_t i = s; i < e && i < vt->screen.ncol; i++){ + l->chars[i].a = defattrs; + l->chars[i].c = L' '; + } +} + +static void +clearlines(TMT *vt, size_t r, size_t n) +{ + for (size_t i = r; i < r + n && i < vt->screen.nline; i++) + clearline(vt, vt->screen.lines[i], 0, vt->screen.ncol); +} + +static void +scrup(TMT *vt, size_t r, size_t n) +{ + n = MIN(n, vt->screen.nline - 1 - r); + + if (n){ + TMTLINE *buf[n]; + + memcpy(buf, vt->screen.lines + r, n * sizeof(TMTLINE *)); + memmove(vt->screen.lines + r, vt->screen.lines + r + n, + (vt->screen.nline - n - r) * sizeof(TMTLINE *)); + memcpy(vt->screen.lines + (vt->screen.nline - n), + buf, n * sizeof(TMTLINE *)); + + clearlines(vt, vt->screen.nline - n, n); + dirtylines(vt, r, vt->screen.nline); + } +} + +static void +scrdn(TMT *vt, size_t r, size_t n) +{ + n = MIN(n, vt->screen.nline - 1 - r); + + if (n){ + TMTLINE *buf[n]; + + memcpy(buf, vt->screen.lines + (vt->screen.nline - n), + n * sizeof(TMTLINE *)); + memmove(vt->screen.lines + r + n, vt->screen.lines + r, + (vt->screen.nline - n - r) * sizeof(TMTLINE *)); + memcpy(vt->screen.lines + r, buf, n * sizeof(TMTLINE *)); + + clearlines(vt, r, n); + dirtylines(vt, r, vt->screen.nline); + } +} + +HANDLER(ed) + size_t b = 0; + size_t e = s->nline; + + switch (P0(0)){ + case 0: b = c->r + 1; clearline(vt, l, c->c, vt->screen.ncol); break; + case 1: e = c->r - 1; clearline(vt, l, 0, c->c); break; + case 2: /* use defaults */ break; + default: /* do nothing */ return; + } + + clearlines(vt, b, e - b); +} + +HANDLER(ich) + size_t n = P1(0); /* XXX use MAX */ + if (n > s->ncol - c->c - 1) n = s->ncol - c->c - 1; + + memmove(l->chars + c->c + n, l->chars + c->c, + MIN(s->ncol - 1 - c->c, + (s->ncol - c->c - n - 1)) * sizeof(TMTCHAR)); + clearline(vt, l, c->c, n); +} + +HANDLER(dch) + size_t n = P1(0); /* XXX use MAX */ + if (n > s->ncol - c->c) n = s->ncol - c->c; + else if (n == 0) return; + + memmove(l->chars + c->c, l->chars + c->c + n, + (s->ncol - c->c - n) * sizeof(TMTCHAR)); + + clearline(vt, l, s->ncol - n, s->ncol); + /* VT102 manual says the attribute for the newly empty characters + * should be the same as the last character moved left, which isn't + * what clearline() currently does. + */ +} + +HANDLER(el) + switch (P0(0)){ + case 0: clearline(vt, l, c->c, vt->screen.ncol); break; + case 1: clearline(vt, l, 0, MIN(c->c + 1, s->ncol - 1)); break; + case 2: clearline(vt, l, 0, vt->screen.ncol); break; + } +} + +HANDLER(sgr) + #define FGBG(c) *(P0(i) < 40? &vt->attrs.fg : &vt->attrs.bg) = c + for (size_t i = 0; i < vt->npar; i++) switch (P0(i)){ + case 0: vt->attrs = defattrs; break; + case 1: case 22: vt->attrs.bold = P0(0) < 20; break; + case 2: case 23: vt->attrs.dim = P0(0) < 20; break; + case 4: case 24: vt->attrs.underline = P0(0) < 20; break; + case 5: case 25: vt->attrs.blink = P0(0) < 20; break; + case 7: case 27: vt->attrs.reverse = P0(0) < 20; break; + case 8: case 28: vt->attrs.invisible = P0(0) < 20; break; + case 10: case 11: vt->acs = P0(0) > 10; break; + case 30: case 40: FGBG(TMT_COLOR_BLACK); break; + case 31: case 41: FGBG(TMT_COLOR_RED); break; + case 32: case 42: FGBG(TMT_COLOR_GREEN); break; + case 33: case 43: FGBG(TMT_COLOR_YELLOW); break; + case 34: case 44: FGBG(TMT_COLOR_BLUE); break; + case 35: case 45: FGBG(TMT_COLOR_MAGENTA); break; + case 36: case 46: FGBG(TMT_COLOR_CYAN); break; + case 37: case 47: FGBG(TMT_COLOR_WHITE); break; + case 39: case 49: FGBG(TMT_COLOR_DEFAULT); break; + } +} + +HANDLER(rep) + if (!c->c) return; + wchar_t r = l->chars[c->c - 1].c; + for (size_t i = 0; i < P1(0); i++) + writecharatcurs(vt, r); +} + +HANDLER(dsr) + char r[BUF_MAX + 1] = {0}; + snprintf(r, BUF_MAX, "\033[%zd;%zdR", c->r + 1, c->c + 1); + CB(vt, TMT_MSG_ANSWER, (const char *)r); +} + +HANDLER(resetparser) + memset(vt->pars, 0, sizeof(vt->pars)); + vt->state = vt->npar = vt->arg = vt->ignored = (bool)0; +} + +HANDLER(consumearg) + if (vt->npar < PAR_MAX) + vt->pars[vt->npar++] = vt->arg; + vt->arg = 0; +} + +HANDLER(fixcursor) + c->r = MIN(c->r, s->nline - 1); + c->c = MIN(c->c, s->ncol - 1); +} + +static bool +handlechar(TMT *vt, char i) +{ + COMMON_VARS; + + char cs[] = {i, 0}; + #define ON(S, C, A) if (vt->state == (S) && strchr(C, i)){ A; return true;} + #define DO(S, C, A) ON(S, C, consumearg(vt); if (!vt->ignored) {A;} \ + fixcursor(vt); resetparser(vt);); + + DO(S_NUL, "\x07", CB(vt, TMT_MSG_BELL, NULL)) + DO(S_NUL, "\x08", if (c->c) c->c--) + DO(S_NUL, "\x09", while (++c->c < s->ncol - 1 && t[c->c].c != L'*')) + DO(S_NUL, "\x0a", c->r < s->nline - 1? (void)c->r++ : scrup(vt, 0, 1)) + DO(S_NUL, "\x0d", c->c = 0) + ON(S_NUL, "\x1b", vt->state = S_ESC) + ON(S_ESC, "\x1b", vt->state = S_ESC) + DO(S_ESC, "H", t[c->c].c = L'*') + DO(S_ESC, "7", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs) + DO(S_ESC, "8", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs) + ON(S_ESC, "+*()", vt->ignored = true; vt->state = S_ARG) + DO(S_ESC, "c", tmt_reset(vt)) + ON(S_ESC, "[", vt->state = S_ARG) + ON(S_ARG, "\x1b", vt->state = S_ESC) + ON(S_ARG, ";", consumearg(vt)) + ON(S_ARG, "?", (void)0) + ON(S_ARG, "0123456789", vt->arg = vt->arg * 10 + atoi(cs)) + DO(S_ARG, "A", c->r = MAX(c->r - P1(0), 0)) + DO(S_ARG, "B", c->r = MIN(c->r + P1(0), s->nline - 1)) + DO(S_ARG, "C", c->c = MIN(c->c + P1(0), s->ncol - 1)) + DO(S_ARG, "D", c->c = MIN(c->c - P1(0), c->c)) + DO(S_ARG, "E", c->c = 0; c->r = MIN(c->r + P1(0), s->nline - 1)) + DO(S_ARG, "F", c->c = 0; c->r = MAX(c->r - P1(0), 0)) + DO(S_ARG, "G", c->c = MIN(P1(0) - 1, s->ncol - 1)) + DO(S_ARG, "d", c->r = MIN(P1(0) - 1, s->nline - 1)) + DO(S_ARG, "Hf", c->r = P1(0) - 1; c->c = P1(1) - 1) + DO(S_ARG, "I", while (++c->c < s->ncol - 1 && t[c->c].c != L'*')) + DO(S_ARG, "J", ed(vt)) + DO(S_ARG, "K", el(vt)) + DO(S_ARG, "L", scrdn(vt, c->r, P1(0))) + DO(S_ARG, "M", scrup(vt, c->r, P1(0))) + DO(S_ARG, "P", dch(vt)) + DO(S_ARG, "S", scrup(vt, 0, P1(0))) + DO(S_ARG, "T", scrdn(vt, 0, P1(0))) + DO(S_ARG, "X", clearline(vt, l, c->c, P1(0))) + DO(S_ARG, "Z", while (c->c && t[--c->c].c != L'*')) + DO(S_ARG, "b", rep(vt)); + DO(S_ARG, "c", CB(vt, TMT_MSG_ANSWER, "\033[?6c")) + DO(S_ARG, "g", if (P0(0) == 3) clearline(vt, vt->tabs, 0, s->ncol)) + DO(S_ARG, "m", sgr(vt)) + DO(S_ARG, "n", if (P0(0) == 6) dsr(vt)) + DO(S_ARG, "h", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "t")) + DO(S_ARG, "i", (void)0) + DO(S_ARG, "l", if (P0(0) == 25) CB(vt, TMT_MSG_CURSOR, "f")) + DO(S_ARG, "s", vt->oldcurs = vt->curs; vt->oldattrs = vt->attrs) + DO(S_ARG, "u", vt->curs = vt->oldcurs; vt->attrs = vt->oldattrs) + DO(S_ARG, "@", ich(vt)) + + return resetparser(vt), false; +} + +static void +notify(TMT *vt, bool update, bool moved) +{ + if (update) CB(vt, TMT_MSG_UPDATE, &vt->screen); + if (moved) CB(vt, TMT_MSG_MOVED, &vt->curs); +} + +static TMTLINE * +allocline(TMT *vt, TMTLINE *o, size_t n, size_t pc) +{ + TMTLINE *l = realloc(o, sizeof(TMTLINE) + n * sizeof(TMTCHAR)); + if (!l) return NULL; + + clearline(vt, l, pc, n); + return l; +} + +static void +freelines(TMT *vt, size_t s, size_t n, bool screen) +{ + for (size_t i = s; vt->screen.lines && i < s + n; i++){ + free(vt->screen.lines[i]); + vt->screen.lines[i] = NULL; + } + if (screen) free(vt->screen.lines); +} + +TMT * +tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p, + const wchar_t *acs) +{ + TMT *vt = calloc(1, sizeof(TMT)); + if (!nline || !ncol || !vt) return free(vt), NULL; + + /* ASCII-safe defaults for box-drawing characters. */ + vt->acschars = acs? acs : L"><^v#+:o##+++++~---_++++|<>*!fo"; + vt->cb = cb; + vt->p = p; + + if (!tmt_resize(vt, nline, ncol)) return tmt_close(vt), NULL; + return vt; +} + +void +tmt_close(TMT *vt) +{ + free(vt->tabs); + freelines(vt, 0, vt->screen.nline, true); + free(vt); +} + +bool +tmt_resize(TMT *vt, size_t nline, size_t ncol) +{ + if (nline < 2 || ncol < 2) return false; + if (nline < vt->screen.nline) + freelines(vt, nline, vt->screen.nline - nline, false); + + TMTLINE **l = realloc(vt->screen.lines, nline * sizeof(TMTLINE *)); + if (!l) return false; + + size_t pc = vt->screen.ncol; + vt->screen.lines = l; + vt->screen.ncol = ncol; + for (size_t i = 0; i < nline; i++){ + TMTLINE *nl = NULL; + if (i >= vt->screen.nline) + nl = vt->screen.lines[i] = allocline(vt, NULL, ncol, 0); + else + nl = allocline(vt, vt->screen.lines[i], ncol, pc); + + if (!nl) return false; + vt->screen.lines[i] = nl; + } + vt->screen.nline = nline; + + vt->tabs = allocline(vt, vt->tabs, ncol, 0); + if (!vt->tabs) return free(l), false; + vt->tabs->chars[0].c = vt->tabs->chars[ncol - 1].c = L'*'; + for (size_t i = 0; i < ncol; i++) if (i % TAB == 0) + vt->tabs->chars[i].c = L'*'; + + fixcursor(vt); + dirtylines(vt, 0, nline); + notify(vt, true, true); + return true; +} + +static void +writecharatcurs(TMT *vt, wchar_t w) +{ + COMMON_VARS; + + #ifdef TMT_HAS_WCWIDTH + extern int wcwidth(wchar_t c); + if (wcwidth(w) > 1) w = TMT_INVALID_CHAR; + if (wcwidth(w) < 0) return; + #endif + + CLINE(vt)->chars[vt->curs.c].c = w; + CLINE(vt)->chars[vt->curs.c].a = vt->attrs; + CLINE(vt)->dirty = vt->dirty = true; + + if (c->c < s->ncol - 1) + c->c++; + else{ + c->c = 0; + c->r++; + } + + if (c->r >= s->nline){ + c->r = s->nline - 1; + scrup(vt, 0, 1); + } +} + +static inline size_t +testmbchar(TMT *vt) +{ + mbstate_t ts = vt->ms; + return vt->nmb? mbrtowc(NULL, vt->mb, vt->nmb, &ts) : (size_t)-2; +} + +static inline wchar_t +getmbchar(TMT *vt) +{ + wchar_t c = 0; + size_t n = mbrtowc(&c, vt->mb, vt->nmb, &vt->ms); + vt->nmb = 0; + return (n == (size_t)-1 || n == (size_t)-2)? TMT_INVALID_CHAR : c; +} + +void +tmt_write(TMT *vt, const char *s, size_t n) +{ + TMTPOINT oc = vt->curs; + n = n? n : strlen(s); + + for (size_t p = 0; p < n; p++){ + if (handlechar(vt, s[p])) + continue; + else if (vt->acs) + writecharatcurs(vt, tacs(vt, (unsigned char)s[p])); + else if (vt->nmb >= BUF_MAX) + writecharatcurs(vt, getmbchar(vt)); + else{ + switch (testmbchar(vt)){ + case (size_t)-1: writecharatcurs(vt, getmbchar(vt)); break; + case (size_t)-2: vt->mb[vt->nmb++] = s[p]; break; + } + + if (testmbchar(vt) <= MB_LEN_MAX) + writecharatcurs(vt, getmbchar(vt)); + } + } + + notify(vt, vt->dirty, memcmp(&oc, &vt->curs, sizeof(oc)) != 0); +} + +const TMTSCREEN * +tmt_screen(const TMT *vt) +{ + return &vt->screen; +} + +const TMTPOINT * +tmt_cursor(const TMT *vt) +{ + return &vt->curs; +} + +void +tmt_clean(TMT *vt) +{ + for (size_t i = 0; i < vt->screen.nline; i++) + vt->dirty = vt->screen.lines[i]->dirty = false; +} + +void +tmt_reset(TMT *vt) +{ + vt->curs.r = vt->curs.c = vt->oldcurs.r = vt->oldcurs.c = vt->acs = (bool)0; + resetparser(vt); + vt->attrs = vt->oldattrs = defattrs; + memset(&vt->ms, 0, sizeof(vt->ms)); + clearlines(vt, 0, vt->screen.nline); + CB(vt, TMT_MSG_CURSOR, "t"); + notify(vt, true, true); +} diff --git a/vendor/libtmt/tmt.h b/vendor/libtmt/tmt.h new file mode 100644 index 00000000..ae0ddbb9 --- /dev/null +++ b/vendor/libtmt/tmt.h @@ -0,0 +1,140 @@ +/* Copyright (c) 2017 Rob King + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the copyright holder nor the + * names of contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS, + * COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef TMT_H +#define TMT_H + +#include <stdbool.h> +#include <stddef.h> +#include <wchar.h> + +/**** INVALID WIDE CHARACTER */ +#ifndef TMT_INVALID_CHAR +#define TMT_INVALID_CHAR ((wchar_t)0xfffd) +#endif + +/**** INPUT SEQUENCES */ +#define TMT_KEY_UP "\033[A" +#define TMT_KEY_DOWN "\033[B" +#define TMT_KEY_RIGHT "\033[C" +#define TMT_KEY_LEFT "\033[D" +#define TMT_KEY_HOME "\033[H" +#define TMT_KEY_END "\033[Y" +#define TMT_KEY_INSERT "\033[L" +#define TMT_KEY_BACKSPACE "\x08" +#define TMT_KEY_ESCAPE "\x1b" +#define TMT_KEY_BACK_TAB "\033[Z" +#define TMT_KEY_PAGE_UP "\033[V" +#define TMT_KEY_PAGE_DOWN "\033[U" +#define TMT_KEY_F1 "\033OP" +#define TMT_KEY_F2 "\033OQ" +#define TMT_KEY_F3 "\033OR" +#define TMT_KEY_F4 "\033OS" +#define TMT_KEY_F5 "\033OT" +#define TMT_KEY_F6 "\033OU" +#define TMT_KEY_F7 "\033OV" +#define TMT_KEY_F8 "\033OW" +#define TMT_KEY_F9 "\033OX" +#define TMT_KEY_F10 "\033OY" + +/**** BASIC DATA STRUCTURES */ +typedef struct TMT TMT; + +typedef enum{ + TMT_COLOR_DEFAULT = -1, + TMT_COLOR_BLACK = 1, + TMT_COLOR_RED, + TMT_COLOR_GREEN, + TMT_COLOR_YELLOW, + TMT_COLOR_BLUE, + TMT_COLOR_MAGENTA, + TMT_COLOR_CYAN, + TMT_COLOR_WHITE, + TMT_COLOR_MAX +} tmt_color_t; + +typedef struct TMTATTRS TMTATTRS; +struct TMTATTRS{ + bool bold; + bool dim; + bool underline; + bool blink; + bool reverse; + bool invisible; + tmt_color_t fg; + tmt_color_t bg; +}; + +typedef struct TMTCHAR TMTCHAR; +struct TMTCHAR{ + wchar_t c; + TMTATTRS a; +}; + +typedef struct TMTPOINT TMTPOINT; +struct TMTPOINT{ + size_t r; + size_t c; +}; + +typedef struct TMTLINE TMTLINE; +struct TMTLINE{ + bool dirty; + TMTCHAR chars[]; +}; + +typedef struct TMTSCREEN TMTSCREEN; +struct TMTSCREEN{ + size_t nline; + size_t ncol; + + TMTLINE **lines; +}; + +/**** CALLBACK SUPPORT */ +typedef enum{ + TMT_MSG_MOVED, + TMT_MSG_UPDATE, + TMT_MSG_ANSWER, + TMT_MSG_BELL, + TMT_MSG_CURSOR +} tmt_msg_t; + +typedef void (*TMTCALLBACK)(tmt_msg_t m, struct TMT *v, const void *r, void *p); + +/**** PUBLIC FUNCTIONS */ +TMT *tmt_open(size_t nline, size_t ncol, TMTCALLBACK cb, void *p, + const wchar_t *acs); +void tmt_close(TMT *vt); +bool tmt_resize(TMT *vt, size_t nline, size_t ncol); +void tmt_write(TMT *vt, const char *s, size_t n); +const TMTSCREEN *tmt_screen(const TMT *vt); +const TMTPOINT *tmt_cursor(const TMT *vt); +void tmt_clean(TMT *vt); +void tmt_reset(TMT *vt); + +#endif diff --git a/virtual-programs/apriltags.folk b/virtual-programs/apriltags.folk index 01377b78..ac2c4f8b 100644 --- a/virtual-programs/apriltags.folk +++ b/virtual-programs/apriltags.folk @@ -1,6 +1,7 @@ if {$::isLaptop} return -On process { +# Plain detector. Runs on entire camera frame. +set mainDetectorProcess [On process { source pi/AprilTags.tcl AprilTags::init @@ -9,29 +10,131 @@ On process { Retract /anyone/ wishes $::thisProcess shares all wishes Retract /anyone/ wishes $::thisProcess shares all claims Wish $::thisProcess receives statements like \ - [list /someone/ claims the camera frame is /grayFrame/] + [list /someone/ claims the camera frame is /grayFrame/ at /timestamp/] Wish $::thisProcess shares statements like \ [list /someone/ wishes /process/ receives statements like /pattern/] Wish $::thisProcess shares statements like \ - [list /someone/ claims tag /tag/ has center /center/ size /size/] - Wish $::thisProcess shares statements like \ - [list /someone/ claims tag /tag/ has corners /corners/] - Wish $::thisProcess shares statements like \ - [list /someone/ claims the AprilTag time is /aprilTime/] + [list /someone/ claims /process/ detects tags /tags/ at /timestamp/ in time /aprilTime/] Wish $::thisProcess shares statements like \ [list /someone/ claims $::thisProcess has pid /pid/] - When the camera frame is /grayFrame/ { + When the camera frame is /grayFrame/ at /timestamp/ { set aprilTime [time { set tags [AprilTags::detect $grayFrame] }] - Commit { - Claim the AprilTag time is $aprilTime + Claim $::thisProcess detects tags $tags at $timestamp in time $aprilTime + } +}] + +# Incremental detector. Looks at regions where there were tags in the +# old camera frame. +On process { + source pi/AprilTags.tcl + AprilTags::init - foreach tag $tags { - Claim tag [dict get $tag id] has center [dict get $tag center] size [dict get $tag size] - Claim tag [dict get $tag id] has corners [dict get $tag corners] + # TODO: Clean this up. We retract these so that we don't bounce + # statements back to the main Folk process that it sends us. + Retract /anyone/ wishes $::thisProcess shares all wishes + Retract /anyone/ wishes $::thisProcess shares all claims + Wish $::thisProcess receives statements like \ + [list /someone/ claims the camera frame is /grayFrame/ at /timestamp/] + Wish $::thisProcess receives statements like \ + [list /someone/ claims $mainDetectorProcess detects tags /tags/ at /timestamp/ in time /aprilTime/] + Wish $::thisProcess shares statements like \ + [list /someone/ wishes /process/ receives statements like /pattern/] + Wish $::thisProcess shares statements like \ + [list /someone/ claims $::thisProcess has pid /pid/] + Wish $::thisProcess shares statements like \ + [list /someone/ claims $::thisProcess detects tags /tags/ at /timestamp/ in time /aprilTime/] + Wish $::thisProcess shares statements like \ + [list /someone/ wishes /something/ is labelled /text/] + Wish $::thisProcess shares statements like \ + [list /someone/ wishes /something/ displays camera slice /slice/] + + proc subimage {im x y subwidth subheight} { + dict with im { + set x [expr {int($x)}] + set y [expr {int($y)}] + set subdata [expr {$data + ($y*$width + $x) * $components}] + dict create \ + width [int $subwidth] \ + height [int $subheight] \ + components $components \ + bytesPerRow $bytesPerRow \ + data [format 0x%x $subdata] + } + } + + When the camera frame is /grayFrame/ at /timestamp/ & \ + /process/ detects tags /prevTags/ at /something/ in time /something/ { + + if {$process eq $::thisProcess} { return } + + set tags [list] + set frameWidth [dict get $grayFrame width] + set frameHeight [dict get $grayFrame height] + set aprilTime 0 + foreach prevTag $prevTags { + set size [dict get $prevTag size] + set corners [dict get $prevTag corners] + set x [min {*}[lmap corner $corners {lindex $corner 0}]] + set y [min {*}[lmap corner $corners {lindex $corner 1}]] + set x1 [max {*}[lmap corner $corners {lindex $corner 0}]] + set y1 [max {*}[lmap corner $corners {lindex $corner 1}]] + + set x [max [- $x $size] 0] + set y [max [- $y $size] 0] + set x1 [min [+ $x1 $size] $frameWidth] + set y1 [min [+ $y1 $size] $frameHeight] + + set subimage [subimage $grayFrame $x $y [- $x1 $x] [- $y1 $y]] + set aprilTime [+ $aprilTime [baretime { + foreach tag [AprilTags::detect $subimage] { + dict with tag { + set center [vec2 add $center [list $x $y]] + set corners [lmap corner $corners {vec2 add $corner [list $x $y]}] + } + lappend tags $tag + } + }]] + } + + # Wish 6 is labelled "[llength $prevTags] -> [llength $tags] ($aprilTime us)" + + Claim $::thisProcess detects tags $tags at $timestamp in time $aprilTime + # Wish 6 is labelled "\n\nStep time $::stepTime\n[join [lmap s [dict values [Statements::all]] {statement short $s}] "\n"]" + } +} + +# This cache is used to remember the last seen position of each tag, +# so that if the incremental detector blinks out, we still use the +# tag's last-found position from it, instead of the older position +# from the full detector, so as you move a tag its position doesn't +# glitch backward. +set ::tagsCache [dict create] +# TODO: Garbage-collect this cache. + +When the collected matches for [list /someone/ detects tags /tags/ at /timestamp/ in time /aprilTime/] are /matches/ { + set tagsSeen [dict create] + foreach match $matches { + set timestamp [dict get $match timestamp] + foreach tag [dict get $match tags] { + set id [dict get $tag id] + dict set tag timestamp $timestamp + + if {[dict exists $::tagsCache $id] && + [dict get $::tagsCache $id timestamp] > $timestamp} { + set tag [dict get $::tagsCache $id] + } else { + dict set ::tagsCache $id $tag } + dict set tagsSeen $id $tag } } + + dict for {id tag} $tagsSeen { + Claim tag $id has center [dict get $tag center] size [dict get $tag size] + Claim tag $id has corners [dict get $tag corners] + } + Claim the AprilTag time is [lmap m $matches {dict get $m aprilTime}] } diff --git a/virtual-programs/camera.folk b/virtual-programs/camera.folk index d9dab784..0a95c8f0 100644 --- a/virtual-programs/camera.folk +++ b/virtual-programs/camera.folk @@ -18,15 +18,18 @@ On process { source pi/Camera.tcl Camera::init $width $height - puts "Camera tid: [getTid]" - - forever { - set cameraTime [time { - set grayFrame [Camera::grayFrame] - }] + puts "Camera tid: [getTid] booting at [clock milliseconds]" + + When $::thisProcess has step count /c/ { + set grayFrame [Camera::grayFrame] Commit { - Claim the camera time is $cameraTime - Claim the camera frame is $grayFrame + Claim the camera time is $::stepTime + Claim the camera frame is $grayFrame at [clock milliseconds] } } } + +# For backward compatibility. +When the camera frame is /grayFrame/ at /timestamp/ { + Claim the camera frame is $grayFrame +} diff --git a/virtual-programs/display.folk b/virtual-programs/display.folk new file mode 100644 index 00000000..d3f6b610 --- /dev/null +++ b/virtual-programs/display.folk @@ -0,0 +1,93 @@ +if {$::isLaptop} return + +namespace eval ::Display { + variable WIDTH + variable HEIGHT + variable LAYER 0 + regexp {mode "(\d+)x(\d+)"} [exec fbset] -> WIDTH HEIGHT + + proc drawOnTop {func args} { + set ::Display::LAYER 1 + uplevel [list $func {*}$args] + set ::Display::LAYER 0 + } + + proc stroke {points width color} { + uplevel [list Wish display runs [list Display::stroke $points $width $color] on layer $::Display::LAYER] + } + + proc circle {x y radius thickness color} { + uplevel [list Wish display runs [list Display::circle $x $y $radius $thickness $color] on layer $::Display::LAYER] + } + + proc text args { + uplevel [list Wish display runs [list Display::text {*}$args] on layer $::Display::LAYER] + } + + proc fillTriangle args { + uplevel [list Wish display runs [list Display::fillTriangle {*}$args] on layer $::Display::LAYER] + } + + proc fillQuad args { + uplevel [list Wish display runs [list Display::fillQuad {*}$args] on layer $::Display::LAYER] + } + + proc fillPolygon args { + uplevel [list Wish display runs [list Display::fillPolygon {*}$args] on layer $::Display::LAYER] + } + + variable displayTime none +} + +On process { + source pi/Display.tcl + Display::init + puts "Display tid: [getTid]" + + # TODO: Clean this up. We retract these so that we don't bounce + # statements back to the main Folk process that it sends us. + Retract /anyone/ wishes $::thisProcess shares all wishes + Retract /anyone/ wishes $::thisProcess shares all claims + Wish $::thisProcess shares statements like \ + [list /someone/ wishes /process/ receives statements like /pattern/] + Wish $::thisProcess shares statements like \ + [list /someone/ claims $::thisProcess has pid /pid/] + Wish $::thisProcess receives statements like \ + [list /someone/ wishes display runs /command/ on layer /layer/] + Wish $::thisProcess receives statements like \ + [list /someone/ wishes display runs /command/] + Wish $::thisProcess shares statements like \ + [list /someone/ claims the display time is /displayTime/] + + while true { + set displayList [list] + foreach match [Statements::findMatches {/someone/ wishes display runs /command/ on layer /layer/}] { + lappend displayList [list [dict get $match layer] [dict get $match command]] + } + foreach match [Statements::findMatches {/someone/ wishes display runs /command/}] { + lappend displayList [list 0 [dict get $match command]] + } + + proc lcomp {a b} { + set layerA [lindex $a 0] + set layerB [lindex $b 0] + if {$layerA == $layerB} { + expr {[lindex $a 1 0] == "Display::text"} + } else { + expr {$layerA - $layerB} + } + } + + set displayCommands [lmap sublist [lsort -command lcomp $displayList] {lindex $sublist 1}] + + set renderTime [baretime [list foreach command $displayCommands { {*}$command }]] + set commitTime [baretime commitThenClearStaging] + + Commit { Claim the display time is "render $renderTime us + commit $commitTime us ($::stepTime)" } + Step + } +} +# TODO: remove this compatibility hack +When the display time is /displayTime/ { + set ::Display::displayTime $displayTime +} diff --git a/virtual-programs/esc-restart.folk b/virtual-programs/esc-restart.folk index 01a6dd94..ee8aa847 100644 --- a/virtual-programs/esc-restart.folk +++ b/virtual-programs/esc-restart.folk @@ -1,5 +1,3 @@ -When the keyboard character log is /k/ { - foreach press $k { - if {$press eq "esc"} {exec sudo systemctl restart folk} - } -}
\ No newline at end of file +When keyboard claims key ESC is down with modifiers alt { + exec sudo systemctl restart folk +} diff --git a/virtual-programs/images.folk b/virtual-programs/images.folk index 6846975e..ee3988ea 100644 --- a/virtual-programs/images.folk +++ b/virtual-programs/images.folk @@ -32,6 +32,7 @@ namespace eval ::image { defineImageType $cc $cc include <stdlib.h> $cc include <string.h> + $cc import ::Heap::cc folkHeapAlloc as folkHeapAlloc $cc code { #undef EXTERN @@ -40,18 +41,30 @@ namespace eval ::image { #include <unistd.h> void - jpeg(FILE* dest, uint8_t* rgb, uint32_t width, uint32_t height, int quality) + jpeg(FILE* dest, uint8_t* data, uint32_t components, uint32_t width, uint32_t height, int quality) { - JSAMPARRAY image; - image = calloc(height, sizeof (JSAMPROW)); - for (size_t i = 0; i < height; i++) { - image[i] = calloc(width * 3, sizeof (JSAMPLE)); - for (size_t j = 0; j < width; j++) { - image[i][j * 3 + 0] = rgb[(i * width + j)]; - image[i][j * 3 + 1] = rgb[(i * width + j)]; - image[i][j * 3 + 2] = rgb[(i * width + j)]; + JSAMPARRAY image; + if (components == 1) { + image = calloc(height, sizeof (JSAMPROW)); + for (size_t i = 0; i < height; i++) { + image[i] = calloc(width * 3, sizeof (JSAMPLE)); + for (size_t j = 0; j < width; j++) { + image[i][j * 3 + 0] = data[(i * width + j)]; + image[i][j * 3 + 1] = data[(i * width + j)]; + image[i][j * 3 + 2] = data[(i * width + j)]; + } + } + } else if (components == 3) { + image = calloc(height, sizeof (JSAMPROW)); + for (size_t i = 0; i < height; i++) { + image[i] = calloc(width * 3, sizeof (JSAMPLE)); + for (size_t j = 0; j < width; j++) { + image[i][j * 3 + 0] = data[(i * width + j) * 3]; + image[i][j * 3 + 1] = data[(i * width + j) * 3 + 1]; + image[i][j * 3 + 2] = data[(i * width + j) * 3 + 2]; + } + } } - } struct jpeg_compress_struct compress; struct jpeg_error_mgr error; @@ -79,11 +92,103 @@ namespace eval ::image { } $cc proc saveAsJpeg {image_t im char* filename} void { FILE* out = fopen(filename, "w"); - jpeg(out, im.data, im.width, im.height, 100); + jpeg(out, im.data, im.components, im.width, im.height, 100); fclose(out); } + # Given the four corners of a region in an image, warp it to a new image of a given width and height + $cc proc warp {image_t im uint32_t tl_x uint32_t tl_y uint32_t tr_x uint32_t tr_y uint32_t br_x uint32_t br_y uint32_t bl_x uint32_t bl_y uint32_t output_width uint32_t output_height} image_t { + image_t ret; + ret.width = output_width; + ret.height = output_height; + ret.components = im.components; + ret.bytesPerRow = ret.width * ret.components; + ret.data = folkHeapAlloc(ret.bytesPerRow * ret.height); + + for (int y = 0; y < output_height; y++) { + for (int x = 0; x < output_width; x++) { + // calculate the position in the input image + float u = (float)x / (float)(output_width - 1); + float v = (float)y / (float)(output_height - 1); + int input_x = tl_x + u * (int)(tr_x - tl_x) + v * (int)(bl_x - tl_x); + int input_y = tl_y + u * (int)(tr_y - tl_y) + v * (int)(bl_y - tl_y); + + if (input_x >= 0 && input_x < im.width && input_y >= 0 && input_y < im.height) { + memcpy(&ret.data[y * ret.bytesPerRow + x * ret.components], + &im.data[input_y * im.bytesPerRow + input_x * im.components], + im.components); + } + } + } + return ret; + } + $cc proc loadJpeg {char* filename} image_t { + FILE* file = fopen(filename, "rb"); + if (!file) { + fprintf(stderr, "Error opening file: %s\n", filename); + exit(1); + } + + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_decompress(&cinfo); + jpeg_stdio_src(&cinfo, file); + jpeg_read_header(&cinfo, TRUE); + jpeg_start_decompress(&cinfo); + + image_t ret; + ret.width = cinfo.output_width; + ret.height = cinfo.output_height; + ret.components = cinfo.output_components; + ret.bytesPerRow = ret.width * ret.components; + ret.data = folkHeapAlloc(ret.bytesPerRow * ret.height); + + JSAMPROW row_pointer[1]; + while (cinfo.output_scanline < cinfo.output_height) { + row_pointer[0] = (JSAMPLE*)ret.data + cinfo.output_scanline * ret.bytesPerRow; + jpeg_read_scanlines(&cinfo, row_pointer, 1); + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + fclose(file); + + return ret; + } + $cc proc freeJpeg {image_t im} void { + // TODO: Free the JPEG. + // ckfree(im.data); + } $cc compile + variable imagesCache [dict create] + # Loads a URL or file path if passed. If passed a valid image_t, + # just returns that image_t. + proc load {im} { + variable imagesCache + if {[dict exists $imagesCache $im]} { + set im [dict get $imagesCache $im] + } else { + set impath $im + if {[string match "http*://*" $impath]} { + set im /tmp/[regsub -all {\W+} $impath "_"] + exec -ignorestderr curl -o$im $impath + } + if {[string match "*jpg" $im] || + [string match "*jpeg" $im] || + [string match "*png" $im]} { + # TODO: Support .png + set path [expr {[file pathtype $im] eq "relative" ? + "$::env(HOME)/folk-images/$im" : + $im}] + set im [image loadJpeg $path] + dict set imagesCache $impath $im + } + } + set im + } + namespace export * namespace ensemble create } @@ -116,9 +221,28 @@ When when /p/ has camera slice /slice/ /lambda/ with environment /e/ { # Display a camera slice When /someone/ wishes /p/ displays camera slice /slice/ & /p/ has region /r/ { - set origin [lindex $r 0 0] + set center [region centroid $r] # set scale [expr {$Display::WIDTH / $Camera::WIDTH}] # Use 1x scale instead of $scale so the projected tag doesn't redetect. # TODO: Mask the tag out? - Wish display runs [list Display::image {*}$origin $slice 1] + Wish display runs [list Display::image {*}$center $slice 0 1] +} + +When /someone/ wishes /p/ displays image /im/ { + set im [image load $im] + When $p has region /r/ { + # Compute a scale for im that will fit in the region width/height + # Draw im with scale and rotation + set center [region centroid $r] + # set width [region width $r] + # set height [region height $r] + # set scale [expr {min($width / [image width $im], + # $height / [image height $im])}] + # Wish $p is labelled $im + Wish display runs [list Display::image {*}$center $im [region angle $r] 1] + } + # On unmatch { + # # HACK: Leaves time for the display to finish trying to display this. + # after 5000 [list image freeJpeg $im] + # } } diff --git a/virtual-programs/mask-tags.folk b/virtual-programs/mask-tags.folk new file mode 100644 index 00000000..4f0ae4b2 --- /dev/null +++ b/virtual-programs/mask-tags.folk @@ -0,0 +1,16 @@ +When tag /something/ has corners /corners/ { + set tagCorners [lmap p $corners {::cameraToProjector $p}] + + set vecBottom [sub [lindex $tagCorners 1] [lindex $tagCorners 0]] + set vecRight [sub [lindex $tagCorners 2] [lindex $tagCorners 1]] + + set offsets {{-0.5 -0.5} {0.5 -0.5} {0.5 0.5} {-0.5 0.5}} + set scales [matmul $offsets [list $vecBottom $vecRight]] + set corners [add $tagCorners $scales] + + set p0 [lindex $corners 0] + set p1 [lindex $corners 1] + set p2 [lindex $corners 2] + set p3 [lindex $corners 3] + Display::drawOnTop Display::fillQuad $p0 $p1 $p2 $p3 black +}
\ No newline at end of file diff --git a/virtual-programs/points-at.folk b/virtual-programs/points-at.folk index 1472b379..a037f98f 100644 --- a/virtual-programs/points-at.folk +++ b/virtual-programs/points-at.folk @@ -19,7 +19,7 @@ When /someone/ wishes /rect/ points /direction/ with length /l/ & /rect/ has reg set whiskerRegion [region scale $region height 0.01px width $scale] set whiskerRegion [region move $whiskerRegion left \ [vec2 distance [region right $whiskerRegion] [region left $region]]px] - set color red + set color gold } elseif {$direction eq "right"} { set whiskerRegion [region scale $region height 0.01px width $scale] set whiskerRegion [region move $whiskerRegion right \ diff --git a/virtual-programs/print.folk b/virtual-programs/print.folk index badd191f..44a7d433 100644 --- a/virtual-programs/print.folk +++ b/virtual-programs/print.folk @@ -163,6 +163,10 @@ proc nextId {} { set id 0 } + while {[file exists "$::env(HOME)/folk-printed-programs/$id.folk"]} { + incr id + } + set fp [open "$::env(HOME)/folk-printed-programs/next-id.txt" w] puts $fp [expr {$id + 1}] close $fp diff --git a/virtual-programs/regions.folk b/virtual-programs/regions.folk index 5de19b98..945fcd20 100644 --- a/virtual-programs/regions.folk +++ b/virtual-programs/regions.folk @@ -1,3 +1,8 @@ When when the distance between /p1/ and /p2/ is /distanceVar/ /body/ with environment /e/ & /p1/ has region /r1/ & /p2/ has region /r2/ { Claim the distance between $p1 and $p2 is [region distance $r1 $r2] } + +When /someone/ wishes region /r/ is /verbed/ /x/ { + Claim $r has region $r + Wish $r is $verbed $x +} diff --git a/virtual-programs/tags-and-calibration.folk b/virtual-programs/tags-and-calibration.folk index afb4aef4..b219b541 100644 --- a/virtual-programs/tags-and-calibration.folk +++ b/virtual-programs/tags-and-calibration.folk @@ -119,7 +119,8 @@ When (non-capturing) tag /tag/ has center /c/ size /size/ { } When (non-capturing) tag /tag/ is a tag { - puts "Added tag $tag" + puts "Added tag $tag" + On unmatch { puts "Removed tag $tag" } set tempPath "$::env(HOME)/folk-printed-programs/$tag.folk.temp" diff --git a/virtual-programs/terminal.folk b/virtual-programs/terminal.folk new file mode 100644 index 00000000..3fb335b5 --- /dev/null +++ b/virtual-programs/terminal.folk @@ -0,0 +1,86 @@ +# Terminal +# +# Spawn terminals with any command (default "bash"): +# Wish $this is a terminal +# Wish $this is a terminal spawning "any command" +# +# Send keyboard events to the terminal: +# Claim $thing has keyboard input +# +# Optionally, draw the terminal on an arbitrary region: +# Claim $thing has terminal region $region +# +# +# Example program: Tie it all together with a simple vim editor... +# +# When $this points up at /target/ & /target/ has program /anything/ { +# Wish $this is a terminal spawning "vim ~/folk-printed-programs/$target.folk" +# When $this has region /r/ { +# Claim $this has terminal region [region move $r right 350px] +# } +# Claim $this has keyboard input +# } +# +# +# Note: Terminals are killed after ::termExpireMs of being unmatched. +# + +source lib/terminal.tcl + +set ::termExpireMs [expr {10*60*1000}] ;# 10 minutes +set ::termInstances [dict create] +set ::termTimeouts [dict create] + +proc ::matchTerminal {id cmd} { + set termKey "$id $cmd" + if {$termKey ni $::termInstances} { + dict set ::termInstances $termKey [Terminal::create 12 43 $cmd] + } + if {$termKey in $::termTimeouts} { + after cancel [dict get $::termTimeouts $termKey] + dict unset ::termTimeouts $termKey + } + dict get $::termInstances $termKey +} + +proc ::unmatchTerminal {id cmd} { + set termKey "$id $cmd" + dict set ::termTimeouts $termKey [ + after $::termExpireMs "::destroyTerminal [list $termKey]" + ] +} + +proc ::destroyTerminal {termKey} { + Terminal::destroy [dict get $::termInstances $termKey] + dict unset ::termInstances $termKey + dict unset ::termTimeouts $termKey +} + +When /anyone/ wishes /thing/ is a terminal { + Wish $thing is a terminal spawning bash +} + +When /thing/ has terminal region /r/ & /r/ has keyboard input { + Claim $thing has keyboard input +} + +When /anyone/ wishes /thing/ is a terminal spawning /cmd/ { + set term [::matchTerminal $thing $cmd] + On unmatch { ::unmatchTerminal $thing $cmd } + + When $::thisProcess has step count /c/ { + set body { + Wish region $region is labelled [Terminal::read $term] + } + When $thing has terminal region /region/ $body + When /nobody/ claims $thing has terminal region /x/ & $thing has region /region/ $body + } + + When /anyone/ claims $thing has keyboard input \ + & /anyone/ claims key /key/ is /direction/ with modifiers /modifiers/ { + if {$direction != "up"} { + set ctrlPressed [expr {"ctrl" in $modifiers}] + Terminal::write $term $key $ctrlPressed + } + } +} diff --git a/virtual-programs/time.folk b/virtual-programs/time.folk new file mode 100644 index 00000000..1dff734d --- /dev/null +++ b/virtual-programs/time.folk @@ -0,0 +1,3 @@ +When $::thisProcess has step count /t/ { + Claim the clock time is [/ [clock milliseconds] 1000.0] +} @@ -14,10 +14,10 @@ proc readFile {filename contentTypeVar} { set response [read $fd]; close $fd; return $response } -proc readPdf {dotCmd contentTypeVar} { +proc getDotAsPdf {dot contentTypeVar} { upvar $contentTypeVar contentType set contentType "application/pdf" - set fd [open |[list dot -Tpdf <<$dotCmd] r] + set fd [open |[list dot -Tpdf <<$dot] r] fconfigure $fd -encoding binary -translation binary set response [read $fd]; close $fd; return $response } @@ -50,6 +50,7 @@ proc handlePage {path contentTypeVar} { </head> <nav> <a href="/new"><button>New program</button></a> + <a href="/programs">Running programs</a> <a href="/timings">Timings</a> <a href="/statementClauseToId.pdf">statementClauseToId graph</a> <a href="/statements.pdf">statements graph</a> @@ -59,6 +60,23 @@ proc handlePage {path contentTypeVar} { </html> } } + "/programs" { + set programs [Statements::findMatches [list /someone/ claims /programName/ has program /program/]] + subst { + <html> + <head> + <link rel="stylesheet" href="/style.css"> + <title>Running programs</title> + </head> + <body> + [join [lmap p $programs { dict with p {subst { + <h2>$programName</h2> + <pre><code>[htmlEscape [lindex $program 1]]</code></pre> + }} }] "\n"] + </body> + </html> + } + } "/timings" { set totalTimes [list] dict for {body totalTime} $Evaluator::totalTimesMap { @@ -68,9 +86,11 @@ proc handlePage {path contentTypeVar} { } set totalTimes [lsort -integer -stride 2 -index 1 $totalTimes] + set totalFrameTime 0 set l [list] foreach {body totalTime} $totalTimes { set runs [dict get $Evaluator::runsMap $body] + set totalFrameTime [expr {$totalFrameTime + $totalTime/$::stepCount}] lappend l [subst { <li> <pre>[htmlEscape $body]</pre> ($runs runs): [dict get $Evaluator::totalTimesMap $body]: $totalTime microseconds total ([expr {$totalTime/$::stepCount}] us per frame), $runs runs ([expr {$totalTime/$runs}] us per run; [expr {$runs/$::stepCount}] runs per frame) @@ -89,7 +109,7 @@ proc handlePage {path contentTypeVar} { <a href="/statementClauseToId.pdf">statementClauseToId graph</a> <a href="/statements.pdf">statements graph</a> </nav> - <h1>Timings</h1> + <h1>Timings (sum per-frame time $totalFrameTime us)</h1> <ul>[join $l "\n"]</ul> </html> } @@ -103,13 +123,10 @@ proc handlePage {path contentTypeVar} { readFile "assets/style.css" contentType } "/statementClauseToId.pdf" { - readPdf [trie dot $Statements::statementClauseToId] contentType + getDotAsPdf [trie dot [Statements::statementClauseToIdTrie]] contentType } "/statements.pdf" { - readPdf [Statements::dot] contentType - } - "/statementPatternToReactions.pdf" { - readPdf [trie dot $Evaluator::statementPatternToReactions] contentType + getDotAsPdf [Statements::dot] contentType } default { subst { |
