diff --git a/README.md b/README.md index db20c84..d1517f7 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ without requiring any extensions or special installation. * [Stdio](#stdio) * [Output](#output) * [Input](#input) - * [Readline](#readline) * [Prompt](#prompt) * [Echo](#echo) * [Input buffer](#input-buffer) @@ -25,6 +24,7 @@ without requiring any extensions or special installation. * [History](#history) * [Autocomplete](#autocomplete) * [Keys](#keys) + * [Readline](#readline) * [Pitfalls](#pitfalls) * [Install](#install) * [Tests](#tests) @@ -39,7 +39,7 @@ Once [installed](#install), you can use the following code to present a prompt i $loop = React\EventLoop\Factory::create(); $stdio = new Stdio($loop); -$stdio->getReadline()->setPrompt('Input > '); +$stdio->setPrompt('Input > '); $stdio->on('data', function ($line) use ($stdio) { $line = rtrim($line, "\r\n"); @@ -135,34 +135,13 @@ Because the `Stdio` is a well-behaving readable stream that will emit incoming data as-is, you can also use this to `pipe()` this stream into other writable streams. -``` +```php $stdio->pipe($logger); ``` -You can control various aspects of the console input through the [`Readline`](#readline), +You can control various aspects of the console input through this interface, so read on.. -### Readline - -The [`Readline`](#readline) class is responsible for reacting to user input and presenting a prompt to the user. -It does so by reading individual bytes from the input stream and writing the current *user input line* to the output stream. - -The *user input line* consists of a *prompt*, following by the current *user input buffer*. -The `Readline` allows you to control various aspects of this *user input line*. - -You can access the current instance through the [`Stdio`](#stdio): - -```php -$readline = $stdio->getReadline(); -``` - -See above for waiting for user input. - -Alternatively, the `Readline` is also a well-behaving readable stream -(implementing ReactPHP's `ReadableStreamInterface`) that emits each complete -line as a `data` event, including the trailing newline. -This is considered advanced usage. - #### Prompt The *prompt* will be written at the beginning of the *user input line*, right before the *user input buffer*. @@ -171,21 +150,21 @@ The `setPrompt($prompt)` method can be used to change the input prompt. The prompt will be printed to the *user input line* as-is, so you will likely want to end this with a space: ```php -$readline->setPrompt('Input: '); +$stdio->setPrompt('Input: '); ``` The default input prompt is empty, i.e. the *user input line* contains only the actual *user input buffer*. You can restore this behavior by passing an empty prompt: ```php -$readline->setPrompt(''); +$stdio->setPrompt(''); ``` The `getPrompt()` method can be used to get the current input prompt. It will return an empty string unless you've set anything else: ```php -assert($readline->getPrompt() === ''); +assert($stdio->getPrompt() === ''); ``` #### Echo @@ -201,7 +180,7 @@ Please note that this often leads to a bad user experience as users will not eve Simply pass a boolean `false` like this: ```php -$readline->setEcho(false); +$stdio->setEcho(false); ``` Alternatively, you can also *hide* the *user input buffer* by using a replacement character. @@ -211,13 +190,13 @@ This often provides a better user experience and allows users to still control t Simply pass a string replacement character likes this: ```php -$readline->setEcho('*'); +$stdio->setEcho('*'); ``` To restore the original behavior where every character appears as-is, simply pass a boolean `true`: ```php -$readline->setEcho(true); +$stdio->setEcho(true); ``` #### Input buffer @@ -235,7 +214,7 @@ the user (like the last password attempt). Simply pass an input string like this: ```php -$readline->addInput('hello'); +$stdio->addInput('hello'); ``` The `setInput($buffer)` method can be used to control the *user input buffer*. @@ -247,7 +226,7 @@ the user (like the last password attempt). Simply pass an input string like this: ```php -$readline->setInput('lastpass'); +$stdio->setInput('lastpass'); ``` The `getInput()` method can be used to access the current *user input buffer*. @@ -255,7 +234,7 @@ This can be useful if you want to append some input behind the current *user inp You can simply access the buffer like this: ```php -$buffer = $readline->getInput(); +$buffer = $stdio->getInput(); ``` #### Cursor @@ -267,14 +246,14 @@ The `setMove($toggle)` method can be used to control whether users are allowed t To disable the left and right arrow keys, simply pass a boolean `false` like this: ```php -$readline->setMove(false); +$stdio->setMove(false); ``` To restore the default behavior where the user can use the left and right arrow keys, simply pass a boolean `true` like this: ```php -$readline->setMove(true); +$stdio->setMove(true); ``` The `getCursorPosition()` method can be used to access the current cursor position, @@ -283,7 +262,7 @@ This can be useful if you want to get a substring of the current *user input buf Simply invoke it like this: ```php -$position = $readline->getCursorPosition(); +$position = $stdio->getCursorPosition(); ``` The `getCursorCell()` method can be used to get the current cursor position, @@ -296,14 +275,14 @@ This method is mostly useful for calculating the visual cursor position on scree but you may also invoke it like this: ```php -$cell = $readline->getCursorCell(); +$cell = $stdio->getCursorCell(); ``` The `moveCursorTo($position)` method can be used to set the current cursor position to the given absolute character position. For example, to move the cursor to the beginning of the *user input buffer*, simply call: ```php -$readline->moveCursorTo(0); +$stdio->moveCursorTo(0); ``` The `moveCursorBy($offset)` method can be used to change the cursor position @@ -312,7 +291,7 @@ A positive number will move the cursor to the right - a negative number will mov For example, to move the cursor one character to the left, simply call: ```php -$readline->moveCursorBy(-1); +$stdio->moveCursorBy(-1); ``` #### History @@ -331,13 +310,13 @@ If you want to automatically add everything from the user input to the history, you may want to use something like this: ```php -$stdio->on('data', function ($line) use ($readline) { +$stdio->on('data', function ($line) use ($stdio) { $line = rtrim($line); - $all = $readline->listHistory(); + $all = $stdio->listHistory(); // skip empty line and duplicate of previous line if ($line !== '' && $line !== end($all)) { - $readline->addHistory($line); + $stdio->addHistory($line); } }); ``` @@ -347,24 +326,24 @@ return an array with all lines in the history. This will be an empty array until you add new entries via `addHistory()`. ```php -$list = $readline->listHistory(); +$list = $stdio->listHistory(); assert(count($list) === 0); ``` -The `addHistory(string $line): Readline` method can be used to +The `addHistory(string $line): void` method can be used to add a new line to the (bottom position of the) history list. A following `listHistory()` call will return this line as the last element. ```php -$readline->addHistory('a'); -$readline->addHistory('b'); +$stdio->addHistory('a'); +$stdio->addHistory('b'); -$list = $readline->listHistory(); +$list = $stdio->listHistory(); assert($list === array('a', 'b')); ``` -The `clearHistory(): Readline` method can be used to +The `clearHistory(): void` method can be used to clear the complete history list. A following `listHistory()` call will return an empty array until you add new entries via `addHistory()` again. @@ -372,13 +351,13 @@ Note that the history feature will effectively be disabled if the history is empty, as the UP and DOWN cursor keys have no function then. ```php -$readline->clearHistory(); +$stdio->clearHistory(); -$list = $readline->listHistory(); +$list = $stdio->listHistory(); assert(count($list) === 0); ``` -The `limitHistory(?int $limit): Readline` method can be used to +The `limitHistory(?int $limit): void` method can be used to set a limit of history lines to keep in memory. By default, only the last 500 lines will be kept in memory and everything else will be discarded. @@ -394,10 +373,10 @@ this to obey the `HISTSIZE` environment variable: $limit = getenv('HISTSIZE'); if ($limit === '' || $limit < 0) { // empty string or negative value means unlimited - $readline->limitHistory(null); + $stdio->limitHistory(null); } elseif ($limit !== false) { // apply any other value if given - $readline->limitHistory($limit); + $stdio->limitHistory($limit); } ``` @@ -415,13 +394,13 @@ By default, users can use autocompletion by using their TAB keys on the keyboard The autocomplete function is not registered by default, thus this feature is effectively disabled, as the TAB key has no function then. -The `setAutocomplete(?callable $autocomplete): Readline` method can be used to +The `setAutocomplete(?callable $autocomplete): void` method can be used to register a new autocomplete handler. In its most simple form, you won't need to assign any arguments and can simply return an array of possible word matches from a callable like this: ```php -$readline->setAutocomplete(function () { +$stdio->setAutocomplete(function () { return array( 'exit', 'echo', @@ -464,7 +443,7 @@ is an argument or a root command and the `$word` argument to autocomplete partial filename matches like this: ```php -$readline->setAutocomplete(function ($word, $offset) { +$stdio->setAutocomplete(function ($word, $offset) { if ($offset <= 1) { // autocomplete root commands at offset=0/1 only return array('cat', 'rm', 'stat'); @@ -486,10 +465,10 @@ and/or manipulate the [input buffer](#input-buffer) and [cursor](#cursor) directly like this: ```php -$readline->setAutocomplete(function () use ($readline) { - if ($readline->getInput() === 'run') { - $readline->setInput('run --test --value=42'); - $readline->moveCursorBy(-2); +$stdio->setAutocomplete(function () use ($stdio) { + if ($stdio->getInput() === 'run') { + $stdio->setInput('run --test --value=42'); + $stdio->moveCursorBy(-2); } // return empty array so normal autocompletion doesn't kick in @@ -501,7 +480,7 @@ You can use a `null` value to remove the autocomplete function again and thus disable the autocomplete function: ```php -$readline->setAutocomplete(null); +$stdio->setAutocomplete(null); ``` #### Keys @@ -527,7 +506,7 @@ For example, you can use the following code to print some help text when the user hits a certain key: ```php -$readline->on('?', function () use ($stdio) { +$stdio->on('?', function () use ($stdio) { $stdio->write('Here\'s some help: …' . PHP_EOL); }); ``` @@ -536,8 +515,8 @@ Similarly, this can be used to manipulate the user input and replace some of the input when the user hits a certain key: ```php -$readline->on('ä', function () use ($readline) { - $readline->addInput('a'); +$stdio->on('ä', function () use ($stdio) { + $stdio->addInput('a'); }); ``` @@ -550,14 +529,47 @@ For example, the following code can be used to register a custom function to the UP arrow cursor key: ```php -$readline->on("\033[A", function () use ($readline) { - $readline->setInput(strtoupper($readline->getInput())); +$stdio->on("\033[A", function () use ($stdio) { + $stdio->setInput(strtoupper($stdio->getInput())); }); ``` +### Readline + +The deprecated `Readline` class is responsible for reacting to user input and +presenting a prompt to the user. It does so by reading individual bytes from the +input stream and writing the current *user input line* to the output stream. + +The deprecated `Readline` class is only used internally and should no longer be +referenced from consuming projects. + +You can access the current instance through the [`Stdio`](#stdio): + +```php +// deprecated +$readline = $stdio->getReadline(); +``` + +All methods that are available on the `Readline` instance are now available on +the `Stdio` class. For BC reasons, they remain available on the `Readline` class +until the next major release, see also above for more details. + +```php +// deprecated +$readline->setPrompt('> '); + +// new +$stdio->setPrompt('> '); +``` + +Internally, the `Readline` is also a well-behaving readable stream +(implementing ReactPHP's `ReadableStreamInterface`) that emits each complete +line as a `data` event, including the trailing newline. +This is considered advanced usage. + ## Pitfalls -The [`Readline`](#readline) has to redraw the current user +The [`Stdio`](#stdio) has to redraw the current user input line whenever output is written to the `STDOUT`. Because of this, it is important to make sure any output is always written like this instead of using `echo` statements: diff --git a/examples/02-interactive.php b/examples/02-interactive.php index 642b11e..b0ca923 100644 --- a/examples/02-interactive.php +++ b/examples/02-interactive.php @@ -7,36 +7,35 @@ $loop = React\EventLoop\Factory::create(); $stdio = new Stdio($loop); -$readline = $stdio->getReadline(); -$readline->setPrompt('> '); +$stdio->setPrompt('> '); // limit history to HISTSIZE env $limit = getenv('HISTSIZE'); if ($limit === '' || $limit < 0) { // empty string or negative value means unlimited - $readline->limitHistory(null); + $stdio->limitHistory(null); } elseif ($limit !== false) { // apply any other value if given - $readline->limitHistory($limit); + $stdio->limitHistory($limit); } // autocomplete the following commands (at offset=0/1 only) -$readline->setAutocomplete(function ($_, $offset) { +$stdio->setAutocomplete(function ($_, $offset) { return $offset > 1 ? array() : array('exit', 'quit', 'help', 'echo', 'print', 'printf'); }); $stdio->write('Welcome to this interactive demo' . PHP_EOL); // react to commands the user entered -$stdio->on('data', function ($line) use ($stdio, $readline) { +$stdio->on('data', function ($line) use ($stdio) { $line = rtrim($line, "\r\n"); // add all lines from input to history // skip empty line and duplicate of previous line - $all = $readline->listHistory(); + $all = $stdio->listHistory(); if ($line !== '' && $line !== end($all)) { - $readline->addHistory($line); + $stdio->addHistory($line); } $stdio->write('you just said: ' . $line . ' (' . strlen($line) . ')' . PHP_EOL); diff --git a/examples/03-commander.php b/examples/03-commander.php index 8aa2c2e..33a9f8c 100644 --- a/examples/03-commander.php +++ b/examples/03-commander.php @@ -10,17 +10,16 @@ $loop = React\EventLoop\Factory::create(); $stdio = new Stdio($loop); -$readline = $stdio->getReadline(); -$readline->setPrompt('> '); +$stdio->setPrompt('> '); // limit history to HISTSIZE env $limit = getenv('HISTSIZE'); if ($limit === '' || $limit < 0) { // empty string or negative value means unlimited - $readline->limitHistory(null); + $stdio->limitHistory(null); } elseif ($limit !== false) { // apply any other value if given - $readline->limitHistory($limit); + $stdio->limitHistory($limit); } // register all available commands and their arguments @@ -39,21 +38,21 @@ }); // autocomplete the following commands (at offset=0/1 only) -$readline->setAutocomplete(function ($_, $offset) { +$stdio->setAutocomplete(function ($_, $offset) { return $offset > 1 ? array() : array('exit', 'quit', 'help', 'echo', 'print', 'printf'); }); $stdio->write('Welcome to this interactive demo' . PHP_EOL); // react to commands the user entered -$stdio->on('data', function ($line) use ($router, $stdio, $readline) { +$stdio->on('data', function ($line) use ($router, $stdio) { $line = rtrim($line, "\r\n"); // add all lines from input to history // skip empty line and duplicate of previous line - $all = $readline->listHistory(); + $all = $stdio->listHistory(); if ($line !== '' && $line !== end($all)) { - $readline->addHistory($line); + $stdio->addHistory($line); } try { diff --git a/examples/04-bindings.php b/examples/04-bindings.php index 9262546..3b1bd53 100644 --- a/examples/04-bindings.php +++ b/examples/04-bindings.php @@ -7,38 +7,36 @@ $loop = React\EventLoop\Factory::create(); $stdio = new Stdio($loop); -$readline = $stdio->getReadline(); - -$readline->setPrompt('> '); +$stdio->setPrompt('> '); // add some special key bindings -$readline->on('a', function () use ($readline) { - $readline->addInput('ä'); +$stdio->on('a', function () use ($stdio) { + $stdio->addInput('ä'); }); -$readline->on('o', function () use ($readline) { - $readline->addInput('ö'); +$stdio->on('o', function () use ($stdio) { + $stdio->addInput('ö'); }); -$readline->on('u', function () use ($readline) { - $readline->addInput('ü'); +$stdio->on('u', function () use ($stdio) { + $stdio->addInput('ü'); }); -$readline->on('?', function () use ($stdio) { +$stdio->on('?', function () use ($stdio) { $stdio->write('Do you need help?'); }); // bind CTRL+E -$readline->on("\x05", function () use ($stdio) { +$stdio->on("\x05", function () use ($stdio) { $stdio->write("ignore CTRL+E" . PHP_EOL); }); // bind CTRL+H -$readline->on("\x08", function () use ($stdio) { +$stdio->on("\x08", function () use ($stdio) { $stdio->write('Use "?" if you need help.' . PHP_EOL); }); $stdio->write('Welcome to this interactive demo' . PHP_EOL); // end once the user enters a command -$stdio->on('data', function ($line) use ($stdio, $readline) { +$stdio->on('data', function ($line) use ($stdio) { $line = rtrim($line, "\r\n"); $stdio->end('you just said: ' . $line . ' (' . strlen($line) . ')' . PHP_EOL); }); diff --git a/examples/05-cursor.php b/examples/05-cursor.php index 08da168..5362b8b 100644 --- a/examples/05-cursor.php +++ b/examples/05-cursor.php @@ -7,33 +7,32 @@ $loop = React\EventLoop\Factory::create(); $stdio = new Stdio($loop); -$readline = $stdio->getReadline(); $value = 10; -$readline->on("\033[A", function () use (&$value, $readline) { +$stdio->on("\033[A", function () use (&$value, $stdio) { $value++; - $readline->setPrompt('Value: ' . $value); + $stdio->setPrompt('Value: ' . $value); }); -$readline->on("\033[B", function () use (&$value, $readline) { +$stdio->on("\033[B", function () use (&$value, $stdio) { --$value; - $readline->setPrompt('Value: ' . $value); + $stdio->setPrompt('Value: ' . $value); }); // hijack enter to just print our current value -$readline->on("\n", function () use ($readline, $stdio, &$value) { +$stdio->on("\n", function () use ($stdio, &$value) { $stdio->write("Your choice was $value\n"); }); // quit on "q" -$readline->on('q', function () use ($stdio) { +$stdio->on('q', function () use ($stdio) { $stdio->end(); }); // user can still type all keys, but we simply hide user input -$readline->setEcho(false); +$stdio->setEcho(false); // instead of showing user input, we just show a custom prompt -$readline->setPrompt('Value: ' . $value); +$stdio->setPrompt('Value: ' . $value); $stdio->write('Welcome to this cursor demo diff --git a/examples/11-login.php b/examples/11-login.php index 271aece..88bced8 100644 --- a/examples/11-login.php +++ b/examples/11-login.php @@ -7,8 +7,7 @@ $loop = React\EventLoop\Factory::create(); $stdio = new Stdio($loop); - -$stdio->getReadline()->setPrompt('Username: '); +$stdio->setPrompt('Username: '); $first = true; $username = null; @@ -17,8 +16,8 @@ $stdio->on('data', function ($line) use ($stdio, &$first, &$username, &$password) { $line = rtrim($line, "\r\n"); if ($first) { - $stdio->getReadline()->setPrompt('Password: '); - $stdio->getReadline()->setEcho('*'); + $stdio->setPrompt('Password: '); + $stdio->setEcho('*'); $username = $line; $first = false; } else { diff --git a/src/Readline.php b/src/Readline.php index 42139e3..b992bee 100644 --- a/src/Readline.php +++ b/src/Readline.php @@ -5,10 +5,15 @@ 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 use Stdio instead + * @see Stdio + */ class Readline extends EventEmitter implements ReadableStreamInterface { private $prompt = ''; @@ -31,7 +36,7 @@ class Readline extends EventEmitter implements ReadableStreamInterface private $autocomplete = null; private $autocompleteSuggestions = 8; - public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output) + public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output, EventEmitterInterface $base = null) { $this->input = $input; $this->output = $output; @@ -62,7 +67,7 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf // "\033[20~" => 'onKeyF10', ); - $decode = function ($code) use ($codes, $that) { + $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 @@ -73,6 +78,13 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf $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; @@ -90,7 +102,9 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf // push resulting data through utf8 sequencer $utf8 = new Utf8Sequencer($parser); - $utf8->on('data', array($this, 'onFallback')); + $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')); @@ -106,6 +120,7 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf * @param string $prompt * @return self * @uses self::redraw() + * @deprecated use Stdio::setPrompt() instead */ public function setPrompt($prompt) { @@ -123,6 +138,7 @@ public function setPrompt($prompt) * * @return string * @see self::setPrompt() + * @deprecated use Stdio::getPrompt() instead */ public function getPrompt() { @@ -154,6 +170,7 @@ public function getPrompt() * @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) { @@ -180,6 +197,7 @@ public function setEcho($echo) * @param boolean $move * @return self * @uses self::redraw() + * @deprecated use Stdio::setMove() instead */ public function setMove($move) { @@ -200,6 +218,7 @@ public function setMove($move) * @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() { @@ -231,6 +250,7 @@ public function getCursorPosition() * @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() { @@ -256,6 +276,7 @@ public function getCursorCell() * @return self * @uses self::moveCursorTo() * @uses self::redraw() + * @deprecated use Stdio::moveCursorBy() instead */ public function moveCursorBy($n) { @@ -273,6 +294,7 @@ public function moveCursorBy($n) * @param int $n * @return self * @uses self::redraw() + * @deprecated use Stdio::moveCursorTo() instead */ public function moveCursorTo($n) { @@ -299,6 +321,7 @@ public function moveCursorTo($n) * @param string $input * @return self * @uses self::redraw() + * @deprecated use Stdio::addInput() instead */ public function addInput($input) { @@ -330,6 +353,7 @@ public function addInput($input) * @param string $input * @return self * @uses self::redraw() + * @deprecated use Stdio::setInput() instead */ public function setInput($input) { @@ -356,6 +380,7 @@ public function setInput($input) * get current text input buffer * * @return string + * @deprecated use Stdio::getInput() instead */ public function getInput() { @@ -368,6 +393,7 @@ public function getInput() * @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) { @@ -380,6 +406,7 @@ public function addHistory($line) * Clears the complete history list * * @return self + * @deprecated use Stdio::clearHistory() instead */ public function clearHistory() { @@ -398,6 +425,7 @@ public function clearHistory() * Returns an array with all lines in the history * * @return string[] + * @deprecated use Stdio::listHistory() instead */ public function listHistory() { @@ -409,6 +437,7 @@ public function listHistory() * * @param int|null $limit * @return self + * @deprecated use Stdio::limitHistory() instead */ public function limitHistory($limit) { @@ -443,6 +472,10 @@ public function limitHistory($limit) * @param callable|null $autocomplete * @return self * @throws \InvalidArgumentException if the given callable is invalid +<<<<<<< HEAD +======= + * @deprecated use Stdio::setAutocomplete() instead +>>>>>>> Deprecate Readline and move all methods to Stdio */ public function setAutocomplete($autocomplete) { @@ -715,19 +748,28 @@ public function onKeyDown() * * @internal */ - public function onFallback($chars) + 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) { - if ($this->listeners($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 = ''; } - $this->emit($char, array($char)); + $emit->emit($char, array($char)); } else { $buffer .= $char; } diff --git a/src/Stdio.php b/src/Stdio.php index 84737b2..3f9384b 100644 --- a/src/Stdio.php +++ b/src/Stdio.php @@ -25,15 +25,15 @@ class Stdio extends EventEmitter implements DuplexStreamInterface public function __construct(LoopInterface $loop, ReadableStreamInterface $input = null, WritableStreamInterface $output = null, Readline $readline = null) { if ($input === null) { - $input = $this->createStdin($loop); + $input = $this->createStdin($loop); // @codeCoverageIgnore } if ($output === null) { - $output = $this->createStdout($loop); + $output = $this->createStdout($loop); // @codeCoverageIgnore } if ($readline === null) { - $readline = new Readline($input, $output); + $readline = new Readline($input, $output, $this); } $this->input = $input; @@ -198,11 +198,260 @@ public function close() $this->output->close(); } + /** + * @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); + } + private function width($str) { return $this->readline->strwidth($str) - 2 * substr_count($str, "\x08"); diff --git a/tests/ReadlineTest.php b/tests/ReadlineTest.php index aa87528..bb6436e 100644 --- a/tests/ReadlineTest.php +++ b/tests/ReadlineTest.php @@ -2,6 +2,7 @@ use Clue\React\Stdio\Readline; use React\Stream\ThroughStream; +use Evenement\EventEmitter; class ReadlineTest extends TestCase { @@ -950,6 +951,15 @@ public function testAutocompleteShowsLimitedNumberOfAvailableOptionsWhenMultiple $this->assertContains("\na b c d e f g (+19 others)\n", $buffer); } + public function testBindCustomFunctionFromBase() + { + $base = new EventEmitter(); + $base->on('a', $this->expectCallableOnceWith('a')); + + $this->readline = new Readline($this->input, $this->output, $base); + $this->input->emit('data', array('a')); + } + public function testBindCustomFunctionOverwritesInput() { $this->readline->on('a', $this->expectCallableOnceWith('a')); @@ -1005,6 +1015,17 @@ public function testBindCustomFunctionToNlFiresOnCr() $this->input->emit('data', array("hello\r")); } + public function testBindCustomFunctionFromBaseCanOverwriteAutocompleteBehavior() + { + $base = new EventEmitter(); + $base->on("\t", $this->expectCallableOnceWith("\t")); + + $this->readline = new Readline($this->input, $this->output, $base); + $this->readline->setAutocomplete($this->expectCallableNever()); + + $this->input->emit('data', array("\t")); + } + public function testEmitEmptyInputOnEnter() { $this->readline->on('data', $this->expectCallableOnceWith("\n")); diff --git a/tests/StdioTest.php b/tests/StdioTest.php index c3f179f..3e5e4b4 100644 --- a/tests/StdioTest.php +++ b/tests/StdioTest.php @@ -25,6 +25,16 @@ public function testCtorDefaultArgs() // $stdio->close(); } + public function testCtorWithoutReadlineWillCreateNewReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + + $stdio = new Stdio($this->loop, $input, $output); + + $this->assertInstanceOf('Clue\React\Stdio\Readline', $stdio->getReadline()); + } + public function testCtorReadlineArgWillBeReturnedBygetReadline() { $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); @@ -449,4 +459,105 @@ public function testErrorEventFromOutputWillBeForwarded() $output->emit('error', array(new \RuntimeException())); } + + public function testPromptWillBeForwardedToReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $readline = new Readline($input, $output); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $stdio->setPrompt('> '); + + $this->assertEquals('> ', $stdio->getPrompt()); + } + + public function testSetAutocompleteWillBeForwardedToReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock(); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $readline->expects($this->once())->method('setAutocomplete')->with(null); + + $stdio->setAutocomplete(null); + } + + public function testSetEchoWillBeForwardedToReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock(); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $readline->expects($this->once())->method('setEcho')->with(false); + + $stdio->setEcho(false); + } + + public function testSetMoveWillBeForwardedToReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock(); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $readline->expects($this->once())->method('setMove')->with(false); + + $stdio->setMove(false); + } + + public function testInputWillBeForwardedToReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $readline = new Readline($input, $output); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $stdio->setInput('hello'); + $stdio->addInput('!'); + + $this->assertEquals('hello!', $stdio->getInput()); + } + + public function testCursorWillBeForwardedToReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $readline = new Readline($input, $output); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $stdio->setInput('hello'); + $stdio->moveCursorTo(0); + $stdio->moveCursorBy(1); + + $this->assertEquals(1, $stdio->getCursorPosition()); + $this->assertEquals(1, $stdio->getCursorCell()); + } + + public function testHistoryWillBeForwardedToReadline() + { + $input = $this->getMockBuilder('React\Stream\ReadableStreamInterface')->getMock(); + $output = $this->getMockBuilder('React\Stream\WritableStreamInterface')->getMock(); + $readline = new Readline($input, $output); + + $stdio = new Stdio($this->loop, $input, $output, $readline); + + $stdio->limitHistory(2); + $stdio->addHistory('hello'); + $stdio->addHistory('world'); + $stdio->addHistory('again'); + + $this->assertEquals(array('world', 'again'), $stdio->listHistory()); + + $stdio->clearHistory(); + $this->assertEquals(array(), $stdio->listHistory()); + } }