From f8db35135e79588b3f8c030eabcfeb521a2b5513 Mon Sep 17 00:00:00 2001 From: Zi Xing Date: Sat, 16 Apr 2022 19:06:04 -0400 Subject: [PATCH] Added added third-party library 'php-school/terminal' --- .../Exception/NotInteractiveTerminal.php | 19 ++ .../php-school/terminal/IO/BufferedOutput.php | 40 +++ .../php-school/terminal/IO/InputStream.php | 20 ++ .../php-school/terminal/IO/OutputStream.php | 19 ++ .../terminal/IO/ResourceInputStream.php | 47 +++ .../terminal/IO/ResourceOutputStream.php | 46 +++ .../php-school/terminal/InputCharacter.php | 137 +++++++++ .../terminal/NonCanonicalReader.php | 79 +++++ .../ThirdParty/php-school/terminal/README.md | 32 ++ .../php-school/terminal/Terminal.php | 133 ++++++++ .../php-school/terminal/UnixTerminal.php | 287 ++++++++++++++++++ 11 files changed, 859 insertions(+) create mode 100644 src/ncc/ThirdParty/php-school/terminal/Exception/NotInteractiveTerminal.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/IO/BufferedOutput.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/IO/InputStream.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/IO/OutputStream.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/IO/ResourceInputStream.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/IO/ResourceOutputStream.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/InputCharacter.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/NonCanonicalReader.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/README.md create mode 100644 src/ncc/ThirdParty/php-school/terminal/Terminal.php create mode 100644 src/ncc/ThirdParty/php-school/terminal/UnixTerminal.php diff --git a/src/ncc/ThirdParty/php-school/terminal/Exception/NotInteractiveTerminal.php b/src/ncc/ThirdParty/php-school/terminal/Exception/NotInteractiveTerminal.php new file mode 100644 index 0000000..764cbf1 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/Exception/NotInteractiveTerminal.php @@ -0,0 +1,19 @@ + + */ +class NotInteractiveTerminal extends \RuntimeException +{ + public static function inputNotInteractive() : self + { + return new self('Input stream is not interactive (non TTY)'); + } + + public static function outputNotInteractive() : self + { + return new self('Output stream is not interactive (non TTY)'); + } +} diff --git a/src/ncc/ThirdParty/php-school/terminal/IO/BufferedOutput.php b/src/ncc/ThirdParty/php-school/terminal/IO/BufferedOutput.php new file mode 100644 index 0000000..ad00297 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/IO/BufferedOutput.php @@ -0,0 +1,40 @@ + + */ +class BufferedOutput implements OutputStream +{ + private $buffer = ''; + + public function write(string $buffer): void + { + $this->buffer .= $buffer; + } + + public function fetch(bool $clean = true) : string + { + $buffer = $this->buffer; + + if ($clean) { + $this->buffer = ''; + } + + return $buffer; + } + + public function __toString() : string + { + return $this->fetch(); + } + + /** + * Whether the stream is connected to an interactive terminal + */ + public function isInteractive() : bool + { + return false; + } +} diff --git a/src/ncc/ThirdParty/php-school/terminal/IO/InputStream.php b/src/ncc/ThirdParty/php-school/terminal/IO/InputStream.php new file mode 100644 index 0000000..8fcd82a --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/IO/InputStream.php @@ -0,0 +1,20 @@ + + */ +interface InputStream +{ + /** + * Callback should be called with the number of bytes requested + * when ready. + */ + public function read(int $numBytes, callable $callback) : void; + + /** + * Whether the stream is connected to an interactive terminal + */ + public function isInteractive() : bool; +} diff --git a/src/ncc/ThirdParty/php-school/terminal/IO/OutputStream.php b/src/ncc/ThirdParty/php-school/terminal/IO/OutputStream.php new file mode 100644 index 0000000..9314cd5 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/IO/OutputStream.php @@ -0,0 +1,19 @@ + + */ +interface OutputStream +{ + /** + * Write the buffer to the stream + */ + public function write(string $buffer) : void; + + /** + * Whether the stream is connected to an interactive terminal + */ + public function isInteractive() : bool; +} diff --git a/src/ncc/ThirdParty/php-school/terminal/IO/ResourceInputStream.php b/src/ncc/ThirdParty/php-school/terminal/IO/ResourceInputStream.php new file mode 100644 index 0000000..d72cbd0 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/IO/ResourceInputStream.php @@ -0,0 +1,47 @@ + + */ +class ResourceInputStream implements InputStream +{ + /** + * @var resource + */ + private $stream; + + public function __construct($stream = STDIN) + { + if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { + throw new \InvalidArgumentException('Expected a valid stream'); + } + + $meta = stream_get_meta_data($stream); + if (strpos($meta['mode'], 'r') === false && strpos($meta['mode'], '+') === false) { + throw new \InvalidArgumentException('Expected a readable stream'); + } + + $this->stream = $stream; + } + + public function read(int $numBytes, callable $callback) : void + { + $buffer = fread($this->stream, $numBytes); + $callback($buffer); + } + + /** + * Whether the stream is connected to an interactive terminal + */ + public function isInteractive() : bool + { + return posix_isatty($this->stream); + } +} diff --git a/src/ncc/ThirdParty/php-school/terminal/IO/ResourceOutputStream.php b/src/ncc/ThirdParty/php-school/terminal/IO/ResourceOutputStream.php new file mode 100644 index 0000000..005ed22 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/IO/ResourceOutputStream.php @@ -0,0 +1,46 @@ + + */ +class ResourceOutputStream implements OutputStream +{ + /** + * @var resource + */ + private $stream; + + public function __construct($stream = STDOUT) + { + if (!is_resource($stream) || get_resource_type($stream) !== 'stream') { + throw new \InvalidArgumentException('Expected a valid stream'); + } + + $meta = stream_get_meta_data($stream); + if (strpos($meta['mode'], 'r') !== false && strpos($meta['mode'], '+') === false) { + throw new \InvalidArgumentException('Expected a writable stream'); + } + + $this->stream = $stream; + } + + public function write(string $buffer): void + { + fwrite($this->stream, $buffer); + } + + /** + * Whether the stream is connected to an interactive terminal + */ + public function isInteractive() : bool + { + return posix_isatty($this->stream); + } +} diff --git a/src/ncc/ThirdParty/php-school/terminal/InputCharacter.php b/src/ncc/ThirdParty/php-school/terminal/InputCharacter.php new file mode 100644 index 0000000..7829512 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/InputCharacter.php @@ -0,0 +1,137 @@ + + */ +class InputCharacter +{ + /** + * @var string + */ + private $data; + + public const UP = 'UP'; + public const DOWN = 'DOWN'; + public const RIGHT = 'RIGHT'; + public const LEFT = 'LEFT'; + public const CTRLA = 'CTRLA'; + public const CTRLB = 'CTRLB'; + public const CTRLE = 'CTRLE'; + public const CTRLF = 'CTRLF'; + public const BACKSPACE = 'BACKSPACE'; + public const CTRLW = 'CTRLW'; + public const ENTER = 'ENTER'; + public const TAB = 'TAB'; + public const ESC = 'ESC'; + + private static $controls = [ + "\033[A" => self::UP, + "\033[B" => self::DOWN, + "\033[C" => self::RIGHT, + "\033[D" => self::LEFT, + "\033OA" => self::UP, + "\033OB" => self::DOWN, + "\033OC" => self::RIGHT, + "\033OD" => self::LEFT, + "\001" => self::CTRLA, + "\002" => self::CTRLB, + "\005" => self::CTRLE, + "\006" => self::CTRLF, + "\010" => self::BACKSPACE, + "\177" => self::BACKSPACE, + "\027" => self::CTRLW, + "\n" => self::ENTER, + "\t" => self::TAB, + "\e" => self::ESC, + ]; + + public function __construct(string $data) + { + $this->data = $data; + } + + public function isHandledControl() : bool + { + return isset(static::$controls[$this->data]); + } + + /** + * Is this character a control sequence? + */ + public function isControl() : bool + { + return preg_match('/[\x00-\x1F\x7F]/', $this->data); + } + + /** + * Is this character a normal character? + */ + public function isNotControl() : bool + { + return ! $this->isControl(); + } + + /** + * Get the raw character or control sequence + */ + public function get() : string + { + return $this->data; + } + + /** + * Get the actual control name that this sequence represents. + * One of the class constants. Eg. self::UP. + * + * Throws an exception if the character is not actually a control sequence + */ + public function getControl() : string + { + if (!isset(static::$controls[$this->data])) { + throw new \RuntimeException(sprintf('Character "%s" is not a control', $this->data)); + } + + return static::$controls[$this->data]; + } + + /** + * Get the raw character or control sequence + */ + public function __toString() : string + { + return $this->get(); + } + + /** + * Does the given control name exist? eg self::UP. + */ + public static function controlExists(string $controlName) : bool + { + return in_array($controlName, static::$controls, true); + } + + /** + * Get all of the available control names + */ + public static function getControls() : array + { + return array_values(array_unique(static::$controls)); + } + + /** + * Create a instance from a given control name. Throws an exception if the + * control name does not exist. + */ + public static function fromControlName(string $controlName) : self + { + if (!static::controlExists($controlName)) { + throw new \InvalidArgumentException(sprintf('Control "%s" does not exist', $controlName)); + } + + return new static(array_search($controlName, static::$controls, true)); + } +} diff --git a/src/ncc/ThirdParty/php-school/terminal/NonCanonicalReader.php b/src/ncc/ThirdParty/php-school/terminal/NonCanonicalReader.php new file mode 100644 index 0000000..6d26666 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/NonCanonicalReader.php @@ -0,0 +1,79 @@ + + */ +class NonCanonicalReader +{ + /** + * @var Terminal + */ + private $terminal; + + /** + * @var bool + */ + private $wasCanonicalModeEnabled; + + /** + * Map of characters to controls. + * Eg map 'w' to the up control. + * + * @var array + */ + private $mappings = []; + + public function __construct(Terminal $terminal) + { + $this->terminal = $terminal; + $this->wasCanonicalModeEnabled = $terminal->isCanonicalMode(); + $this->terminal->disableCanonicalMode(); + } + + public function addControlMapping(string $character, string $mapToControl) : void + { + if (!InputCharacter::controlExists($mapToControl)) { + throw new \InvalidArgumentException(sprintf('Control "%s" does not exist', $mapToControl)); + } + + $this->mappings[$character] = $mapToControl; + } + + public function addControlMappings(array $mappings) : void + { + foreach ($mappings as $character => $mapToControl) { + $this->addControlMapping($character, $mapToControl); + } + } + + /** + * This should be ran with the terminal canonical mode disabled. + * + * @return InputCharacter + */ + public function readCharacter() : InputCharacter + { + $char = $this->terminal->read(4); + + if (isset($this->mappings[$char])) { + return InputCharacter::fromControlName($this->mappings[$char]); + } + + return new InputCharacter($char); + } + + public function __destruct() + { + if ($this->wasCanonicalModeEnabled) { + $this->terminal->enableCanonicalMode(); + } + } +} diff --git a/src/ncc/ThirdParty/php-school/terminal/README.md b/src/ncc/ThirdParty/php-school/terminal/README.md new file mode 100644 index 0000000..c96b02a --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/README.md @@ -0,0 +1,32 @@ +

Terminal Utility

+ +

+ Small utility to help provide a simple, consise API for terminal interaction +

+ +

+ + + + + + + + + + + +

+ +--- + +## Install + +```bash +composer require php-school/terminal +``` + +## TODO + +- [ ] Docs diff --git a/src/ncc/ThirdParty/php-school/terminal/Terminal.php b/src/ncc/ThirdParty/php-school/terminal/Terminal.php new file mode 100644 index 0000000..96d1603 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/Terminal.php @@ -0,0 +1,133 @@ + + * @author Aydin Hassan + */ +interface Terminal +{ + /** + * Get the available width of the terminal + */ + public function getWidth() : int; + + /** + * Get the available height of the terminal + */ + public function getHeight() : int; + + /** + * Get the number of colours the terminal supports (1, 8, 256, true colours) + */ + public function getColourSupport() : int; + + /** + * Disables echoing every character back to the terminal. This means + * we do not have to clear the line when reading. + */ + public function disableEchoBack() : void; + + /** + * Enable echoing back every character input to the terminal. + */ + public function enableEchoBack() : void; + + /** + * Is echo back mode enabled + */ + public function isEchoBack() : bool; + + /** + * Disable canonical input (allow each key press for reading, rather than the whole line) + * + * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html + */ + public function disableCanonicalMode() : void; + + /** + * Enable canonical input - read input by line + * + * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html + */ + public function enableCanonicalMode() : void; + + /** + * Is canonical mode enabled or not + */ + public function isCanonicalMode() : bool; + + /** + * Check if the Input & Output streams are interactive. Eg - they are + * connected to a terminal. + * + * @return bool + */ + public function isInteractive() : bool; + + /** + * Restore the terminals original configuration + */ + public function restoreOriginalConfiguration() : void; + + /** + * Test whether terminal supports colour output + */ + public function supportsColour() : bool; + + /** + * Clear the terminal window + */ + public function clear() : void; + + /** + * Clear the current cursors line + */ + public function clearLine() : void; + + /** + * Erase screen from the current line down to the bottom of the screen + */ + public function clearDown() : void; + + /** + * Clean the whole console without jumping the window + */ + public function clean() : void; + + /** + * Enable cursor display + */ + public function enableCursor() : void; + + /** + * Disable cursor display + */ + public function disableCursor() : void; + + /** + * Move the cursor to the top left of the window + */ + public function moveCursorToTop() : void; + + /** + * Move the cursor to the start of a specific row + */ + public function moveCursorToRow(int $rowNumber) : void; + + /** + * Move the cursor to a specific column + */ + public function moveCursorToColumn(int $columnNumber) : void; + + /** + * Read from the input stream + */ + public function read(int $bytes) : string; + + /** + * Write to the output stream + */ + public function write(string $buffer) : void; +} diff --git a/src/ncc/ThirdParty/php-school/terminal/UnixTerminal.php b/src/ncc/ThirdParty/php-school/terminal/UnixTerminal.php new file mode 100644 index 0000000..879b1a7 --- /dev/null +++ b/src/ncc/ThirdParty/php-school/terminal/UnixTerminal.php @@ -0,0 +1,287 @@ + + * @author Aydin Hassan + */ +class UnixTerminal implements Terminal +{ + /** + * @var bool + */ + private $isCanonical; + + /** + * Whether terminal echo back is enabled or not. + * Eg. user key presses and the terminal immediately shows it. + * + * @var bool + */ + private $echoBack = true; + + /** + * @var int + */ + private $width; + + /** + * @var int + */ + private $height; + + /** + * @var int; + */ + private $colourSupport; + + /** + * @var string + */ + private $originalConfiguration; + + /** + * @var InputStream + */ + private $input; + + /** + * @var OutputStream + */ + private $output; + + public function __construct(InputStream $input, OutputStream $output) + { + $this->getOriginalConfiguration(); + $this->getOriginalCanonicalMode(); + $this->input = $input; + $this->output = $output; + } + + private function getOriginalCanonicalMode() : void + { + exec('stty -a', $output); + $this->isCanonical = (strpos(implode("\n", $output), ' icanon') !== false); + } + + public function getWidth() : int + { + return $this->width ?: $this->width = (int) exec('tput cols'); + } + + public function getHeight() : int + { + return $this->height ?: $this->height = (int) exec('tput lines'); + } + + public function getColourSupport() : int + { + return $this->colourSupport ?: $this->colourSupport = (int) exec('tput colors'); + } + + private function getOriginalConfiguration() : string + { + return $this->originalConfiguration ?: $this->originalConfiguration = exec('stty -g'); + } + + /** + * Disables echoing every character back to the terminal. This means + * we do not have to clear the line when reading. + */ + public function disableEchoBack() : void + { + exec('stty -echo'); + $this->echoBack = false; + } + + /** + * Enable echoing back every character input to the terminal. + */ + public function enableEchoBack() : void + { + exec('stty echo'); + $this->echoBack = true; + } + + /** + * Is echo back mode enabled + */ + public function isEchoBack() : bool + { + return $this->echoBack; + } + + /** + * Disable canonical input (allow each key press for reading, rather than the whole line) + * + * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html + */ + public function disableCanonicalMode() : void + { + if ($this->isCanonical) { + exec('stty -icanon'); + $this->isCanonical = false; + } + } + + /** + * Enable canonical input - read input by line + * + * @see https://www.gnu.org/software/libc/manual/html_node/Canonical-or-Not.html + */ + public function enableCanonicalMode() : void + { + if (!$this->isCanonical) { + exec('stty icanon'); + $this->isCanonical = true; + } + } + + /** + * Is canonical mode enabled or not + */ + public function isCanonicalMode() : bool + { + return $this->isCanonical; + } + + /** + * Restore the original terminal configuration + */ + public function restoreOriginalConfiguration() : void + { + exec('stty ' . $this->getOriginalConfiguration()); + } + + /** + * Check if the Input & Output streams are interactive. Eg - they are + * connected to a terminal. + * + * @return bool + */ + public function isInteractive() : bool + { + return $this->input->isInteractive() && $this->output->isInteractive(); + } + + /** + * Assert that both the Input & Output streams are interactive. Throw + * `NotInteractiveTerminal` if not. + */ + public function mustBeInteractive() : void + { + if (!$this->input->isInteractive()) { + throw NotInteractiveTerminal::inputNotInteractive(); + } + + if (!$this->output->isInteractive()) { + throw NotInteractiveTerminal::outputNotInteractive(); + } + } + + /** + * @see https://github.com/symfony/Console/blob/master/Output/StreamOutput.php#L95-L102 + */ + public function supportsColour() : bool + { + if (DIRECTORY_SEPARATOR === '\\') { + return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); + } + + return $this->isInteractive(); + } + + public function clear() : void + { + $this->output->write("\033[2J"); + } + + public function clearLine() : void + { + $this->output->write("\033[2K"); + } + + /** + * Erase screen from the current line down to the bottom of the screen + */ + public function clearDown() : void + { + $this->output->write("\033[J"); + } + + public function clean() : void + { + foreach (range(0, $this->getHeight()) as $rowNum) { + $this->moveCursorToRow($rowNum); + $this->clearLine(); + } + } + + public function enableCursor() : void + { + $this->output->write("\033[?25h"); + } + + public function disableCursor() : void + { + $this->output->write("\033[?25l"); + } + + public function moveCursorToTop() : void + { + $this->output->write("\033[H"); + } + + public function moveCursorToRow(int $rowNumber) : void + { + $this->output->write(sprintf("\033[%d;0H", $rowNumber)); + } + + public function moveCursorToColumn(int $column) : void + { + $this->output->write(sprintf("\033[%dC", $column)); + } + + public function showSecondaryScreen() : void + { + $this->output->write("\033[?47h"); + } + + public function showPrimaryScreen() : void + { + $this->output->write("\033[?47l"); + } + + /** + * Read bytes from the input stream + */ + public function read(int $bytes): string + { + $buffer = ''; + $this->input->read($bytes, function ($data) use (&$buffer) { + $buffer .= $data; + }); + return $buffer; + } + + /** + * Write to the output stream + */ + public function write(string $buffer): void + { + $this->output->write($buffer); + } + + /** + * Restore the original terminal configuration on shutdown. + */ + public function __destruct() + { + $this->restoreOriginalConfiguration(); + } +}