summaryrefslogtreecommitdiffstats
path: root/vendor/clue/stdio-react/src/Stdio.php
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/clue/stdio-react/src/Stdio.php')
-rw-r--r--vendor/clue/stdio-react/src/Stdio.php630
1 files changed, 630 insertions, 0 deletions
diff --git a/vendor/clue/stdio-react/src/Stdio.php b/vendor/clue/stdio-react/src/Stdio.php
new file mode 100644
index 0000000..aff2959
--- /dev/null
+++ b/vendor/clue/stdio-react/src/Stdio.php
@@ -0,0 +1,630 @@
+<?php
+
+namespace Clue\React\Stdio;
+
+use Evenement\EventEmitter;
+use React\EventLoop\LoopInterface;
+use React\Stream\DuplexStreamInterface;
+use React\Stream\ReadableResourceStream;
+use React\Stream\ReadableStreamInterface;
+use React\Stream\Util;
+use React\Stream\WritableResourceStream;
+use React\Stream\WritableStreamInterface;
+
+class Stdio extends EventEmitter implements DuplexStreamInterface
+{
+ private $input;
+ private $output;
+ private $readline;
+
+ private $ending = false;
+ private $closed = false;
+ private $incompleteLine = '';
+ private $originalTtyMode = null;
+
+ /**
+ *
+ * This class takes an optional `LoopInterface|null $loop` parameter that can be used to
+ * pass the event loop instance to use for this object. You can use a `null` value
+ * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
+ * This value SHOULD NOT be given unless you're sure you want to explicitly use a
+ * given event loop instance.
+ *
+ * @param ?LoopInterface $loop
+ * @param ?ReadableStreamInterface $input
+ * @param ?WritableStreamInterface $output
+ * @param ?Readline $readline
+ */
+ public function __construct(LoopInterface $loop = null, ReadableStreamInterface $input = null, WritableStreamInterface $output = null, Readline $readline = null)
+ {
+ if ($input === null) {
+ $input = $this->createStdin($loop); // @codeCoverageIgnore
+ }
+
+ if ($output === null) {
+ $output = $this->createStdout($loop); // @codeCoverageIgnore
+ }
+
+ if ($readline === null) {
+ $readline = new Readline($input, $output, $this);
+ }
+
+ $this->input = $input;
+ $this->output = $output;
+ $this->readline = $readline;
+
+ $that = $this;
+
+ // readline data emits a new line
+ $incomplete =& $this->incompleteLine;
+ $this->readline->on('data', function($line) use ($that, &$incomplete) {
+ // readline emits a new line on enter, so start with a blank line
+ $incomplete = '';
+ $that->emit('data', array($line));
+ });
+
+ // handle all input events (readline forwards all input events)
+ $this->readline->on('error', array($this, 'handleError'));
+ $this->readline->on('end', array($this, 'handleEnd'));
+ $this->readline->on('close', array($this, 'handleCloseInput'));
+
+ // handle all output events
+ $this->output->on('error', array($this, 'handleError'));
+ $this->output->on('close', array($this, 'handleCloseOutput'));
+ }
+
+ public function __destruct()
+ {
+ $this->restoreTtyMode();
+ }
+
+ public function pause()
+ {
+ $this->input->pause();
+ }
+
+ public function resume()
+ {
+ $this->input->resume();
+ }
+
+ public function isReadable()
+ {
+ return $this->input->isReadable();
+ }
+
+ public function isWritable()
+ {
+ return $this->output->isWritable();
+ }
+
+ public function pipe(WritableStreamInterface $dest, array $options = array())
+ {
+ Util::pipe($this, $dest, $options);
+
+ return $dest;
+ }
+
+ public function write($data)
+ {
+ // return false if already ended, return true if writing empty string
+ if ($this->ending || $data === '') {
+ return !$this->ending;
+ }
+
+ $out = $data;
+
+ $lastNewline = strrpos($data, "\n");
+
+ $restoreReadline = false;
+
+ if ($this->incompleteLine !== '') {
+ // the last write did not end with a newline => append to existing row
+
+ // move one line up and move cursor to last position before writing data
+ $out = "\033[A" . "\r\033[" . $this->width($this->incompleteLine) . "C" . $out;
+
+ // data contains a newline, so this will overwrite the readline prompt
+ if ($lastNewline !== false) {
+ // move cursor to beginning of readline prompt and clear line
+ // clearing is important because $data may not overwrite the whole line
+ $out = "\r\033[K" . $out;
+
+ // make sure to restore readline after this output
+ $restoreReadline = true;
+ }
+ } else {
+ // here, we're writing to a new line => overwrite readline prompt
+
+ // move cursor to beginning of readline prompt and clear line
+ $out = "\r\033[K" . $out;
+
+ // we always overwrite the readline prompt, so restore it on next line
+ $restoreReadline = true;
+ }
+
+ // following write will have have to append to this line if it does not end with a newline
+ $endsWithNewline = substr($data, -1) === "\n";
+
+ if ($endsWithNewline) {
+ // line ends with newline, so this is line is considered complete
+ $this->incompleteLine = '';
+ } else {
+ // always end data with newline in order to append readline on next line
+ $out .= "\n";
+
+ if ($lastNewline === false) {
+ // contains no newline at all, everything is incomplete
+ $this->incompleteLine .= $data;
+ } else {
+ // contains a newline, everything behind it is incomplete
+ $this->incompleteLine = (string)substr($data, $lastNewline + 1);
+ }
+ }
+
+ if ($restoreReadline) {
+ // write output and restore original readline prompt and line buffer
+ return $this->output->write($out . $this->readline->getDrawString());
+ } else {
+ // restore original cursor position in readline prompt
+ $pos = $this->width($this->readline->getPrompt()) + $this->readline->getCursorCell();
+ if ($pos !== 0) {
+ // we always start at beginning of line, move right by X
+ $out .= "\033[" . $pos . "C";
+ }
+
+ // write to actual output stream
+ return $this->output->write($out);
+ }
+ }
+
+ public function end($data = null)
+ {
+ if ($this->ending) {
+ return;
+ }
+
+ if ($data !== null) {
+ $this->write($data);
+ }
+
+ $this->ending = true;
+
+ // clear readline output, close input and end output
+ $this->readline->setInput('')->setPrompt('');
+ $this->input->close();
+ $this->output->end();
+ }
+
+ public function close()
+ {
+ if ($this->closed) {
+ return;
+ }
+
+ $this->ending = true;
+ $this->closed = true;
+
+ $this->input->close();
+ $this->output->close();
+
+ $this->emit('close');
+ $this->removeAllListeners();
+ }
+
+ /**
+ * @deprecated
+ * @return Readline
+ */
+ public function getReadline()
+ {
+ return $this->readline;
+ }
+
+
+ /**
+ * prompt to prepend to input line
+ *
+ * Will redraw the current input prompt with the current input buffer.
+ *
+ * @param string $prompt
+ * @return void
+ */
+ public function setPrompt($prompt)
+ {
+ $this->readline->setPrompt($prompt);
+ }
+
+ /**
+ * returns the prompt to prepend to input line
+ *
+ * @return string
+ * @see self::setPrompt()
+ */
+ public function getPrompt()
+ {
+ return $this->readline->getPrompt();
+ }
+
+ /**
+ * sets whether/how to echo text input
+ *
+ * The default setting is `true`, which means that every character will be
+ * echo'ed as-is, i.e. you can see what you're typing.
+ * For example: Typing "test" shows "test".
+ *
+ * You can turn this off by supplying `false`, which means that *nothing*
+ * will be echo'ed while you're typing. This could be a good idea for
+ * password prompts. Note that this could be confusing for users, so using
+ * a character replacement as following is often preferred.
+ * For example: Typing "test" shows "" (nothing).
+ *
+ * Alternative, you can supply a single character replacement character
+ * that will be echo'ed for each character in the text input. This could
+ * be a good idea for password prompts, where an asterisk character ("*")
+ * is often used to indicate typing activity and password length.
+ * For example: Typing "test" shows "****" (with asterisk replacement)
+ *
+ * Changing this setting will redraw the current prompt and echo the current
+ * input buffer according to the new setting.
+ *
+ * @param boolean|string $echo echo can be turned on (boolean true) or off (boolean true), or you can supply a single character replacement string
+ * @return void
+ */
+ public function setEcho($echo)
+ {
+ $this->readline->setEcho($echo);
+ }
+
+ /**
+ * whether or not to support moving cursor left and right
+ *
+ * switching cursor support moves the cursor to the end of the current
+ * input buffer (if any).
+ *
+ * @param boolean $move
+ * @return void
+ */
+ public function setMove($move)
+ {
+ $this->readline->setMove($move);
+ }
+
+ /**
+ * Gets current cursor position measured in number of text characters.
+ *
+ * Note that the number of text characters doesn't necessarily reflect the
+ * number of monospace cells occupied by the text characters. If you want
+ * to know the latter, use `self::getCursorCell()` instead.
+ *
+ * @return int
+ * @see self::getCursorCell() to get the position measured in monospace cells
+ * @see self::moveCursorTo() to move the cursor to a given character position
+ * @see self::moveCursorBy() to move the cursor by given number of characters
+ * @see self::setMove() to toggle whether the user can move the cursor position
+ */
+ public function getCursorPosition()
+ {
+ return $this->readline->getCursorPosition();
+ }
+
+ /**
+ * Gets current cursor position measured in monospace cells.
+ *
+ * Note that the cell position doesn't necessarily reflect the number of
+ * text characters. If you want to know the latter, use
+ * `self::getCursorPosition()` instead.
+ *
+ * Most "normal" characters occupy a single monospace cell, i.e. the ASCII
+ * sequence for "A" requires a single cell, as do most UTF-8 sequences
+ * like "Ä".
+ *
+ * However, there are a number of code points that do not require a cell
+ * (i.e. invisible surrogates) or require two cells (e.g. some asian glyphs).
+ *
+ * Also note that this takes the echo mode into account, i.e. the cursor is
+ * always at position zero if echo is off. If using a custom echo character
+ * (like asterisk), it will take its width into account instead of the actual
+ * input characters.
+ *
+ * @return int
+ * @see self::getCursorPosition() to get current cursor position measured in characters
+ * @see self::moveCursorTo() to move the cursor to a given character position
+ * @see self::moveCursorBy() to move the cursor by given number of characters
+ * @see self::setMove() to toggle whether the user can move the cursor position
+ * @see self::setEcho()
+ */
+ public function getCursorCell()
+ {
+ return $this->readline->getCursorCell();
+ }
+
+ /**
+ * Moves cursor to right by $n chars (or left if $n is negative).
+ *
+ * Zero value or values out of range (exceeding current input buffer) are
+ * simply ignored.
+ *
+ * Will redraw() the readline only if the visible cell position changes,
+ * see `self::getCursorCell()` for more details.
+ *
+ * @param int $n
+ * @return void
+ */
+ public function moveCursorBy($n)
+ {
+ $this->readline->moveCursorBy($n);
+ }
+
+ /**
+ * Moves cursor to given position in current line buffer.
+ *
+ * Values out of range (exceeding current input buffer) are simply ignored.
+ *
+ * Will redraw() the readline only if the visible cell position changes,
+ * see `self::getCursorCell()` for more details.
+ *
+ * @param int $n
+ * @return void
+ */
+ public function moveCursorTo($n)
+ {
+ $this->readline->moveCursorTo($n);
+ }
+
+ /**
+ * Appends the given input to the current text input buffer at the current position
+ *
+ * This moves the cursor accordingly to the number of characters added.
+ *
+ * @param string $input
+ * @return void
+ */
+ public function addInput($input)
+ {
+ $this->readline->addInput($input);
+ }
+
+ /**
+ * set current text input buffer
+ *
+ * this moves the cursor to the end of the current
+ * input buffer (if any).
+ *
+ * @param string $input
+ * @return void
+ */
+ public function setInput($input)
+ {
+ $this->readline->setInput($input);
+ }
+
+ /**
+ * get current text input buffer
+ *
+ * @return string
+ */
+ public function getInput()
+ {
+ return $this->readline->getInput();
+ }
+
+ /**
+ * Adds a new line to the (bottom position of the) history list
+ *
+ * @param string $line
+ * @return void
+ */
+ public function addHistory($line)
+ {
+ $this->readline->addHistory($line);
+ }
+
+ /**
+ * Clears the complete history list
+ *
+ * @return void
+ */
+ public function clearHistory()
+ {
+ $this->readline->clearHistory();
+ }
+
+ /**
+ * Returns an array with all lines in the history
+ *
+ * @return string[]
+ */
+ public function listHistory()
+ {
+ return $this->readline->listHistory();
+ }
+
+ /**
+ * Limits the history to a maximum of N entries and truncates the current history list accordingly
+ *
+ * @param int|null $limit
+ * @return void
+ */
+ public function limitHistory($limit)
+ {
+ $this->readline->limitHistory($limit);
+ }
+
+ /**
+ * set autocompletion handler to use
+ *
+ * The autocomplete handler will be called whenever the user hits the TAB
+ * key.
+ *
+ * @param callable|null $autocomplete
+ * @return void
+ * @throws \InvalidArgumentException if the given callable is invalid
+ */
+ public function setAutocomplete($autocomplete)
+ {
+ $this->readline->setAutocomplete($autocomplete);
+ }
+
+ /**
+ * whether or not to emit a audible/visible BELL signal when using a disabled function
+ *
+ * By default, this class will emit a BELL signal when using a disable function,
+ * such as using the <kbd>left</kbd> or <kbd>backspace</kbd> keys when
+ * already at the beginning of the line.
+ *
+ * Whether or not the BELL is audible/visible depends on the termin and its
+ * settings, i.e. some terminals may "beep" or flash the screen or emit a
+ * short vibration.
+ *
+ * @param bool $bell
+ * @return void
+ */
+ public function setBell($bell)
+ {
+ $this->readline->setBell($bell);
+ }
+
+ private function width($str)
+ {
+ return $this->readline->strwidth($str) - 2 * substr_count($str, "\x08");
+ }
+
+ /** @internal */
+ public function handleError(\Exception $e)
+ {
+ $this->emit('error', array($e));
+ $this->close();
+ }
+
+ /** @internal */
+ public function handleEnd()
+ {
+ $this->emit('end');
+ }
+
+ /** @internal */
+ public function handleCloseInput()
+ {
+ $this->restoreTtyMode();
+
+ if (!$this->output->isWritable()) {
+ $this->close();
+ }
+ }
+
+ /** @internal */
+ public function handleCloseOutput()
+ {
+ if (!$this->input->isReadable()) {
+ $this->close();
+ }
+ }
+
+ /**
+ * @codeCoverageIgnore this is covered by functional tests with/without ext-readline
+ */
+ private function restoreTtyMode()
+ {
+ if (function_exists('readline_callback_handler_remove')) {
+ // remove dummy readline handler to turn to default input mode
+ readline_callback_handler_remove();
+ } elseif ($this->originalTtyMode !== null && is_resource(STDIN) && $this->isTty()) {
+ // Reset stty so it behaves normally again
+ shell_exec('stty ' . escapeshellarg($this->originalTtyMode));
+ $this->originalTtyMode = null;
+ }
+
+ // restore blocking mode so following programs behave normally
+ if (defined('STDIN') && is_resource(STDIN)) {
+ stream_set_blocking(STDIN, true);
+ }
+ }
+
+ /**
+ * @param ?LoopInterface $loop
+ * @return ReadableStreamInterface
+ * @codeCoverageIgnore this is covered by functional tests with/without ext-readline
+ */
+ private function createStdin(LoopInterface $loop = null)
+ {
+ // STDIN not defined ("php -a") or already closed (`fclose(STDIN)`)
+ // also support starting program with closed STDIN ("example.php 0<&-")
+ // the stream is a valid resource and is not EOF, but fstat fails
+ if (!defined('STDIN') || !is_resource(STDIN) || fstat(STDIN) === false) {
+ $stream = new ReadableResourceStream(fopen('php://memory', 'r'), $loop);
+ $stream->close();
+ return $stream;
+ }
+
+ $stream = new ReadableResourceStream(STDIN, $loop);
+
+ if (function_exists('readline_callback_handler_install')) {
+ // Prefer `ext-readline` to install dummy handler to turn on raw input mode.
+ // We will never actually feed the readline handler and instead
+ // handle all input in our `Readline` implementation.
+ readline_callback_handler_install('', function () { });
+ return $stream;
+ }
+
+ if ($this->isTty()) {
+ $this->originalTtyMode = rtrim(shell_exec('stty -g'), PHP_EOL);
+
+ // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
+ shell_exec('stty -icanon -echo');
+ }
+
+ // register shutdown function to restore TTY mode in case of unclean shutdown (uncaught exception)
+ // this will not trigger on SIGKILL etc., but the terminal should take care of this
+ register_shutdown_function(array($this, 'close'));
+
+ return $stream;
+ }
+
+ /**
+ * @param ?LoopInterface $loop
+ * @return WritableStreamInterface
+ * @codeCoverageIgnore this is covered by functional tests
+ */
+ private function createStdout(LoopInterface $loop = null)
+ {
+ // STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`)
+ // also support starting program with closed STDOUT ("example.php >&-")
+ // the stream is a valid resource and is not EOF, but fstat fails
+ if (!defined('STDOUT') || !is_resource(STDOUT) || fstat(STDOUT) === false) {
+ $output = new WritableResourceStream(fopen('php://memory', 'r+'), $loop);
+ $output->close();
+ } else {
+ $output = new WritableResourceStream(STDOUT, $loop);
+ }
+
+ return $output;
+ }
+
+ /**
+ * @return bool
+ * @codeCoverageIgnore
+ */
+ private function isTty()
+ {
+ if (PHP_VERSION_ID >= 70200) {
+ // Prefer `stream_isatty()` (available as of PHP 7.2 only)
+ return stream_isatty(STDIN);
+ } elseif (function_exists('posix_isatty')) {
+ // Otherwise use `posix_isatty` if available (requires `ext-posix`)
+ return posix_isatty(STDIN);
+ }
+
+ // otherwise try to guess based on stat file mode and device major number
+ // Must be special character device: ($mode & S_IFMT) === S_IFCHR
+ // And device major number must be allocated to TTYs (2-5 and 128-143)
+ // For what it's worth, checking for device gid 5 (tty) is less reliable.
+ // @link http://man7.org/linux/man-pages/man7/inode.7.html
+ // @link https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html#terminal-devices
+ $stat = fstat(STDIN);
+ $mode = isset($stat['mode']) ? ($stat['mode'] & 0170000) : 0;
+ $major = isset($stat['dev']) ? (($stat['dev'] >> 8) & 0xff) : 0;
+
+ return ($mode === 0020000 && $major >= 2 && $major <= 143 && ($major <=5 || $major >= 128));
+ }
+}