summaryrefslogtreecommitdiffstats
path: root/vendor/clue/stdio-react
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:38:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:38:42 +0000
commitc3ca98e1b35123f226c7f4c596b5dee78caa4223 (patch)
tree9b6eb109283da55e7d9064baa9fac795a40264cb /vendor/clue/stdio-react
parentInitial commit. (diff)
downloadicinga-php-thirdparty-c3ca98e1b35123f226c7f4c596b5dee78caa4223.tar.xz
icinga-php-thirdparty-c3ca98e1b35123f226c7f4c596b5dee78caa4223.zip
Adding upstream version 0.11.0.upstream/0.11.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/clue/stdio-react')
-rw-r--r--vendor/clue/stdio-react/LICENSE21
-rw-r--r--vendor/clue/stdio-react/composer.json37
-rw-r--r--vendor/clue/stdio-react/src/Readline.php1017
-rw-r--r--vendor/clue/stdio-react/src/Stdio.php630
4 files changed, 1705 insertions, 0 deletions
diff --git a/vendor/clue/stdio-react/LICENSE b/vendor/clue/stdio-react/LICENSE
new file mode 100644
index 0000000..da15612
--- /dev/null
+++ b/vendor/clue/stdio-react/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Christian Lück
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/clue/stdio-react/composer.json b/vendor/clue/stdio-react/composer.json
new file mode 100644
index 0000000..0e86dcb
--- /dev/null
+++ b/vendor/clue/stdio-react/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "clue/stdio-react",
+ "description": "Async, event-driven console input & output (STDIN, STDOUT) for truly interactive CLI applications, built on top of ReactPHP",
+ "keywords": ["stdio", "stdin", "stdout", "interactive", "CLI", "readline", "autocomplete", "autocompletion", "history", "ReactPHP", "async"],
+ "homepage": "https://github.com/clue/reactphp-stdio",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Christian Lück",
+ "email": "christian@clue.engineering"
+ }
+ ],
+ "require": {
+ "php": ">=5.3",
+ "clue/term-react": "^1.0 || ^0.1.1",
+ "clue/utf8-react": "^1.0 || ^0.1",
+ "react/event-loop": "^1.2",
+ "react/stream": "^1.2"
+ },
+ "require-dev": {
+ "clue/arguments": "^2.0",
+ "clue/commander": "^1.2",
+ "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
+ },
+ "suggest": {
+ "ext-mbstring": "Using ext-mbstring should provide slightly better performance for handling I/O"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": { "Clue\\React\\Stdio\\": "src/" }
+ },
+ "autoload-dev": {
+ "psr-4": { "Clue\\Tests\\React\\Stdio\\": "tests/" }
+ }
+}
diff --git a/vendor/clue/stdio-react/src/Readline.php b/vendor/clue/stdio-react/src/Readline.php
new file mode 100644
index 0000000..b75650e
--- /dev/null
+++ b/vendor/clue/stdio-react/src/Readline.php
@@ -0,0 +1,1017 @@
+<?php
+
+namespace Clue\React\Stdio;
+
+use Clue\React\Term\ControlCodeParser;
+use Clue\React\Utf8\Sequencer as Utf8Sequencer;
+use Evenement\EventEmitter;
+use Evenement\EventEmitterInterface;
+use React\Stream\ReadableStreamInterface;
+use React\Stream\Util;
+use React\Stream\WritableStreamInterface;
+
+/**
+ * @deprecated 2.3.0 Use `Stdio` instead
+ * @see Stdio
+ */
+class Readline extends EventEmitter implements ReadableStreamInterface
+{
+ private $prompt = '';
+ private $linebuffer = '';
+ private $linepos = 0;
+ private $echo = true;
+ private $move = true;
+ private $bell = true;
+ private $encoding = 'utf-8';
+
+ private $input;
+ private $output;
+ private $sequencer;
+ private $closed = false;
+
+ private $historyLines = array();
+ private $historyPosition = null;
+ private $historyUnsaved = null;
+ private $historyLimit = 500;
+
+ private $autocomplete = null;
+ private $autocompleteSuggestions = 8;
+
+ public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output, EventEmitterInterface $base = null)
+ {
+ $this->input = $input;
+ $this->output = $output;
+
+ if (!$this->input->isReadable()) {
+ $this->close();
+ return;
+ }
+ // push input through control code parser
+ $parser = new ControlCodeParser($input);
+
+ $that = $this;
+ $codes = array(
+ "\n" => 'onKeyEnter', // ^J
+ "\x7f" => 'onKeyBackspace', // ^?
+ "\t" => 'onKeyTab', // ^I
+ "\x04" => 'handleEnd', // ^D
+
+ "\033[A" => 'onKeyUp',
+ "\033[B" => 'onKeyDown',
+ "\033[C" => 'onKeyRight',
+ "\033[D" => 'onKeyLeft',
+
+ "\033[1~" => 'onKeyHome',
+// "\033[2~" => 'onKeyInsert',
+ "\033[3~" => 'onKeyDelete',
+ "\033[4~" => 'onKeyEnd',
+
+// "\033[20~" => 'onKeyF10',
+ );
+ $decode = function ($code) use ($codes, $that, $base) {
+ // The user confirms input with enter key which should usually
+ // generate a NL (`\n`) character. Common terminals also seem to
+ // accept a CR (`\r`) character in place and handle this just like a
+ // NL. Similarly `ext-readline` uses different `icrnl` and `igncr`
+ // TTY settings on some platforms, so we also accept CR as an alias
+ // for NL here. This implies key binding for NL will also trigger.
+ if ($code === "\r") {
+ $code = "\n";
+ }
+
+ // forward compatibility: check if any key binding exists on base Stdio instance
+ if ($base !== null && $base->listeners($code)) {
+ $base->emit($code, array($code));
+ return;
+ }
+
+ // deprecated: check if any key binding exists on this Readline instance
+ if ($that->listeners($code)) {
+ $that->emit($code, array($code));
+ return;
+ }
+
+ if (isset($codes[$code])) {
+ $method = $codes[$code];
+ $that->$method($code);
+ return;
+ }
+ };
+
+ $parser->on('csi', $decode);
+ $parser->on('c0', $decode);
+
+ // push resulting data through utf8 sequencer
+ $utf8 = new Utf8Sequencer($parser);
+ $utf8->on('data', function ($data) use ($that, $base) {
+ $that->onFallback($data, $base);
+ });
+
+ // process all stream events (forwarded from input stream)
+ $utf8->on('end', array($this, 'handleEnd'));
+ $utf8->on('error', array($this, 'handleError'));
+ $utf8->on('close', array($this, 'close'));
+ }
+
+ /**
+ * prompt to prepend to input line
+ *
+ * Will redraw the current input prompt with the current input buffer.
+ *
+ * @param string $prompt
+ * @return self
+ * @uses self::redraw()
+ * @deprecated use Stdio::setPrompt() instead
+ */
+ public function setPrompt($prompt)
+ {
+ if ($prompt === $this->prompt) {
+ return $this;
+ }
+
+ $this->prompt = $prompt;
+
+ return $this->redraw();
+ }
+
+ /**
+ * returns the prompt to prepend to input line
+ *
+ * @return string
+ * @see self::setPrompt()
+ * @deprecated use Stdio::getPrompt() instead
+ */
+ public function getPrompt()
+ {
+ return $this->prompt;
+ }
+
+ /**
+ * 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 self
+ * @uses self::redraw()
+ * @deprecated use Stdio::setEcho() instead
+ */
+ public function setEcho($echo)
+ {
+ if ($echo === $this->echo) {
+ return $this;
+ }
+
+ $this->echo = $echo;
+
+ // only redraw if there is any input
+ if ($this->linebuffer !== '') {
+ $this->redraw();
+ }
+
+ return $this;
+ }
+
+ /**
+ * 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 self
+ * @uses self::redraw()
+ * @deprecated use Stdio::setMove() instead
+ */
+ public function setMove($move)
+ {
+ $this->move = !!$move;
+
+ return $this->moveCursorTo($this->strlen($this->linebuffer));
+ }
+
+ /**
+ * 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
+ * @deprecated use Stdio::getCursorPosition() instead
+ */
+ public function getCursorPosition()
+ {
+ return $this->linepos;
+ }
+
+ /**
+ * 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()
+ * @deprecated use Stdio::getCursorCell() instead
+ */
+ public function getCursorCell()
+ {
+ if ($this->echo === false) {
+ return 0;
+ }
+ if ($this->echo !== true) {
+ return $this->strwidth($this->echo) * $this->linepos;
+ }
+ return $this->strwidth($this->substr($this->linebuffer, 0, $this->linepos));
+ }
+
+ /**
+ * 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 self
+ * @uses self::moveCursorTo()
+ * @uses self::redraw()
+ * @deprecated use Stdio::moveCursorBy() instead
+ */
+ public function moveCursorBy($n)
+ {
+ return $this->moveCursorTo($this->linepos + $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 self
+ * @uses self::redraw()
+ * @deprecated use Stdio::moveCursorTo() instead
+ */
+ public function moveCursorTo($n)
+ {
+ if ($n < 0 || $n === $this->linepos || $n > $this->strlen($this->linebuffer)) {
+ return $this;
+ }
+
+ $old = $this->getCursorCell();
+ $this->linepos = $n;
+
+ // only redraw if visible cell position change (implies cursor is actually visible)
+ if ($this->getCursorCell() !== $old) {
+ $this->redraw();
+ }
+
+ return $this;
+ }
+
+ /**
+ * 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 self
+ * @uses self::redraw()
+ * @deprecated use Stdio::addInput() instead
+ */
+ public function addInput($input)
+ {
+ if ($input === '') {
+ return $this;
+ }
+
+ // read everything up until before current position
+ $pre = $this->substr($this->linebuffer, 0, $this->linepos);
+ $post = $this->substr($this->linebuffer, $this->linepos);
+
+ $this->linebuffer = $pre . $input . $post;
+ $this->linepos += $this->strlen($input);
+
+ // only redraw if input should be echo'ed (i.e. is not hidden anyway)
+ if ($this->echo !== false) {
+ $this->redraw();
+ }
+
+ return $this;
+ }
+
+ /**
+ * set current text input buffer
+ *
+ * this moves the cursor to the end of the current
+ * input buffer (if any).
+ *
+ * @param string $input
+ * @return self
+ * @uses self::redraw()
+ * @deprecated use Stdio::setInput() instead
+ */
+ public function setInput($input)
+ {
+ if ($this->linebuffer === $input) {
+ return $this;
+ }
+
+ // remember old input length if echo replacement is used
+ $oldlen = (is_string($this->echo)) ? $this->strlen($this->linebuffer) : null;
+
+ $this->linebuffer = $input;
+ $this->linepos = $this->strlen($this->linebuffer);
+
+ // only redraw if input should be echo'ed (i.e. is not hidden anyway)
+ // and echo replacement is used, make sure the input length changes
+ if ($this->echo !== false && $this->linepos !== $oldlen) {
+ $this->redraw();
+ }
+
+ return $this;
+ }
+
+ /**
+ * get current text input buffer
+ *
+ * @return string
+ * @deprecated use Stdio::getInput() instead
+ */
+ public function getInput()
+ {
+ return $this->linebuffer;
+ }
+
+ /**
+ * Adds a new line to the (bottom position of the) history list
+ *
+ * @param string $line
+ * @return self
+ * @uses self::limitHistory() to make sure list does not exceed limits
+ * @deprecated use Stdio::addHistory() instead
+ */
+ public function addHistory($line)
+ {
+ $this->historyLines []= $line;
+
+ return $this->limitHistory($this->historyLimit);
+ }
+
+ /**
+ * Clears the complete history list
+ *
+ * @return self
+ * @deprecated use Stdio::clearHistory() instead
+ */
+ public function clearHistory()
+ {
+ $this->historyLines = array();
+ $this->historyPosition = null;
+
+ if ($this->historyUnsaved !== null) {
+ $this->setInput($this->historyUnsaved);
+ $this->historyUnsaved = null;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns an array with all lines in the history
+ *
+ * @return string[]
+ * @deprecated use Stdio::listHistory() instead
+ */
+ public function listHistory()
+ {
+ return $this->historyLines;
+ }
+
+ /**
+ * Limits the history to a maximum of N entries and truncates the current history list accordingly
+ *
+ * @param int|null $limit
+ * @return self
+ * @deprecated use Stdio::limitHistory() instead
+ */
+ public function limitHistory($limit)
+ {
+ $this->historyLimit = $limit === null ? null : $limit;
+
+ // limit send and currently exceeded
+ if ($this->historyLimit !== null && isset($this->historyLines[$this->historyLimit])) {
+ // adjust position in history according to new position after applying limit
+ if ($this->historyPosition !== null) {
+ $this->historyPosition -= count($this->historyLines) - $this->historyLimit;
+
+ // current position will drop off from list => restore original
+ if ($this->historyPosition < 0) {
+ $this->setInput($this->historyUnsaved);
+ $this->historyPosition = null;
+ $this->historyUnsaved = null;
+ }
+ }
+
+ $this->historyLines = array_slice($this->historyLines, -$this->historyLimit, $this->historyLimit);
+ }
+
+ return $this;
+ }
+
+ /**
+ * set autocompletion handler to use
+ *
+ * The autocomplete handler will be called whenever the user hits the TAB
+ * key.
+ *
+ * @param callable|null $autocomplete
+ * @return self
+ * @throws \InvalidArgumentException if the given callable is invalid
+ * @deprecated use Stdio::setAutocomplete() instead
+ */
+ public function setAutocomplete($autocomplete)
+ {
+ if ($autocomplete !== null && !is_callable($autocomplete)) {
+ throw new \InvalidArgumentException('Invalid autocomplete function given');
+ }
+
+ $this->autocomplete = $autocomplete;
+
+ return $this;
+ }
+
+ /**
+ * 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
+ * @internal use Stdio::setBell() instead
+ */
+ public function setBell($bell)
+ {
+ $this->bell = (bool)$bell;
+ }
+
+ /**
+ * redraw the current input prompt
+ *
+ * Usually, there should be no need to call this method manually. It will
+ * be invoked automatically whenever we detect the readline input needs to
+ * be (re)written to the output.
+ *
+ * Clear the current line and draw the input prompt. If input echo is
+ * enabled, will also draw the current input buffer and move to the current
+ * input buffer position.
+ *
+ * @return self
+ * @internal
+ */
+ public function redraw()
+ {
+ // Erase characters from cursor to end of line and then redraw actual input
+ $this->output->write("\r\033[K" . $this->getDrawString());
+
+ return $this;
+ }
+
+ /**
+ * Returns the string that is used to draw the input prompt
+ *
+ * @return string
+ * @internal
+ */
+ public function getDrawString()
+ {
+ $output = $this->prompt;
+ if ($this->echo !== false) {
+ if ($this->echo === true) {
+ $buffer = $this->linebuffer;
+ } else {
+ $buffer = str_repeat($this->echo, $this->strlen($this->linebuffer));
+ }
+
+ // write output, then move back $reverse chars (by sending backspace)
+ $output .= $buffer . str_repeat("\x08", $this->strwidth($buffer) - $this->getCursorCell());
+ }
+
+ return $output;
+ }
+
+ /** @internal */
+ public function onKeyBackspace()
+ {
+ // left delete only if not at the beginning
+ if ($this->linepos === 0) {
+ $this->bell();
+ } else {
+ $this->deleteChar($this->linepos - 1);
+ }
+ }
+
+ /** @internal */
+ public function onKeyDelete()
+ {
+ // right delete only if not at the end
+ if ($this->isEol()) {
+ $this->bell();
+ } else {
+ $this->deleteChar($this->linepos);
+ }
+ }
+
+ /** @internal */
+ public function onKeyHome()
+ {
+ if ($this->move && $this->linepos !== 0) {
+ $this->moveCursorTo(0);
+ } else {
+ $this->bell();
+ }
+ }
+
+ /** @internal */
+ public function onKeyEnd()
+ {
+ if ($this->move && !$this->isEol()) {
+ $this->moveCursorTo($this->strlen($this->linebuffer));
+ } else {
+ $this->bell();
+ }
+ }
+
+ /** @internal */
+ public function onKeyTab()
+ {
+ if ($this->autocomplete === null) {
+ $this->bell();
+ return;
+ }
+
+ // current word prefix and offset for start of word in input buffer
+ // "echo foo|bar world" will return just "foo" with word offset 5
+ $word = $this->substr($this->linebuffer, 0, $this->linepos);
+ $start = 0;
+ $end = $this->linepos;
+
+ // buffer prefix and postfix for everything that will *not* be matched
+ // above example will return "echo " and "bar world"
+ $prefix = '';
+ $postfix = $this->substr($this->linebuffer, $this->linepos);
+
+ // skip everything before last space
+ $pos = strrpos($word, ' ');
+ if ($pos !== false) {
+ $prefix = (string)substr($word, 0, $pos + 1);
+ $word = (string)substr($word, $pos + 1);
+ $start = $this->strlen($prefix);
+ }
+
+ // skip double quote (") or single quote (') from argument
+ $quote = null;
+ if (isset($word[0]) && ($word[0] === '"' || $word[0] === '\'')) {
+ $quote = $word[0];
+ ++$start;
+ $prefix .= $word[0];
+ $word = (string)substr($word, 1);
+ }
+
+ // invoke autocomplete callback
+ $words = call_user_func($this->autocomplete, $word, $start, $end);
+
+ // return early if autocomplete does not return anything
+ if ($words === null) {
+ return;
+ }
+
+ // remove from list of possible words that do not start with $word or are duplicates
+ $words = array_unique($words);
+ if ($word !== '' && $words) {
+ $words = array_filter($words, function ($w) use ($word) {
+ return strpos($w, $word) === 0;
+ });
+ }
+
+ // return if neither of the possible words match
+ if (!$words) {
+ $this->bell();
+ return;
+ }
+
+ // search longest common prefix among all possible matches
+ $found = reset($words);
+ $all = count($words);
+ if ($all > 1) {
+ while ($found !== '') {
+ // count all words that start with $found
+ $matches = count(array_filter($words, function ($w) use ($found) {
+ return strpos($w, $found) === 0;
+ }));
+
+ // ALL words match $found => common substring found
+ if ($all === $matches) {
+ break;
+ }
+
+ // remove last letter from $found and try again
+ $found = $this->substr($found, 0, -1);
+ }
+
+ // found more than one possible match with this prefix => print options
+ if ($found === $word || $found === '') {
+ // limit number of possible matches
+ if (count($words) > $this->autocompleteSuggestions) {
+ $more = count($words) - ($this->autocompleteSuggestions - 1);
+ $words = array_slice($words, 0, $this->autocompleteSuggestions - 1);
+ $words []= '(+' . $more . ' others)';
+ }
+
+ $this->output->write("\n" . implode(' ', $words) . "\n");
+ $this->redraw();
+
+ return;
+ }
+ }
+
+ if ($quote !== null && $all === 1 && (strpos($postfix, $quote) === false || strpos($postfix, $quote) > strpos($postfix, ' '))) {
+ // add closing quote if word started in quotes and postfix does not already contain closing quote before next space
+ $found .= $quote;
+ } elseif ($found === '') {
+ // add single quotes around empty match
+ $found = '\'\'';
+ }
+
+ if ($postfix === '' && $all === 1) {
+ // append single space after match unless there's a postfix or there are multiple completions
+ $found .= ' ';
+ }
+
+ // replace word in input with best match and adjust cursor
+ $this->linebuffer = $prefix . $found . $postfix;
+ $this->moveCursorBy($this->strlen($found) - $this->strlen($word));
+ }
+
+ /** @internal */
+ public function onKeyEnter()
+ {
+ if ($this->echo !== false) {
+ $this->output->write("\n");
+ }
+ $this->processLine("\n");
+ }
+
+ /** @internal */
+ public function onKeyLeft()
+ {
+ if ($this->move && $this->linepos !== 0) {
+ $this->moveCursorBy(-1);
+ } else {
+ $this->bell();
+ }
+ }
+
+ /** @internal */
+ public function onKeyRight()
+ {
+ if ($this->move && !$this->isEol()) {
+ $this->moveCursorBy(1);
+ } else {
+ $this->bell();
+ }
+ }
+
+ /** @internal */
+ public function onKeyUp()
+ {
+ // ignore if already at top or history is empty
+ if ($this->historyPosition === 0 || !$this->historyLines) {
+ $this->bell();
+ return;
+ }
+
+ if ($this->historyPosition === null) {
+ // first time up => move to last entry
+ $this->historyPosition = count($this->historyLines) - 1;
+ $this->historyUnsaved = $this->getInput();
+ } else {
+ // somewhere in the list => move by one
+ $this->historyPosition--;
+ }
+
+ $this->setInput($this->historyLines[$this->historyPosition]);
+ }
+
+ /** @internal */
+ public function onKeyDown()
+ {
+ // ignore if not currently cycling through history
+ if ($this->historyPosition === null) {
+ $this->bell();
+ return;
+ }
+
+ if (isset($this->historyLines[$this->historyPosition + 1])) {
+ // this is still a valid position => advance by one and apply
+ $this->historyPosition++;
+ $this->setInput($this->historyLines[$this->historyPosition]);
+ } else {
+ // moved beyond bottom => restore original unsaved input
+ $this->setInput($this->historyUnsaved);
+ $this->historyPosition = null;
+ $this->historyUnsaved = null;
+ }
+ }
+
+ /**
+ * Will be invoked for character(s) that could not otherwise be processed by the sequencer
+ *
+ * @internal
+ */
+ public function onFallback($chars, EventEmitterInterface $base = null)
+ {
+ // check if there's any special key binding for any of the chars
+ $buffer = '';
+ foreach ($this->strsplit($chars) as $char) {
+ // forward compatibility: check if any key binding exists on base Stdio instance
+ // deprecated: check if any key binding exists on this Readline instance
+ $emit = null;
+ if ($base !== null && $base->listeners($char)) {
+ $emit = $base;
+ } else if ($this->listeners($char)) {
+ $emit = $this;
+ }
+
+ if ($emit !== null) {
+ // special key binding for this character found
+ // process all characters before this one before invoking function
+ if ($buffer !== '') {
+ $this->addInput($buffer);
+ $buffer = '';
+ }
+ $emit->emit($char, array($char));
+ } else {
+ $buffer .= $char;
+ }
+ }
+
+ // process remaining input characters after last special key binding
+ if ($buffer !== '') {
+ $this->addInput($buffer);
+ }
+ }
+
+ /**
+ * delete a character at the given position
+ *
+ * Removing a character left to the current cursor will also move the cursor
+ * to the left.
+ *
+ * @param int $n
+ */
+ private function deleteChar($n)
+ {
+ // read everything up until before current position
+ $pre = $this->substr($this->linebuffer, 0, $n);
+ $post = $this->substr($this->linebuffer, $n + 1);
+
+ $this->linebuffer = $pre . $post;
+
+ // move cursor one cell to the left if we're deleting in front of the cursor
+ if ($n < $this->linepos) {
+ --$this->linepos;
+ }
+
+ $this->redraw();
+ }
+
+ /**
+ * process the current line buffer, emit event and redraw empty line
+ *
+ * @uses self::setInput()
+ */
+ protected function processLine($eol)
+ {
+ // reset history cycle position
+ $this->historyPosition = null;
+ $this->historyUnsaved = null;
+
+ // store and reset/clear/redraw current input
+ $line = $this->linebuffer;
+ if ($line !== '') {
+ // the line is not empty, reset it (and implicitly redraw prompt)
+ $this->setInput('');
+ } elseif ($this->echo !== false) {
+ // explicitly redraw prompt after empty line
+ $this->redraw();
+ }
+
+ // process stored input buffer
+ $this->emit('data', array($line . $eol));
+ }
+
+ /**
+ * @param string $str
+ * @return int
+ * @codeCoverageIgnore
+ */
+ private function strlen($str)
+ {
+ // prefer mb_strlen() if available
+ if (function_exists('mb_strlen')) {
+ return mb_strlen($str, $this->encoding);
+ }
+
+ // otherwise replace all unicode chars with dots and count dots
+ return strlen(preg_replace('/./us', '.', $str));
+ }
+
+ /**
+ * @param string $str
+ * @param int $start
+ * @param ?int $len
+ * @return string
+ * @codeCoverageIgnore
+ */
+ private function substr($str, $start = 0, $len = null)
+ {
+ if ($len === null) {
+ $len = $this->strlen($str) - $start;
+ }
+
+ // prefer mb_substr() if available
+ if (function_exists('mb_substr')) {
+ return (string)mb_substr($str, $start, $len, $this->encoding);
+ }
+
+ // otherwise build array with all unicode chars and slice array
+ preg_match_all('/./us', $str, $matches);
+
+ return implode('', array_slice($matches[0], $start, $len));
+ }
+
+ /**
+ * @internal
+ * @param string $str
+ * @return int
+ * @codeCoverageIgnore
+ */
+ public function strwidth($str)
+ {
+ // prefer mb_strwidth() if available
+ if (function_exists('mb_strwidth')) {
+ return mb_strwidth($str, $this->encoding);
+ }
+
+ // otherwise replace each double-width unicode graphemes with two dots, all others with single dot and count number of dots
+ // mbstring's list of double-width graphemes is *very* long: https://3v4l.org/GEg3u
+ // let's use symfony's list from https://github.com/symfony/polyfill-mbstring/blob/e79d363049d1c2128f133a2667e4f4190904f7f4/Mbstring.php#L523
+ // which looks like they originally came from http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
+ return strlen(preg_replace(
+ array(
+ '/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u',
+ '/./us',
+ ),
+ array(
+ '..',
+ '.',
+ ),
+ $str
+ ));
+ }
+
+ /**
+ * @param string $str
+ * @return string[]
+ */
+ private function strsplit($str)
+ {
+ return preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ /**
+ * @return bool
+ */
+ private function isEol()
+ {
+ return $this->linepos === $this->strlen($this->linebuffer);
+ }
+
+ /**
+ * @return void
+ */
+ private function bell()
+ {
+ if ($this->bell) {
+ $this->output->write("\x07"); // BEL a.k.a. \a
+ }
+ }
+
+ /** @internal */
+ public function handleEnd()
+ {
+ if ($this->linebuffer !== '') {
+ $this->processLine('');
+ }
+
+ if (!$this->closed) {
+ $this->emit('end');
+ $this->close();
+ }
+ }
+
+ /** @internal */
+ public function handleError(\Exception $error)
+ {
+ $this->emit('error', array($error));
+ $this->close();
+ }
+
+ public function isReadable()
+ {
+ return !$this->closed && $this->input->isReadable();
+ }
+
+ public function pause()
+ {
+ $this->input->pause();
+ }
+
+ public function resume()
+ {
+ $this->input->resume();
+ }
+
+ public function pipe(WritableStreamInterface $dest, array $options = array())
+ {
+ Util::pipe($this, $dest, $options);
+
+ return $dest;
+ }
+
+ public function close()
+ {
+ if ($this->closed) {
+ return;
+ }
+
+ $this->closed = true;
+
+ $this->input->close();
+
+ $this->emit('close');
+ $this->removeAllListeners();
+ }
+}
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));
+ }
+}