diff --git a/composer.json b/composer.json index 0869497..afb132a 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "require-dev": { "phpunit/phpunit": "^7.1", "squizlabs/php_codesniffer": "^3.2", - "phpstan/phpstan": "^0.9.2" + "phpstan/phpstan": "^0.12" }, "autoload" : { "psr-4" : { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..9d52fd9 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,2 @@ +parameters: + checkMissingIterableValueType: false diff --git a/src/IO/BufferedOutput.php b/src/IO/BufferedOutput.php index ad00297..de4e98d 100644 --- a/src/IO/BufferedOutput.php +++ b/src/IO/BufferedOutput.php @@ -7,6 +7,9 @@ */ class BufferedOutput implements OutputStream { + /** + * @var string + */ private $buffer = ''; public function write(string $buffer): void diff --git a/src/IO/NonBlockingResourceInputStream.php b/src/IO/NonBlockingResourceInputStream.php new file mode 100644 index 0000000..5da4a11 --- /dev/null +++ b/src/IO/NonBlockingResourceInputStream.php @@ -0,0 +1,38 @@ +innerStream = new ResourceInputStream($stream ?? STDIN); + stream_set_blocking($stream, false); + } + + /** + * @inheritDoc + */ + public function read(int $numBytes, callable $callback): void + { + $this->innerStream->read($numBytes, $callback); + } + + /** + * @inheritDoc + */ + public function isInteractive(): bool + { + return $this->innerStream->isInteractive(); + } +} diff --git a/src/IO/ResourceInputStream.php b/src/IO/ResourceInputStream.php index d72cbd0..b334c37 100644 --- a/src/IO/ResourceInputStream.php +++ b/src/IO/ResourceInputStream.php @@ -17,8 +17,13 @@ class ResourceInputStream implements InputStream */ private $stream; - public function __construct($stream = STDIN) + /** + * @param resource $stream + */ + public function __construct($stream = null) { + $stream = $stream ? $stream : STDIN; + if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { throw new \InvalidArgumentException('Expected a valid stream'); } diff --git a/src/IO/ResourceOutputStream.php b/src/IO/ResourceOutputStream.php index 005ed22..c4f5fcf 100644 --- a/src/IO/ResourceOutputStream.php +++ b/src/IO/ResourceOutputStream.php @@ -17,8 +17,13 @@ class ResourceOutputStream implements OutputStream */ private $stream; - public function __construct($stream = STDOUT) + /** + * @param resource $stream + */ + public function __construct($stream = null) { + $stream = $stream ? $stream : STDOUT; + if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { throw new \InvalidArgumentException('Expected a valid stream'); } diff --git a/src/InputCharacter.php b/src/InputCharacter.php index 4aec960..57d6f82 100644 --- a/src/InputCharacter.php +++ b/src/InputCharacter.php @@ -28,6 +28,9 @@ class InputCharacter public const TAB = 'TAB'; public const ESC = 'ESC'; + /** + * @var array + */ private static $controls = [ "\033[A" => self::UP, "\033[B" => self::DOWN, @@ -60,7 +63,7 @@ public function isHandledControl() : bool */ public function isControl() : bool { - return preg_match('/[\x00-\x1F\x7F]/', $this->data); + return (bool) preg_match('/[\x00-\x1F\x7F]/', $this->data); } /** @@ -128,6 +131,6 @@ public static function fromControlName(string $controlName) : self throw new \InvalidArgumentException(sprintf('Control "%s" does not exist', $controlName)); } - return new static(array_search($controlName, static::$controls, true)); + return new self(array_search($controlName, static::$controls, true)); } } diff --git a/src/NonCanonicalReader.php b/src/NonCanonicalReader.php index 6d26666..7838879 100644 --- a/src/NonCanonicalReader.php +++ b/src/NonCanonicalReader.php @@ -3,7 +3,7 @@ namespace PhpSchool\Terminal; /** - * This class takes a terminal and disabled canonical mode. It reads the input + * This class takes a terminal and disables canonical mode. It reads the input * and returns characters and control sequences as `InputCharacters` as soon * as they are read - character by character. * @@ -56,13 +56,15 @@ public function addControlMappings(array $mappings) : void /** * This should be ran with the terminal canonical mode disabled. - * - * @return InputCharacter */ - public function readCharacter() : InputCharacter + public function readCharacter() : ?InputCharacter { $char = $this->terminal->read(4); + if ($char === '') { + return null; + } + if (isset($this->mappings[$char])) { return InputCharacter::fromControlName($this->mappings[$char]); } diff --git a/src/UnixTerminal.php b/src/UnixTerminal.php index 879b1a7..b4dc47d 100644 --- a/src/UnixTerminal.php +++ b/src/UnixTerminal.php @@ -1,4 +1,4 @@ -getOriginalConfiguration(); - $this->getOriginalCanonicalMode(); + $this->initTerminal(); + $this->input = $input; $this->output = $output; } + private function initTerminal() : void + { + $this->getOriginalConfiguration(); + $this->getOriginalCanonicalMode(); + + pcntl_async_signals(true); + + $this->onSignal(SIGWINCH, [$this, 'refreshDimensions']); + } + private function getOriginalCanonicalMode() : void { exec('stty -a', $output); $this->isCanonical = (strpos(implode("\n", $output), ' icanon') !== false); } + private function getOriginalConfiguration() : string + { + return $this->originalConfiguration ?: $this->originalConfiguration = exec('stty -g'); + } + public function getWidth() : int { return $this->width ?: $this->width = (int) exec('tput cols'); @@ -84,9 +104,26 @@ public function getColourSupport() : int return $this->colourSupport ?: $this->colourSupport = (int) exec('tput colors'); } - private function getOriginalConfiguration() : string + private function refreshDimensions() : void { - return $this->originalConfiguration ?: $this->originalConfiguration = exec('stty -g'); + $this->width = (int) exec('tput cols'); + $this->height = (int) exec('tput lines'); + } + + public function onSignal(int $signo, callable $handler) : void + { + if (!isset($this->signalHandlers[$signo])) { + $this->signalHandlers[$signo] = []; + pcntl_signal($signo, [$this, 'handleSignal']); + } + $this->signalHandlers[$signo][] = $handler; + } + + public function handleSignal(int $signo) : void + { + foreach ($this->signalHandlers[$signo] as $signalHandler) { + $signalHandler(); + } } /**