diff --git a/examples/boxdraw.ftm b/examples/boxdraw.ftm new file mode 100644 index 0000000..ec88eeb --- /dev/null +++ b/examples/boxdraw.ftm @@ -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 +drawfilledbox 37 5 30 8 +write + +savepng boxdraw.png diff --git a/examples/boxdraw.php b/examples/boxdraw.php new file mode 100644 index 0000000..3dab759 --- /dev/null +++ b/examples/boxdraw.php @@ -0,0 +1,77 @@ + 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]); + + } + +]; diff --git a/examples/boxdraw.png b/examples/boxdraw.png new file mode 100644 index 0000000..086fc99 Binary files /dev/null and b/examples/boxdraw.png differ diff --git a/examples/dd.ftm b/examples/dd.ftm index cb15d10..15d19ab 100644 --- a/examples/dd.ftm +++ b/examples/dd.ftm @@ -1,5 +1,5 @@ -set terminal 80x5 -set typedelay 250 +set terminal 80x10 +set typedelay 100 set fps 30 sub prompt @@ -7,33 +7,93 @@ sub prompt delay 500 endsub +write "dd" " - convert and copy a file" +write u{2500} *80 + +mark %top + + + +write Example 1: " Creating a disk image" prompt "~" type "dd " + +delay 1000 + mark dd-if 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 +delay 2000 + mark dd-of 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 +delay 2000 + mark dd-bs type "bs=16M " annotate @dd-bs "Block\nsize" at dd-bs annotatelength @dd-bs 6 +delay 2000 + mark dd-status type "status=progress " annotate @dd-status "Show progress" at dd-status annotatelength @dd-status 15 +delay 2000 write -prompt "~" - delay 2000 clearannotate +moveto %top +clear to-bottom +delay 500 -delay 500 \ No newline at end of file +write Example 2: " Create a file with empty data" +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 + +delay 2000 + +clearannotate +moveto %top +clear to-bottom +delay 500 + diff --git a/examples/dd.gif b/examples/dd.gif index 01471a9..0a2f97c 100644 Binary files a/examples/dd.gif and b/examples/dd.gif differ diff --git a/examples/dd.mp4 b/examples/dd.mp4 new file mode 100644 index 0000000..92ec116 Binary files /dev/null and b/examples/dd.mp4 differ diff --git a/examples/keys.ftm b/examples/keys.ftm new file mode 100644 index 0000000..d0a27e2 --- /dev/null +++ b/examples/keys.ftm @@ -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 \ No newline at end of file diff --git a/examples/keys.png b/examples/keys.png new file mode 100644 index 0000000..03fa7c8 Binary files /dev/null and b/examples/keys.png differ diff --git a/src/FakeTerminal.php b/src/FakeTerminal.php index 00c9035..2d005e3 100644 --- a/src/FakeTerminal.php +++ b/src/FakeTerminal.php @@ -16,6 +16,8 @@ class FakeTerminal private $subs = []; + private $funcs = []; + private $execMode = null; private $execTarget = null; @@ -32,6 +34,12 @@ class FakeTerminal private $frameDuration = 0; + private $startTime; + + private $nextStat; + + private $lastFrames; + private $output; 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) { $this->output = $output; @@ -57,9 +79,36 @@ class FakeTerminal $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) + { + $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"); + if (!$fp) return; $lc = 0; while (!feof($fp)) { $lc++; @@ -73,17 +122,7 @@ class FakeTerminal fprintf(STDERR, "Error executing %s line %d: %s\n", $filename, $lc, $t->getMessage()); } } - - 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); - } - } - + fclose($fp); } public function evaluate(array $data) @@ -108,6 +147,16 @@ class FakeTerminal $this->setProp($prop, $value); break; + case 'include': + $file = array_shift($args); + $this->includeFile($file); + break; + + case 'load': + $file = array_shift($args); + $this->loadModule($file); + break; + case 'sub': $subname = array_shift($args); $subargs = $args; @@ -202,11 +251,81 @@ class FakeTerminal $this->renderer->updateAnnotation($id, [ 'length' => intval(array_shift($args))]); 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: if (array_key_exists($cmd, $this->subs)) { $this->callSub($cmd, $args); return; } + if (array_key_exists($cmd, $this->funcs)) { + $this->callFunc($cmd, $args); + return; + } throw new \LogicException("No such command ${cmd}"); } @@ -224,8 +343,7 @@ class FakeTerminal $file = sprintf($this->output, $this->frame++); $this->renderer->writePng($file); - //$this->dumper->render($this->term); - //printf("\e[0m\rwrite: \e[1m%s\e[0m", $file); + $this->showStats(); $this->writtenFiles[] = $file; } @@ -255,9 +373,10 @@ class FakeTerminal { //printf("write: %s\n", json_encode($args)); $buf = null; + $lastArg = null; while (count($args)) { $arg = array_shift($args); - if ($arg[0] == "<" && strlen($arg) > 1) { + if ($arg && $arg[0] == "<" && strlen($arg) > 1) { if (substr($arg, -1, 1) == ">") { $this->setupTerm(substr($arg, 1, -1)); continue; @@ -272,9 +391,11 @@ class FakeTerminal $buf = join(" ", $buf); $this->setupTerm(substr($buf, 1, -1)); $buf = null; + } elseif (preg_match('/u\{([0-9a-f]+)\}/', $arg, $match)) { $arg = mb_chr(hexdec($match[1])); + $lastArg = $arg; //printf("write: '%s'\n", $arg); if (!$type) { $this->term->write($arg); @@ -284,9 +405,26 @@ class FakeTerminal $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 { //printf("write: '%s'\n", $arg); + $lastArg = $arg; if (!$type) { $this->term->write($arg); } else { @@ -310,6 +448,7 @@ class FakeTerminal $this->term->setCursor($this->term->getCursorPosition()[0], 0); break; + case 'br': case 'return': $this->term->setCursor($this->term->getCursorPosition()[0] + 1, 0); break; @@ -371,13 +510,18 @@ class FakeTerminal foreach ($data as $k=>$v) { if ($v[0] == '$' && strlen($v) > 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); } } + public function callFunc(string $func, array $args=[]) + { + call_user_func($this->funcs[$func], $this->term, ...$args); + } + public function setProp(string $prop, $value) { switch ($prop) {