summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorOmar Rizwan <omar@omar.website>2023-09-08 22:30:02 +0000
committerOmar Rizwan <omar@omar.website>2023-09-08 22:30:02 +0000
commit4af0fc0e8aa6d89912e0516626fcd7c995b4abda (patch)
tree80df9a2ed12b3235af67dd427dcf31fd3de0773e
parentc: Scope getters so fields w/ same name across structs don't collide (diff)
parentSome peering cleanup (receive before Step); build in FPS counting (diff)
downloadfolk-4af0fc0e8aa6d89912e0516626fcd7c995b4abda.tar.gz
folk-4af0fc0e8aa6d89912e0516626fcd7c995b4abda.zip
Merge branch 'main' into osnr/vulkan-display
-rw-r--r--Makefile4
-rw-r--r--README.md20
-rw-r--r--calibrate.tcl1
-rw-r--r--hosts.tcl4
-rw-r--r--laptop.tcl2
-rw-r--r--lib/environment.tcl13
-rw-r--r--lib/evaluator.tcl39
-rw-r--r--lib/language.tcl2
-rw-r--r--lib/math.tcl14
-rw-r--r--lib/peer.tcl95
-rw-r--r--lib/process.tcl13
-rw-r--r--lib/terminal.tcl193
-rw-r--r--lib/trie.tcl9
-rw-r--r--main.tcl215
-rw-r--r--pi/AprilTags.tcl4
-rw-r--r--pi/Display.tcl127
-rw-r--r--pi/KeyCodes.tcl262
-rw-r--r--pi/Keyboard.tcl38
-rw-r--r--pi/pi.tcl104
-rw-r--r--pi/rotate.tcl14
-rw-r--r--test/stale.tcl6
-rw-r--r--test/survival.tcl25
-rw-r--r--user-programs/haippi7/web-image.folk86
-rw-r--r--vendor/libtmt/README.rst637
-rw-r--r--vendor/libtmt/tmt.c506
-rw-r--r--vendor/libtmt/tmt.h140
-rw-r--r--virtual-programs/apriltags.folk129
-rw-r--r--virtual-programs/camera.folk19
-rw-r--r--virtual-programs/display.folk93
-rw-r--r--virtual-programs/esc-restart.folk8
-rw-r--r--virtual-programs/images.folk150
-rw-r--r--virtual-programs/mask-tags.folk16
-rw-r--r--virtual-programs/points-at.folk2
-rw-r--r--virtual-programs/print.folk4
-rw-r--r--virtual-programs/regions.folk5
-rw-r--r--virtual-programs/tags-and-calibration.folk3
-rw-r--r--virtual-programs/terminal.folk86
-rw-r--r--virtual-programs/time.folk3
-rw-r--r--web.tcl33
39 files changed, 2565 insertions, 559 deletions
diff --git a/Makefile b/Makefile
index 9c5c1528..6f5fce0b 100644
--- a/Makefile
+++ b/Makefile
@@ -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 .
diff --git a/README.md b/README.md
index 65dd409d..6ce006fb 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/hosts.tcl b/hosts.tcl
index 7bc292e6..26f1b6c3 100644
--- a/hosts.tcl
+++ b/hosts.tcl
@@ -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.
}
diff --git a/laptop.tcl b/laptop.tcl
index c58dbb1f..6a640460 100644
--- a/laptop.tcl
+++ b/laptop.tcl
@@ -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;
}
diff --git a/main.tcl b/main.tcl
index d00deb1a..2f95fe25 100644
--- a/main.tcl
+++ b/main.tcl
@@ -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]
-}
diff --git a/pi/pi.tcl b/pi/pi.tcl
index 2a50f549..e3b78b6a 100644
--- a/pi/pi.tcl
+++ b/pi/pi.tcl
@@ -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]
+}
diff --git a/web.tcl b/web.tcl
index 37081ba2..fd6ec2db 100644
--- a/web.tcl
+++ b/web.tcl
@@ -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 {