diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:38:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:38:42 +0000 |
commit | c3ca98e1b35123f226c7f4c596b5dee78caa4223 (patch) | |
tree | 9b6eb109283da55e7d9064baa9fac795a40264cb /vendor/clue/stdio-react | |
parent | Initial commit. (diff) | |
download | icinga-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/LICENSE | 21 | ||||
-rw-r--r-- | vendor/clue/stdio-react/composer.json | 37 | ||||
-rw-r--r-- | vendor/clue/stdio-react/src/Readline.php | 1017 | ||||
-rw-r--r-- | vendor/clue/stdio-react/src/Stdio.php | 630 |
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)); + } +} |