Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/vendor/
|
||||
20
composer.json
Normal file
20
composer.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "noccylabs/react-term-io",
|
||||
"description": "Interactive terminal I/O library for ReactPHP",
|
||||
"type": "library",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"NoccyLabs\\React\\TermIo\\": "src/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christopher Vagnetoft",
|
||||
"email": "labs@noccy.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"clue/term-react": "^1.4"
|
||||
}
|
||||
}
|
||||
289
composer.lock
generated
Normal file
289
composer.lock
generated
Normal file
@@ -0,0 +1,289 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9319b87c1012a4273dfc71b03c8b01a7",
|
||||
"packages": [
|
||||
{
|
||||
"name": "clue/term-react",
|
||||
"version": "v1.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/clue/reactphp-term.git",
|
||||
"reference": "00f297dc597eaee2ebf98af8f27cca5d21d60fa3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/clue/reactphp-term/zipball/00f297dc597eaee2ebf98af8f27cca5d21d60fa3",
|
||||
"reference": "00f297dc597eaee2ebf98af8f27cca5d21d60fa3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3",
|
||||
"react/stream": "^1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
|
||||
"react/event-loop": "^1.2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Clue\\React\\Term\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering"
|
||||
}
|
||||
],
|
||||
"description": "Streaming terminal emulator, built on top of ReactPHP.",
|
||||
"homepage": "https://github.com/clue/reactphp-term",
|
||||
"keywords": [
|
||||
"C0",
|
||||
"CSI",
|
||||
"ansi",
|
||||
"apc",
|
||||
"ascii",
|
||||
"c1",
|
||||
"control codes",
|
||||
"dps",
|
||||
"osc",
|
||||
"pm",
|
||||
"reactphp",
|
||||
"streaming",
|
||||
"terminal",
|
||||
"vt100",
|
||||
"xterm"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/clue/reactphp-term/issues",
|
||||
"source": "https://github.com/clue/reactphp-term/tree/v1.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://clue.engineering/support",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/clue",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2024-01-30T10:22:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "evenement/evenement",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/igorw/evenement.git",
|
||||
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc",
|
||||
"reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9 || ^6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Evenement\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Igor Wiedler",
|
||||
"email": "igor@wiedler.ch"
|
||||
}
|
||||
],
|
||||
"description": "Événement is a very simple event dispatching library for PHP",
|
||||
"keywords": [
|
||||
"event-dispatcher",
|
||||
"event-emitter"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/igorw/evenement/issues",
|
||||
"source": "https://github.com/igorw/evenement/tree/v3.0.2"
|
||||
},
|
||||
"time": "2023-08-08T05:53:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "react/event-loop",
|
||||
"version": "v1.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/reactphp/event-loop.git",
|
||||
"reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
|
||||
"reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pcntl": "For signal handling support when using the StreamSelectLoop"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\EventLoop\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering",
|
||||
"homepage": "https://clue.engineering/"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"email": "reactphp@ceesjankiewiet.nl",
|
||||
"homepage": "https://wyrihaximus.net/"
|
||||
},
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"email": "jsorgalla@gmail.com",
|
||||
"homepage": "https://sorgalla.com/"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"email": "cboden@gmail.com",
|
||||
"homepage": "https://cboden.dev/"
|
||||
}
|
||||
],
|
||||
"description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.",
|
||||
"keywords": [
|
||||
"asynchronous",
|
||||
"event-loop"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/reactphp/event-loop/issues",
|
||||
"source": "https://github.com/reactphp/event-loop/tree/v1.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://opencollective.com/reactphp",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2023-11-13T13:48:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "react/stream",
|
||||
"version": "v1.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/reactphp/stream.git",
|
||||
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d",
|
||||
"reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
|
||||
"php": ">=5.3.8",
|
||||
"react/event-loop": "^1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"clue/stream-filter": "~1.2",
|
||||
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"React\\Stream\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Lück",
|
||||
"email": "christian@clue.engineering",
|
||||
"homepage": "https://clue.engineering/"
|
||||
},
|
||||
{
|
||||
"name": "Cees-Jan Kiewiet",
|
||||
"email": "reactphp@ceesjankiewiet.nl",
|
||||
"homepage": "https://wyrihaximus.net/"
|
||||
},
|
||||
{
|
||||
"name": "Jan Sorgalla",
|
||||
"email": "jsorgalla@gmail.com",
|
||||
"homepage": "https://sorgalla.com/"
|
||||
},
|
||||
{
|
||||
"name": "Chris Boden",
|
||||
"email": "cboden@gmail.com",
|
||||
"homepage": "https://cboden.dev/"
|
||||
}
|
||||
],
|
||||
"description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP",
|
||||
"keywords": [
|
||||
"event-driven",
|
||||
"io",
|
||||
"non-blocking",
|
||||
"pipe",
|
||||
"reactphp",
|
||||
"readable",
|
||||
"stream",
|
||||
"writable"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/reactphp/stream/issues",
|
||||
"source": "https://github.com/reactphp/stream/tree/v1.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://opencollective.com/reactphp",
|
||||
"type": "open_collective"
|
||||
}
|
||||
],
|
||||
"time": "2024-06-11T12:45:25+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
9
src/Key.php
Normal file
9
src/Key.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\TermIo;
|
||||
|
||||
class Key
|
||||
{
|
||||
const ESCAPE = 'esc';
|
||||
const ENTER = 'c-J';
|
||||
}
|
||||
14
src/KeyEvent.php
Normal file
14
src/KeyEvent.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\TermIo;
|
||||
|
||||
class KeyEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string|Key $key,
|
||||
public readonly bool $ctrl,
|
||||
public readonly bool $alt,
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
123
src/KeyMapper.php
Normal file
123
src/KeyMapper.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\TermIo;
|
||||
|
||||
use Evenement\EventEmitterInterface;
|
||||
use Evenement\EventEmitterTrait;
|
||||
|
||||
class KeyMapper implements EventEmitterInterface
|
||||
{
|
||||
const ON_PRINTABLE = "printable";
|
||||
const ON_ACTION = "action";
|
||||
const ON_KEY = "key";
|
||||
|
||||
use EventEmitterTrait;
|
||||
/** @var array<string,string> Key binds and actions */
|
||||
private array $binds = [];
|
||||
/** @var array<string,array> Defined chords indexed by the base key*/
|
||||
private array $chords = [];
|
||||
/** @var bool If we are currently parsing a chord */
|
||||
private bool $inChord = false;
|
||||
/** @var array<string> The currently entered chord */
|
||||
private array $chord = [];
|
||||
/** @var int When the chord was last updated, if delta too high flush before push */
|
||||
private int $putChord = 0;
|
||||
|
||||
public function __construct(Terminal $terminal)
|
||||
{
|
||||
$terminal->on('key', $this->onKey(...));
|
||||
}
|
||||
|
||||
public function bind(string $keyOrChord, string $action, bool $force = false): void
|
||||
{
|
||||
if (isset($this->binds[$keyOrChord]) && !$force) {
|
||||
throw new \RuntimeException("Key or chord already bound: {$keyOrChord}");
|
||||
}
|
||||
$this->binds[$keyOrChord] = $action;
|
||||
$this->updateChords();
|
||||
}
|
||||
|
||||
public function bindMultiple(array $keys, bool $force = false): void
|
||||
{
|
||||
foreach ($keys as $key => $action) {
|
||||
$this->bind($key, $action, $force);
|
||||
}
|
||||
}
|
||||
|
||||
public function unbind(string $keyOrChord): void
|
||||
{
|
||||
unset($this->binds[$keyOrChord]);
|
||||
$this->updateChords();
|
||||
}
|
||||
|
||||
public function findKeyForAction(string $action): ?string
|
||||
{
|
||||
return array_find_key($this->binds, fn($v) => $v === $action);
|
||||
}
|
||||
|
||||
private function updateChords(): void
|
||||
{
|
||||
$this->chords = [];
|
||||
$chords = array_filter(array_keys($this->binds), fn($k) => str_contains($k, " "));
|
||||
foreach ($chords as $chord) {
|
||||
$chordSplit = explode(" ", $chord);
|
||||
$chordBase = reset($chordSplit);
|
||||
if (!isset($this->chords[$chordBase])) {
|
||||
$this->chords[$chordBase] = [];
|
||||
}
|
||||
$this->chords[$chordBase][] = $chordSplit;
|
||||
}
|
||||
}
|
||||
|
||||
public function onKey(string $key, bool $printable = false): void
|
||||
{
|
||||
if (!$this->inChord) {
|
||||
if ($this->isChord($key)) {
|
||||
$this->inChord = true;
|
||||
$this->chord = [ $key ];
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$this->chord[] = $key;
|
||||
$this->matchChord();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($this->binds[$key])) {
|
||||
$this->emit(self::ON_ACTION, [ $this->binds[$key] ]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($printable) {
|
||||
$this->emit(self::ON_PRINTABLE, [ $key ]);
|
||||
} else {
|
||||
$this->emit(self::ON_KEY, [ $key ]);
|
||||
}
|
||||
}
|
||||
|
||||
private function matchChord(): void
|
||||
{
|
||||
$chord = array_values($this->chord);
|
||||
$chords = $this->chords[$chord] ?? null;
|
||||
|
||||
if ($chords === null) return;
|
||||
|
||||
// FIXME
|
||||
|
||||
$this->chord = [];
|
||||
$this->inChord = false;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the key is defined as a chord key
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function isChord(string $key): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
64
src/Style.php
Normal file
64
src/Style.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\TermIo;
|
||||
|
||||
class Style
|
||||
{
|
||||
public function __construct(
|
||||
public readonly ?int $fg = null,
|
||||
public readonly ?int $bg = null,
|
||||
public readonly ?bool $bold = null,
|
||||
public readonly ?bool $dim = null,
|
||||
public readonly ?bool $italic = null,
|
||||
public readonly ?bool $underline = null,
|
||||
public readonly ?bool $reverse = null,
|
||||
public readonly bool $reset = false,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(string $content): string
|
||||
{
|
||||
return $this->toSgr() . $content;
|
||||
}
|
||||
|
||||
public function toSgr(): string
|
||||
{
|
||||
$sgr = [];
|
||||
|
||||
if ($this->reset == true) $sgr[] = "0";
|
||||
if ($this->bold == true) $sgr[] = "1";
|
||||
if ($this->bold == false) $sgr[] = "22";
|
||||
if ($this->dim == true) $sgr[] = "2";
|
||||
if ($this->dim == false) $sgr[] = "22";
|
||||
if ($this->italic == true) $sgr[] = "3";
|
||||
if ($this->italic == false) $sgr[] = "23";
|
||||
if ($this->underline == true) $sgr[] = "4";
|
||||
if ($this->underline == false) $sgr[] = "24";
|
||||
if ($this->reverse == true) $sgr[] = "7";
|
||||
if ($this->reverse == false) $sgr[] = "27";
|
||||
|
||||
if ($this->fg !== null) {
|
||||
if ($this->fg > 7) {
|
||||
$sgr[] = 90 + $this->fg - 8;
|
||||
} else {
|
||||
$sgr[] = 30 + $this->fg;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->bg !== null) {
|
||||
if ($this->bg > 7) {
|
||||
$sgr[] = 100 + $this->bg - 8;
|
||||
} else {
|
||||
$sgr[] = 40 + $this->bg;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($sgr) == 0) {
|
||||
return "";
|
||||
}
|
||||
return "\e[".join(";",$sgr)."m";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
224
src/Terminal.php
Normal file
224
src/Terminal.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\TermIo;
|
||||
|
||||
use Clue\React\Term\ControlCodeParser;
|
||||
use Evenement\EventEmitterTrait;
|
||||
use Evenement\EventEmitterInterface;
|
||||
use React\EventLoop\Loop;
|
||||
use React\Stream\ReadableResourceStream;
|
||||
use React\Stream\ReadableStreamInterface;
|
||||
use React\Stream\WritableResourceStream;
|
||||
use React\Stream\WritableStreamInterface;
|
||||
|
||||
class Terminal implements EventEmitterInterface
|
||||
{
|
||||
use EventEmitterTrait;
|
||||
/** @var string ON_KEY Emitted when a key is pressed */
|
||||
const ON_KEY = 'key';
|
||||
/** @var string ON_RESIZE Emitted when the terminal is resized */
|
||||
const ON_RESIZE = 'resize';
|
||||
|
||||
private ?ReadableStreamInterface $stdin;
|
||||
|
||||
private ?WritableStreamInterface $stdout;
|
||||
|
||||
private ?ControlCodeParser $parser;
|
||||
|
||||
private ?string $savedStty = null;
|
||||
|
||||
private bool $altBuffer = false;
|
||||
|
||||
private bool $buffering = false;
|
||||
|
||||
private array $buffer = [];
|
||||
|
||||
private int $lines = 0;
|
||||
|
||||
private int $cols = 0;
|
||||
|
||||
public function __construct(
|
||||
?ReadableStreamInterface $stdin = null,
|
||||
?WritableStreamInterface $stdout = null,
|
||||
)
|
||||
{
|
||||
$stdin ??= new ReadableResourceStream(STDIN);
|
||||
if ($stdin instanceof ReadableResourceStream) {
|
||||
$this->savedStty = exec("stty -g");
|
||||
exec("stty raw -icanon time 0 -echo");
|
||||
}
|
||||
$this->stdin = $stdin;
|
||||
|
||||
$this->parser = new ControlCodeParser($this->stdin);
|
||||
$this->parser->on("data", function ($data) {
|
||||
$this->emit(self::ON_KEY, [ $data, true ]);
|
||||
// echo "data: ".addcslashes($data,"\e")."\n";
|
||||
});
|
||||
$this->parser->on("csi", function ($data) {
|
||||
$m = match ($data) {
|
||||
"\e[A" => 'up',
|
||||
"\e[B" => 'down',
|
||||
"\e[D" => 'left',
|
||||
"\e[C" => 'right',
|
||||
"\e[H" => 'home',
|
||||
"\e[F" => 'end',
|
||||
"\e[5~" => 'pgup',
|
||||
"\e[6~" => 'pgdn',
|
||||
default => null,
|
||||
};
|
||||
if ($m !== null) {
|
||||
$this->emit(self::ON_KEY, [ $m ]);
|
||||
} else {
|
||||
echo "csi: ".addcslashes($data,"\e")."\n";
|
||||
}
|
||||
});
|
||||
$this->parser->on("osc", function ($data) {
|
||||
echo "osc: ".addcslashes($data,"\e")."\n";
|
||||
});
|
||||
$this->parser->on("apc", function ($data) {
|
||||
echo "apc: ".addcslashes($data,"\e")."\n";
|
||||
});
|
||||
$this->parser->on("dps", function ($data) {
|
||||
echo "dps: ".addcslashes($data,"\e")."\n";
|
||||
});
|
||||
$this->parser->on("pm", function ($data) {
|
||||
echo "pm: ".addcslashes($data,"\e")."\n";
|
||||
});
|
||||
$this->parser->on("c0", function ($data) {
|
||||
if ($data == "\x7F") {
|
||||
$this->emit(self::ON_KEY, [ 'bs' ]);
|
||||
} elseif ($data == "\n") {
|
||||
$this->emit(self::ON_KEY, [ 'enter' ]);
|
||||
} else {
|
||||
$this->emit(self::ON_KEY, [ 'c-'.chr(ord($data) + 96) ]);
|
||||
}
|
||||
//echo "c0: ".addcslashes($data,"\e")." (".dechex(ord($data)).")\n";
|
||||
});
|
||||
$this->parser->on("c1", function ($data) {
|
||||
if ($data == "\e\e") {
|
||||
$this->emit(self::ON_KEY, [ 'esc' ]);
|
||||
} elseif (strlen($data) == 2 && $data[0] == "\e") {
|
||||
$this->emit(self::ON_KEY, [ 'm-' . $data[1] ]);
|
||||
} else {
|
||||
echo "c1: ".addcslashes($data,"\e")."\n";
|
||||
}
|
||||
});
|
||||
|
||||
$stdout ??= new WritableResourceStream(STDOUT);
|
||||
if ($stdout instanceof WritableResourceStream) {
|
||||
echo "\e[?7l";
|
||||
}
|
||||
$this->stdout = $stdout;
|
||||
|
||||
pcntl_signal(SIGWINCH, function () {
|
||||
$this->measure();
|
||||
Loop::futureTick(fn() => $this->emit("resize", [ $this->lines, $this->cols ]));
|
||||
});
|
||||
$this->measure();
|
||||
|
||||
}
|
||||
|
||||
private function measure(): void
|
||||
{
|
||||
$this->lines = intval(exec("tput lines"));
|
||||
$this->cols = intval(exec("tput cols"));
|
||||
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
$this->stdin = null;
|
||||
|
||||
if ($this->savedStty) {
|
||||
exec("stty {$this->savedStty}");
|
||||
}
|
||||
|
||||
if ($this->stdout instanceof WritableResourceStream) {
|
||||
echo "\e[?7h";
|
||||
echo "\e[?25h";
|
||||
if ($this->altBuffer) {
|
||||
echo "\e[?1049l";
|
||||
}
|
||||
}
|
||||
|
||||
$this->stdout = null;
|
||||
|
||||
if ($this->parser) {
|
||||
$this->parser->removeAllListeners();
|
||||
$this->parser = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function setAlternateBuffer(bool $buffer): void
|
||||
{
|
||||
$this->altBuffer = $buffer;
|
||||
echo "\e[?1049" . ($buffer ? "h" : "l");
|
||||
}
|
||||
|
||||
public function enableBuffering(bool $buffering = true): void
|
||||
{
|
||||
$this->buffering = $buffering;
|
||||
if ($buffering == false) {
|
||||
$this->flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function setCursorVisible(bool $visible): void
|
||||
{
|
||||
echo "\e[?25" . ($visible?"h":"l");
|
||||
}
|
||||
|
||||
public function writeAt(int $line, int $column, string $data, ?Style $style = null): void
|
||||
{
|
||||
$l = $line+1;
|
||||
$c = $column+1;
|
||||
$data = "\e[{$l};{$c}H" . ($style ? $style($data) : $data);
|
||||
$this->write($data);
|
||||
}
|
||||
|
||||
public function write(string $data, ?Style $style = null): void
|
||||
{
|
||||
if ($style) {
|
||||
$data = $style($data);
|
||||
}
|
||||
if ($this->buffering) {
|
||||
$this->buffer[] = $data;
|
||||
} else {
|
||||
if ($this->stdout) {
|
||||
$this->stdout->write($data);
|
||||
} else {
|
||||
// echo $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function flush(): void
|
||||
{
|
||||
$this->stdout->write(join("",$this->buffer));
|
||||
$this->buffer = [];
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of the terminal window
|
||||
*
|
||||
* @return object{lines:int,cols:int}
|
||||
*/
|
||||
public function getSize(): object
|
||||
{
|
||||
return (object)[
|
||||
'lines' => $this->lines,
|
||||
'cols' => $this->cols,
|
||||
];
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->write("\e[0m\e[H\e[2J");
|
||||
}
|
||||
|
||||
}
|
||||
35
src/Theme.php
Normal file
35
src/Theme.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\React\TermIo;
|
||||
|
||||
class Theme
|
||||
{
|
||||
private array $defaults = [];
|
||||
|
||||
private array $styles = [];
|
||||
|
||||
public function define(string $element, Style $defaultStyle): void
|
||||
{
|
||||
$this->defaults[$element] = $defaultStyle;
|
||||
$this->styles[$element] = $defaultStyle;
|
||||
}
|
||||
|
||||
public function set(string $element, ?Style $style): void
|
||||
{
|
||||
if (!isset($this->defaults[$element])) {
|
||||
throw new \InvalidArgumentException("The theme element {$element} is not defined");
|
||||
}
|
||||
if ($style !== null) {
|
||||
$this->styles[$element] = $style;
|
||||
} else {
|
||||
$this->styles[$element] = $this->defaults[$element];
|
||||
}
|
||||
}
|
||||
|
||||
public function get(String $element): ?Style
|
||||
{
|
||||
return $this->styles[$element] ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user