Much awesome, very Wow!

* Import native PHP code as callable functions (load)
* Include other scripts (include)
* Multitude of fixes
This commit is contained in:
Chris 2021-02-02 18:08:15 +01:00
parent a7278bd50a
commit e23559fd72
9 changed files with 325 additions and 23 deletions

16
examples/boxdraw.ftm Normal file
View File

@ -0,0 +1,16 @@
set terminal 80x25
load boxdraw.php
writefile boxdraw.ftm
drawhline 5 0 70
drawvline 5 0 5
drawbox 22 3 40 14
write <sgr fg:white bg:blue>
drawfilledbox 37 5 30 8
write <sgr>
savepng boxdraw.png

77
examples/boxdraw.php Normal file
View File

@ -0,0 +1,77 @@
<?php return [
/**
* Draw a box frame
*/
'drawbox' => function ($term, $column, $line, $width, $height) {
fprintf(STDERR, "drawbox: {%d,%d}+[%d,%d]\n", $column, $line, $width, $height);
$oldpos = $term->getCursorPosition();
$term->setCursor($line, $column);
$term->write("\u{250c}" . str_repeat("\u{2500}", $width - 2) . "\u{2510}");
$term->setCursor($line + $height, $column);
$term->write("\u{2514}" . str_repeat("\u{2500}", $width - 2) . "\u{2518}");
for ($l = $line + 1; $l < $line + $height; $l++) {
$term->setCursor($l, $column);
$term->write("\u{2502}");
$term->setCursor($l, $column + $width - 1);
$term->write("\u{2502}");
}
$term->setCursor($oldpos[0], $oldpos[1]);
},
/**
* Draw a box and fill it
*/
'drawfilledbox' => function ($term, $column, $line, $width, $height) {
fprintf(STDERR, "drawbox: {%d,%d}+[%d,%d]\n", $column, $line, $width, $height);
$oldpos = $term->getCursorPosition();
$term->setCursor($line, $column);
$term->write("\u{250c}" . str_repeat("\u{2500}", $width - 2) . "\u{2510}");
$term->setCursor($line + $height, $column);
$term->write("\u{2514}" . str_repeat("\u{2500}", $width - 2) . "\u{2518}");
for ($l = $line + 1; $l < $line + $height; $l++) {
$term->setCursor($l, $column);
$term->write("\u{2502}" . str_repeat(" ", $width - 2) . "\u{2502}");
}
$term->setCursor($oldpos[0], $oldpos[1]);
},
/**
* Draw a horizontal line
*/
'drawhline' => function ($term, $line, $fromCol, $toCol) {
$oldpos = $term->getCursorPosition();
$term->setCursor($line, $fromCol);
$term->write(str_repeat("\u{2500}", ($toCol-$fromCol)));
$term->setCursor($oldpos[0], $oldpos[1]);
},
/**
* Draw a vertical line
*/
'drawvline' => function ($term, $column, $fromLine, $toLine) {
$oldpos = $term->getCursorPosition();
for ($l = $fromLine; $l <= $toLine; $l++) {
$term->setCursor($l, $column);
$term->write("\u{2502}");
}
$term->setCursor($oldpos[0], $oldpos[1]);
}
];

BIN
examples/boxdraw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,5 +1,5 @@
set terminal 80x5 set terminal 80x10
set typedelay 250 set typedelay 100
set fps 30 set fps 30
sub prompt sub prompt
@ -7,33 +7,93 @@ sub prompt
delay 500 delay 500
endsub endsub
write <sgr bold> "dd" <sgr> " - convert and copy a file" <return>
write u{2500} *80 <return>
mark %top
write <sgr fg:cyan bold> Example 1: <sgr fg:cyan> " Creating a disk image" <sgr> <return> <return>
prompt "~" prompt "~"
type "dd " type "dd "
delay 1000
mark dd-if mark dd-if
type "if=/dev/sdb " type "if=/dev/sdb "
annotate @dd-if "Input" at dd-if annotate @dd-if "Input file\n(Source)" at dd-if
annotatelength @dd-if 11 annotatelength @dd-if 11
delay 2000
mark dd-of mark dd-of
type "of=image.img " type "of=image.img "
annotate @dd-of "Output" at dd-of annotate @dd-of "Output file\n(Destination)" at dd-of
annotatelength @dd-of 12 annotatelength @dd-of 12
delay 2000
mark dd-bs mark dd-bs
type "bs=16M " type "bs=16M "
annotate @dd-bs "Block\nsize" at dd-bs annotate @dd-bs "Block\nsize" at dd-bs
annotatelength @dd-bs 6 annotatelength @dd-bs 6
delay 2000
mark dd-status mark dd-status
type "status=progress " type "status=progress "
annotate @dd-status "Show progress" at dd-status annotate @dd-status "Show progress" at dd-status
annotatelength @dd-status 15 annotatelength @dd-status 15
delay 2000
write <return> write <return>
prompt "~"
delay 2000 delay 2000
clearannotate clearannotate
moveto %top
clear to-bottom
delay 500
delay 500 write <sgr fg:cyan bold> Example 2: <sgr fg:cyan> " Create a file with empty data" <sgr> <return> <return>
prompt "~"
type "dd "
delay 1000
mark dd-if
type "if=/dev/zero "
annotate @dd-if "Reading from\n/dev/zero will\nreturn '0'\nindefinitely" at dd-if
annotatelength @dd-if 12
delay 2000
mark dd-bs
type "bs=1M "
annotate @dd-bs "Block\nsize" at dd-bs
annotatelength @dd-bs 5
delay 2000
mark dd-count
type "count=100 "
annotate @dd-count "Number of\nblocks to\ncopy:\n100*1M=100MB" at dd-count
annotatelength @dd-count 9
delay 2000
mark dd-of
type "> "
annotate @dd-of "Without of= the output\nwill go to standard output,\nwhich can be redirected" at dd-of
annotatelength @dd-of 1
delay 2000
type "empty.img"
write <return>
delay 2000
clearannotate
moveto %top
clear to-bottom
delay 500

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
examples/dd.mp4 Normal file

Binary file not shown.

5
examples/keys.ftm Normal file
View File

@ -0,0 +1,5 @@
set terminal 80x25
showkeys ctrl alt shift F4 up up down down left right left right B A
savepng keys.png

BIN
examples/keys.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -16,6 +16,8 @@ class FakeTerminal
private $subs = []; private $subs = [];
private $funcs = [];
private $execMode = null; private $execMode = null;
private $execTarget = null; private $execTarget = null;
@ -32,6 +34,12 @@ class FakeTerminal
private $frameDuration = 0; private $frameDuration = 0;
private $startTime;
private $nextStat;
private $lastFrames;
private $output; private $output;
private $bookmarks = []; private $bookmarks = [];
@ -47,6 +55,20 @@ class FakeTerminal
} }
private function showStats()
{
if ($this->nextStat === null) {
$this->nextStat = microtime(true) + 2;
return;
} elseif ($this->nextStat > microtime(true)) {
return;
}
$this->nextStat = microtime(true) + 2;
$fps = ($this->frame - $this->lastFrames) / 2;
$this->lastFrames = $this->frame;
fprintf(STDERR, "rendering: f=%d fps=%.1f t=%.1d\n", $this->frame, $fps, microtime(true) - $this->startTime);
}
public function setOutput(string $output) public function setOutput(string $output)
{ {
$this->output = $output; $this->output = $output;
@ -57,9 +79,36 @@ class FakeTerminal
$this->outputVideo = $outputVideo; $this->outputVideo = $outputVideo;
} }
public function loadModule(string $filename)
{
$funcs = include $filename;
foreach ($funcs as $name=>$func) {
$this->funcs[$name] = $func;
}
}
public function executeFile(string $filename) public function executeFile(string $filename)
{
$this->startTime = microtime(true);
$this->includeFile($filename);
echo "\r\e[K";
if ($this->outputVideo) {
fprintf(STDERR, "Converting %s → %s\n", $this->output, $this->outputVideo);
passthru(sprintf("ffmpeg -v error -r %d -i %s -y %s", $this->fps, $this->output, $this->outputVideo));
fprintf(STDERR, "Cleaning up %d files...\n", count($this->writtenFiles));
foreach ($this->writtenFiles as $fn) {
unlink($fn);
}
}
}
public function includeFile(string $filename)
{ {
$fp = fopen($filename, "r"); $fp = fopen($filename, "r");
if (!$fp) return;
$lc = 0; $lc = 0;
while (!feof($fp)) { while (!feof($fp)) {
$lc++; $lc++;
@ -73,17 +122,7 @@ class FakeTerminal
fprintf(STDERR, "Error executing %s line %d: %s\n", $filename, $lc, $t->getMessage()); fprintf(STDERR, "Error executing %s line %d: %s\n", $filename, $lc, $t->getMessage());
} }
} }
fclose($fp);
echo "\r\e[K";
if ($this->outputVideo) {
fprintf(STDERR, "Converting %s → %s\n", $this->output, $this->outputVideo);
passthru(sprintf("ffmpeg -v error -r %d -i %s -y %s", $this->fps, $this->output, $this->outputVideo));
fprintf(STDERR, "Cleaning up %d files...\n", count($this->writtenFiles));
foreach ($this->writtenFiles as $fn) {
unlink($fn);
}
}
} }
public function evaluate(array $data) public function evaluate(array $data)
@ -108,6 +147,16 @@ class FakeTerminal
$this->setProp($prop, $value); $this->setProp($prop, $value);
break; break;
case 'include':
$file = array_shift($args);
$this->includeFile($file);
break;
case 'load':
$file = array_shift($args);
$this->loadModule($file);
break;
case 'sub': case 'sub':
$subname = array_shift($args); $subname = array_shift($args);
$subargs = $args; $subargs = $args;
@ -202,11 +251,81 @@ class FakeTerminal
$this->renderer->updateAnnotation($id, [ 'length' => intval(array_shift($args))]); $this->renderer->updateAnnotation($id, [ 'length' => intval(array_shift($args))]);
break; break;
case 'showkeys':
$this->renderer->setKeyOverlay($args);
break;
case 'clear':
$what = array_shift($args);
$tw = $this->term->getColumns();
$th = $this->term->getLines();
[$cl,$cc] = $this->term->getCursorPosition();
$sa = $this->term->getAttributes();
switch ($what) {
case 'screen':
for ($l = 0; $l < $th; $l++) {
for ($c = 0; $c < $tw; $c++) {
$this->term->bufferSetRaw($l, $c, [ ' ', $sa ]);
}
}
break;
case 'line':
for ($c = 0; $c < $tw; $c++) {
$this->term->bufferSetRaw($cl, $c, [ ' ', $sa ]);
}
break;
case 'to-eol':
for ($c = $cc; $c < $tw; $c++) {
$this->term->bufferSetRaw($cl, $c, [ ' ', $sa ]);
}
break;
case 'to-bol':
for ($c = 0; $c <= $cc; $c++) {
$this->term->bufferSetRaw($cl, $c, [ ' ', $sa ]);
}
break;
case 'above':
for ($l = 0; $l < $cl; $l++) {
for ($c = 0; $c < $tw; $c++) {
$this->term->bufferSetRaw($l, $c, [ ' ', $sa ]);
}
}
break;
case 'to-top':
for ($l = 0; $l <= $cl; $l++) {
for ($c = 0; $c < $tw; $c++) {
$this->term->bufferSetRaw($l, $c, [ ' ', $sa ]);
}
}
break;
case 'below':
for ($l = $cl + 1; $l < $th; $l++) {
for ($c = 0; $c < $tw; $c++) {
$this->term->bufferSetRaw($l, $c, [ ' ', $sa ]);
}
}
break;
case 'to-bottom':
for ($l = $cl; $l < $th; $l++) {
for ($c = 0; $c < $tw; $c++) {
$this->term->bufferSetRaw($l, $c, [ ' ', $sa ]);
}
}
break;
default:
fprintf(STDERR, "error: Can't clear '%s'\n", $what);
}
break;
default: default:
if (array_key_exists($cmd, $this->subs)) { if (array_key_exists($cmd, $this->subs)) {
$this->callSub($cmd, $args); $this->callSub($cmd, $args);
return; return;
} }
if (array_key_exists($cmd, $this->funcs)) {
$this->callFunc($cmd, $args);
return;
}
throw new \LogicException("No such command ${cmd}"); throw new \LogicException("No such command ${cmd}");
} }
@ -224,8 +343,7 @@ class FakeTerminal
$file = sprintf($this->output, $this->frame++); $file = sprintf($this->output, $this->frame++);
$this->renderer->writePng($file); $this->renderer->writePng($file);
//$this->dumper->render($this->term); $this->showStats();
//printf("\e[0m\rwrite: \e[1m%s\e[0m", $file);
$this->writtenFiles[] = $file; $this->writtenFiles[] = $file;
} }
@ -255,9 +373,10 @@ class FakeTerminal
{ {
//printf("write: %s\n", json_encode($args)); //printf("write: %s\n", json_encode($args));
$buf = null; $buf = null;
$lastArg = null;
while (count($args)) { while (count($args)) {
$arg = array_shift($args); $arg = array_shift($args);
if ($arg[0] == "<" && strlen($arg) > 1) { if ($arg && $arg[0] == "<" && strlen($arg) > 1) {
if (substr($arg, -1, 1) == ">") { if (substr($arg, -1, 1) == ">") {
$this->setupTerm(substr($arg, 1, -1)); $this->setupTerm(substr($arg, 1, -1));
continue; continue;
@ -272,9 +391,11 @@ class FakeTerminal
$buf = join(" ", $buf); $buf = join(" ", $buf);
$this->setupTerm(substr($buf, 1, -1)); $this->setupTerm(substr($buf, 1, -1));
$buf = null; $buf = null;
} elseif (preg_match('/u\{([0-9a-f]+)\}/', $arg, $match)) { } elseif (preg_match('/u\{([0-9a-f]+)\}/', $arg, $match)) {
$arg = mb_chr(hexdec($match[1])); $arg = mb_chr(hexdec($match[1]));
$lastArg = $arg;
//printf("write: '%s'\n", $arg); //printf("write: '%s'\n", $arg);
if (!$type) { if (!$type) {
$this->term->write($arg); $this->term->write($arg);
@ -284,9 +405,26 @@ class FakeTerminal
$this->delayMs($this->typeDelay + rand(-$this->typeJitter,$this->typeJitter)); $this->delayMs($this->typeDelay + rand(-$this->typeJitter,$this->typeJitter));
} }
} }
} elseif (preg_match('/\*([0-9]+)/', $arg, $match)) {
$repeat = intval($match[1]) - 1;
//printf("repeat: '%s' * %d\n", $lastArg, $repeat);
for ($n = 0; $n < $repeat; $n++) {
if (!$type) {
$this->term->write((string)$lastArg);
} else {
for ($c = 0; $c < mb_strlen($lastArg); $c++) {
$this->term->write(mb_substr($lastArg, $c, 1));
$this->delayMs($this->typeDelay + rand(-$this->typeJitter,$this->typeJitter));
}
}
}
} else { } else {
//printf("write: '%s'\n", $arg); //printf("write: '%s'\n", $arg);
$lastArg = $arg;
if (!$type) { if (!$type) {
$this->term->write($arg); $this->term->write($arg);
} else { } else {
@ -310,6 +448,7 @@ class FakeTerminal
$this->term->setCursor($this->term->getCursorPosition()[0], 0); $this->term->setCursor($this->term->getCursorPosition()[0], 0);
break; break;
case 'br':
case 'return': case 'return':
$this->term->setCursor($this->term->getCursorPosition()[0] + 1, 0); $this->term->setCursor($this->term->getCursorPosition()[0] + 1, 0);
break; break;
@ -371,13 +510,18 @@ class FakeTerminal
foreach ($data as $k=>$v) { foreach ($data as $k=>$v) {
if ($v[0] == '$' && strlen($v) > 1) { if ($v[0] == '$' && strlen($v) > 1) {
$i = intval(substr($v, 1)) - 1; $i = intval(substr($v, 1)) - 1;
$data[$k] = array_key_exists($i, $args)?$args[$i]:""; $data[$k] = array_key_exists($i, $args)?$args[$i]:$v;
} }
} }
$this->evaluate($data); $this->evaluate($data);
} }
} }
public function callFunc(string $func, array $args=[])
{
call_user_func($this->funcs[$func], $this->term, ...$args);
}
public function setProp(string $prop, $value) public function setProp(string $prop, $value)
{ {
switch ($prop) { switch ($prop) {