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); } }