Initial commit
This commit is contained in:
commit
0a61c34d43
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/composer.lock
|
||||
/vendor
|
||||
*~
|
||||
/junk
|
17
composer.json
Normal file
17
composer.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "noccylabs/termbuf",
|
||||
"description": "A virtual terminal buffer",
|
||||
"type": "library",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christopher Vagnetoft",
|
||||
"email": "cvagnetoft@gmail.com"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"NoccyLabs\\TermBuf\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
245
src/Renderer/GdRenderer.php
Normal file
245
src/Renderer/GdRenderer.php
Normal file
@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\TermBuf\Renderer;
|
||||
|
||||
use NoccyLabs\TermBuf\TerminalBuffer;
|
||||
|
||||
class GdRenderer
|
||||
{
|
||||
|
||||
private $font = __DIR__."/../../firacode.ttf";
|
||||
private $boldFont = __DIR__."/../../firacode-bold.ttf";
|
||||
|
||||
private $gd;
|
||||
|
||||
private $palette = [
|
||||
0x000000,
|
||||
0xCC0000,
|
||||
0x00CC00,
|
||||
0xCCCC00,
|
||||
0x0000CC,
|
||||
0xCC00CC,
|
||||
0x00CCCC,
|
||||
0xCCCCCC,
|
||||
];
|
||||
|
||||
private $annotations = [];
|
||||
|
||||
private $annotationFontSize = 2;
|
||||
|
||||
private $keyOverlay = [];
|
||||
|
||||
private $cellWidth;
|
||||
|
||||
private $cellHeight;
|
||||
|
||||
public function render(TerminalBuffer $buffer)
|
||||
{
|
||||
|
||||
$lines = $buffer->getLines();
|
||||
$cols = $buffer->getColumns();
|
||||
|
||||
$cw = 8;
|
||||
$ch = 17;
|
||||
|
||||
$this->cellWidth = $cw;
|
||||
$this->cellHeight = $ch;
|
||||
|
||||
$this->gd = imagecreatetruecolor($cols * $cw, $lines * $ch);
|
||||
|
||||
for ($line = 0; $line < $lines; $line++) {
|
||||
for ($column = 0; $column < $cols; $column++) {
|
||||
$this->renderChar($line*$ch, $column*$cw, $cw, $ch, $buffer->bufferGetRaw($line, $column));
|
||||
}
|
||||
}
|
||||
|
||||
[$cursorLine,$cursorColumn] = $buffer->getCursorPosition();
|
||||
$cursorX = $cursorColumn * $cw;
|
||||
$cursorY = $cursorLine * $ch;
|
||||
|
||||
imagerectangle($this->gd, $cursorX, $cursorY, $cursorX + $cw, $cursorY + $ch - 1, 0xFFFFFF);
|
||||
|
||||
$this->drawAnnotations();
|
||||
$this->drawKeyOverlay();
|
||||
|
||||
}
|
||||
|
||||
private function renderChar(int $y, int $x, int $w, int $h, array $raw)
|
||||
{
|
||||
[ $char, $attrs ] = $raw;
|
||||
|
||||
$bold = false;
|
||||
$underline = false;
|
||||
$color = 0xFFFFFF;
|
||||
$bgcolor = 0x000000;
|
||||
|
||||
//echo "\rattr: " . json_encode($attrs); usleep(100000);
|
||||
foreach ($attrs as $attr) {
|
||||
$attr = intval($attr);
|
||||
if ($attr === 0) {
|
||||
$color = 0xFFFFFF;
|
||||
$bgcolor = 0x000000;
|
||||
$bold = false;
|
||||
$underline = false;
|
||||
} elseif ($attr == 1) {
|
||||
$bold = true;
|
||||
} elseif ($attr == 4) {
|
||||
$underline = true;
|
||||
} elseif ($attr == 22) {
|
||||
$bold = false;
|
||||
} elseif ($attr == 24) {
|
||||
$underline = false;
|
||||
} elseif ($attr >= 30 && $attr <= 39) {
|
||||
$color = $this->palette[$attr - 30];
|
||||
} elseif ($attr >= 40 && $attr <= 49) {
|
||||
$bgcolor = $this->palette[$attr - 40];
|
||||
}
|
||||
}
|
||||
//$attr = join(";", $attrs)??"0";
|
||||
//printf("\e[%d;%dH\e[%sm%s", $line + 1, $column + 1, $attr, $char);
|
||||
imagefilledrectangle($this->gd, $x, $y, $x + $w, $y + $h, $bgcolor);
|
||||
imagettftext($this->gd, 10, 0, $x, $y + 12, $color, $bold?$this->boldFont:$this->font, $char);
|
||||
if ($bold) {
|
||||
//imagettftext($this->gd, 10, 0, $x + 1, $y + 10, $color, $this->font, $char);
|
||||
}
|
||||
if ($underline) {
|
||||
imageline($this->gd, $x, $y + $h - 2, $x + $w, $y + $h - 2, $color);
|
||||
}
|
||||
}
|
||||
|
||||
public function writePng(string $filename)
|
||||
{
|
||||
imagepng($this->gd, $filename);
|
||||
}
|
||||
|
||||
private function drawAnnotations()
|
||||
{
|
||||
$fw = imagefontwidth($this->annotationFontSize);
|
||||
$fh = imagefontheight($this->annotationFontSize);
|
||||
$bg = 0xFFEEDD;
|
||||
$bgs = 0x808080;
|
||||
$bgf = 0xFF8800;
|
||||
|
||||
foreach ($this->annotations as $annotation) {
|
||||
[$line, $column, $text, $width] = $annotation;
|
||||
$x = $this->cellWidth * $column;
|
||||
$y = $this->cellHeight * ($line + 1) + 5;
|
||||
$lines = explode("\\n", $text);
|
||||
$lc = count($lines);
|
||||
$lw = max(array_map("strlen", $lines));
|
||||
$w = 4 + ($fw * $lw);
|
||||
$h = 2 + ($fh * $lc);
|
||||
$aw = floor($this->cellWidth / 2);
|
||||
for ($n = 0; $n < $aw; $n++) {
|
||||
imageline($this->gd, $x + $n, $y - $n, $x + (2 * $aw) - $n, $y - $n, $bg);
|
||||
}
|
||||
imagerectangle($this->gd, $x+1, $y+1, $x + $w + 1, $y + $h + 1, $bgs);
|
||||
imagefilledrectangle($this->gd, $x, $y, $x + $w, $y + $h, $bg);
|
||||
$lp = 0;
|
||||
foreach ($lines as $line) {
|
||||
imagestring($this->gd, $this->annotationFontSize, $x + 2, $y + 1 + ($lp++ * $fh), $line, 0x0);
|
||||
}
|
||||
|
||||
if ($width > 0) {
|
||||
imageline($this->gd, $x - 1, $y - 4, $x - 1, $y - 7, $bgf);
|
||||
imageline($this->gd, $x + ($width * $this->cellWidth) + 1, $y - 4, $x + ($width * $this->cellWidth) + 1, $y - 7, $bgf);
|
||||
imageline($this->gd, $x - 1, $y - 4, $x + ($width * $this->cellWidth) + 1, $y - 4, $bgf);
|
||||
imageline($this->gd, $x - 1, $y - 5, $x + ($width * $this->cellWidth) + 1, $y - 5, $bgf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addAnnotation(string $id, int $line, int $column, string $text)
|
||||
{
|
||||
$this->annotations[$id] = [ $line, $column, $text, 0 ];
|
||||
}
|
||||
|
||||
public function updateAnnotation(string $id, array $props)
|
||||
{
|
||||
if (!array_key_exists($id, $this->annotations)) {
|
||||
return;
|
||||
}
|
||||
foreach ($props as $k=>$v) {
|
||||
switch ($k) {
|
||||
case 'length':
|
||||
$this->annotations[$id][3] = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeAnnotation(string $id)
|
||||
{
|
||||
unset ($this->annotations[$id]);
|
||||
}
|
||||
|
||||
public function clearAnnotations()
|
||||
{
|
||||
$this->annotations = [];
|
||||
}
|
||||
|
||||
private function drawKeyOverlay()
|
||||
{
|
||||
|
||||
$x = 0;
|
||||
$d = 5;
|
||||
$y = imagesy($this->gd) - 32;
|
||||
$h = 24;
|
||||
$w = 32;
|
||||
$ww = 48;
|
||||
|
||||
foreach ($this->keyOverlay as $key) {
|
||||
$x += $d;
|
||||
$char = $this->translateKeyToGlyph($key);
|
||||
if (mb_strlen($char) > 2) {
|
||||
$this->drawRoundedRectangle($x, $y, $ww, $h, 3, 0xCCCCCC);
|
||||
imagettftext($this->gd, 8, 0, $x + 5, $y + 16, 0x000000, $this->boldFont, $char);
|
||||
$x += $ww;
|
||||
} else {
|
||||
$this->drawRoundedRectangle($x, $y, $w, $h, 3, 0xCCCCCC);
|
||||
imagettftext($this->gd, 10, 0, $x + ((mb_strlen($char)==1)?12:8), $y + 16, 0x000000, $this->boldFont, $char);
|
||||
$x += $w;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function setKeyOverlay(array $keys)
|
||||
{
|
||||
$this->keyOverlay = $keys;
|
||||
}
|
||||
|
||||
private function drawRoundedRectangle(int $left, int $top, int $width, int $height, int $radius, $color)
|
||||
{
|
||||
|
||||
$right = $left + $width;
|
||||
$bottom = $top + $height;
|
||||
imagefilledrectangle($this->gd, $left + $radius, $top, $right - $radius, $bottom, $color);
|
||||
imagefilledrectangle($this->gd, $left, $top + $radius, $right, $bottom - $radius, $color);
|
||||
|
||||
imagefilledellipse($this->gd, $left + $radius, $top + $radius, $radius * 2, $radius * 2, $color);
|
||||
imagefilledellipse($this->gd, $left + $radius, $bottom - $radius, $radius * 2, $radius * 2, $color);
|
||||
imagefilledellipse($this->gd, $right - $radius, $top + $radius, $radius * 2, $radius * 2, $color);
|
||||
imagefilledellipse($this->gd, $right - $radius, $bottom - $radius, $radius * 2, $radius * 2, $color);
|
||||
|
||||
}
|
||||
|
||||
private function translateKeyToGlyph(string $key)
|
||||
{
|
||||
switch ($key) {
|
||||
case "up":
|
||||
return "↑";
|
||||
case "down":
|
||||
return "↓";
|
||||
case "left":
|
||||
return "←";
|
||||
case "right":
|
||||
return "→";
|
||||
case "backspace":
|
||||
case "bs":
|
||||
return mb_chr(0x232b);
|
||||
default:
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
}
|
39
src/Renderer/TerminalRenderer.php
Normal file
39
src/Renderer/TerminalRenderer.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\TermBuf\Renderer;
|
||||
|
||||
use NoccyLabs\TermBuf\TerminalBuffer;
|
||||
|
||||
class TerminalRenderer
|
||||
{
|
||||
|
||||
|
||||
public function render(TerminalBuffer $buffer)
|
||||
{
|
||||
|
||||
$lines = $buffer->getLines();
|
||||
$cols = $buffer->getColumns();
|
||||
|
||||
$this->clearScreen();
|
||||
for ($line = 0; $line < $lines; $line++) {
|
||||
for ($column = 0; $column < $cols; $column++) {
|
||||
$this->renderChar($line, $column, $buffer->bufferGetRaw($line, $column));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function clearScreen()
|
||||
{
|
||||
echo "\e[H\e[2J";
|
||||
}
|
||||
|
||||
private function renderChar(int $line, int $column, array $raw)
|
||||
{
|
||||
[ $char, $attrs ] = $raw;
|
||||
if ($char === ' ') return;
|
||||
$attr = join(";", $attrs)??"0";
|
||||
printf("\e[%d;%dH\e[%sm%s", $line + 1, $column + 1, $attr, $char);
|
||||
}
|
||||
|
||||
}
|
187
src/TerminalBuffer.php
Normal file
187
src/TerminalBuffer.php
Normal file
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\TermBuf;
|
||||
|
||||
|
||||
class TerminalBuffer
|
||||
{
|
||||
|
||||
private $buffer = [];
|
||||
|
||||
private $cursorLine = 0;
|
||||
|
||||
private $cursorColumn = 0;
|
||||
|
||||
private $columns = 0;
|
||||
|
||||
private $lines = 0;
|
||||
|
||||
private $textAttributes = [];
|
||||
|
||||
private $wrapAtEnd = true;
|
||||
|
||||
private $scrollAtEnd = true;
|
||||
|
||||
public function __construct(int $columns, int $lines)
|
||||
{
|
||||
$this->columns = $columns;
|
||||
$this->lines = $lines;
|
||||
}
|
||||
|
||||
public function getLines(): int
|
||||
{
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
public function getColumns(): int
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
protected function translateBufferOffs(int $line, int $column): int
|
||||
{
|
||||
return ($line * $this->columns) + $column;
|
||||
}
|
||||
|
||||
public function bufferGetRaw(int $line, int $column): array
|
||||
{
|
||||
$offs = 'c'.$this->translateBufferOffs($line, $column);
|
||||
if (array_key_exists($offs, $this->buffer)) {
|
||||
return $this->buffer[$offs];
|
||||
}
|
||||
return [ ' ', [ 0 ] ];
|
||||
}
|
||||
|
||||
public function bufferSetRaw(int $line, int $column, array $raw)
|
||||
{
|
||||
$offs = 'c'.$this->translateBufferOffs($line, $column);
|
||||
$this->buffer[$offs] = $raw;
|
||||
}
|
||||
|
||||
public function bufferSetChar(int $line, int $column, string $char, array $attr=null)
|
||||
{
|
||||
$this->bufferSetRaw($line, $column, [
|
||||
$char,
|
||||
$attr??$this->textAttributes
|
||||
]);
|
||||
}
|
||||
|
||||
public function setAttributes(array $attr)
|
||||
{
|
||||
$this->textAttributes = $attr;
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->textAttributes;
|
||||
}
|
||||
|
||||
public function setCursor(int $line, int $column)
|
||||
{
|
||||
$this->cursorColumn = $column;
|
||||
if ($line >= $this->lines) {
|
||||
$scrollBy = $line - $this->lines + 1;
|
||||
$this->scrollBuffer(-$scrollBy);
|
||||
$line = $this->lines - 1;
|
||||
}
|
||||
$this->cursorLine = $line;
|
||||
}
|
||||
|
||||
public function getCursorPosition(): array
|
||||
{
|
||||
return [ $this->cursorLine, $this->cursorColumn ];
|
||||
}
|
||||
|
||||
public function moveCursorColumns(int $columns)
|
||||
{
|
||||
$lines = 0;
|
||||
while (($this->cursorColumn + $columns) > $this->columns) {
|
||||
$lines++;
|
||||
$columns -= $this->columns;
|
||||
}
|
||||
// TODO: This will always wrap, make it respect the wrap and scroll attributes
|
||||
$this->setCursor($this->cursorLine + $lines, $this->cursorColumn + $columns);
|
||||
}
|
||||
|
||||
public function moveCursorRows(int $rows)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function write(string $text)
|
||||
{
|
||||
//printf("write %d bytes\n", mb_strlen($text));
|
||||
for ($ch = 0; $ch < mb_strlen($text); $ch++) {
|
||||
$c = mb_substr($text, $ch, 1);
|
||||
if (mb_ord($c) >= 32) {
|
||||
//printf("\r[%s] %d,%d\n", $c, $this->cursorLine, $this->cursorColumn); usleep(250000);
|
||||
$this->bufferSetChar( $this->cursorLine, $this->cursorColumn, $c);
|
||||
$this->moveCursorColumns(1);
|
||||
} elseif ($c == chr(10)) {
|
||||
//printf("\r[NL] %d,%d\n", $c, $this->cursorLine, $this->cursorColumn); usleep(250000);
|
||||
$line = $this->cursorLine + 1;
|
||||
$this->setCursor($line, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function writeAnsi(string $text)
|
||||
{
|
||||
for ($ch = 0; $ch < mb_strlen($text); $ch++) {
|
||||
$c = mb_substr($text, $ch, 1);
|
||||
$cc = mb_substr($text, $ch, 2);
|
||||
if ($cc == "\e[") {
|
||||
$sb = null; $ch += 2;
|
||||
while ($ch < mb_strlen($text)) {
|
||||
$cp = mb_substr($text, $ch++, 1);
|
||||
if (ctype_alpha($cp)) { $ch--; break; }
|
||||
$sb .= $cp;
|
||||
}
|
||||
$attr = explode(";", $sb);
|
||||
$this->setAttributes($attr);
|
||||
} else {
|
||||
$this->write($c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll lines in the buffer, from $firstLine to $lastLine, moving every line by
|
||||
* $scroll lines while adding empty lines at top or bottom.
|
||||
*/
|
||||
public function scrollLines(int $firstLine, int $lastLine, int $scroll = -1)
|
||||
{
|
||||
if ($scroll < 0) {
|
||||
for ($n = 0; $n < abs($scroll); $n++) {
|
||||
for ($line = $firstLine; $line < $lastLine; $line++) {
|
||||
for ($col = 0; $col < $this->columns; $col++) {
|
||||
$this->bufferSetRaw($line, $col, $this->bufferGetRaw($line + 1, $col));
|
||||
}
|
||||
}
|
||||
for ($col = 0; $col < $this->columns; $col++) {
|
||||
$this->bufferSetRaw($lastLine, $col, [' ', []]);
|
||||
}
|
||||
}
|
||||
} elseif ($scroll > 0) {
|
||||
$first = $firstLine + $scroll; // add scroll to top
|
||||
$last = $lastLine - $scroll; // remove from bottom
|
||||
for ($line = $last - 1; $line >= $first; $line--) {
|
||||
for ($col = 0; $col < $this->columns; $col++) {
|
||||
$this->bufferSetRaw($line, $col, $this->bufferGetRaw($line - 1, $col));
|
||||
}
|
||||
}
|
||||
for ($line = $firstLine; $line <= $firstLine + $scroll; $line++) {
|
||||
for ($col = 0; $col < $this->columns; $col++) {
|
||||
$this->bufferSetRaw($line, $col, [' ', []]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function scrollBuffer(int $scroll)
|
||||
{
|
||||
$this->scrollLines(0, $this->lines - 1, $scroll);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user