Giter VIP home page Giter VIP logo

php-stdio-react's Introduction

clue/stdio-react Build Status

Async, event-driven and UTF-8 aware console input & output (STDIN, STDOUT), built on top of for React PHP

Table of Contents

Quickstart example

Once installed, you can use the following code to present a prompt in a CLI program:

$loop = React\EventLoop\Factory::create();
$stdio = new Stdio($loop);

$stdio->getReadline()->setPrompt('Input > ');

$stdio->on('line', function ($line) use ($stdio) {
    var_dump($line);
    
    if ($line === 'quit') {
        $stdio->end();
    }
});

$loop->run();

See also the examples.

Usage

Stdio

The Stdio is the main interface to this library. It is responsible for orchestrating the input and output streams by registering and forwarding the corresponding events. It also registers everything with the main EventLoop.

$loop = React\EventLoop\Factory::create();
$stdio = new Stdio($loop);

See below for waiting for user input and writing output. Alternatively, the Stdio is also a well-behaving duplex stream (implementing React's DuplexStreamInterface) that emits each complete line as a data event (including the trailing newline). This is considered advanced usage.

Output

The Stdio is a well-behaving writable stream implementing React's WritableStreamInterface.

The writeln($line) method can be used to print a line to console output. A trailing newline will be added automatically.

$stdio->writeln('hello world');

The write($text) method can be used to print the given text characters to console output. This is useful if you need more control or want to output individual bytes or binary output:

$stdio->write('hello');
$stdio->write(" world\n");

The overwrite($text) method can be used to overwrite/replace the last incomplete line with the given text:

$stdio->write('Loading…');
$stdio->overwrite('Done!');

Alternatively, you can also use the Stdio as a writable stream. You can pipe() any readable stream into this stream.

Input

The Stdio is a well-behaving readable stream implementing React's ReadableStreamInterface.

It will emit a line event for every line read from console input. The event will contain the input buffer as-is, without the trailing newline. You can register any number of event handlers like this:

$stdio->on('line', function ($line) {
    if ($line === 'start') {
        doSomething();
    }
});

You can control various aspects of the console input through the Readline, so read on..

Using the line event is the recommended way to wait for user input. Alternatively, using the Readline as a readable stream is considered advanced usage.

Alternatively, you can also use the Stdio as a readable stream, which emits each complete line as a data event (including the trailing newline). This can be used to pipe() this stream into other writable streams.

Readline

The 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:

$readline = $stdio->getReadline();

See above for waiting for user input. Alternatively, the Readline is also a well-behaving readable stream (implementing React's ReadableStreamInterface) that emits each complete line as a data event (without 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.

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:

$readline->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:

$readline->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:

assert($readline->getPrompt() === '');

Echo

The echo mode controls how the actual user input buffer will be presented in the user input line.

The setEcho($echo) method can be used to control the echo mode. The default is to print the user input buffer as-is.

You can disable printing the user input buffer, e.g. for password prompts. The user will still be able to type, but will not receive any indication of the current user input buffer. Please note that this often leads to a bad user experience as users will not even see their cursor position. Simply pass a boolean false like this:

$readline->setEcho(false);

Alternatively, you can also hide the user input buffer by using a replacement character. One replacement character will be printed for each character in the user input buffer. This is useful for password prompts to give users an indicatation that their key presses are registered. This often provides a better user experience and allows users to still control their cursor position. Simply pass a string replacement character likes this:

$readline->setEcho('*');

To restore the original behavior where every character appears as-is, simply pass a boolean true:

$readline->setEcho(true);

Input buffer

Everything the user types will be buffered in the current user input buffer. Once the user hits enter, the user input buffer will be processed and cleared.

The setInput($buffer) method can be used to control the user input buffer. The user will be able to delete and/or rewrite the buffer at any time. Changing the user input buffer can be useful for presenting a preset input to the user (like the last password attempt). Simply pass an input string like this:

$readline->setInput('lastpass');

The getInput() method can be used to access the current user input buffer. This can be useful if you want to append some input behind the current user input buffer. You can simply access the buffer like this:

$buffer = $readline->getInput();

Cursor

By default, users can control their (horizontal) cursor position by using their arrow keys on the keyboard. Also, every character pressed on the keyboard advances the cursor position.

The setMove($toggle) method can be used to control whether users are allowed to use their arrow keys. To disable the left and right arrow keys, simply pass a boolean false like this:

$readline->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:

$readline->setMove(true);

The getCursorPosition() method can be used to access the current cursor position, measured in number of characters. This can be useful if you want to get a substring of the current user input buffer. Simply invoke it like this:

$position = $readline->getCursorPosition();

The getCursorCell() method can be used to get the current cursor position, measured in number of monospace cells. Most normal characters (plain ASCII and most multi-byte UTF-8 sequences) take a single monospace cell. However, there are a number of characters that have no visual representation (and do not take a cell at all) or characters that do not fit within a single cell (like some asian glyphs). This method is mostly useful for calculating the visual cursor position on screen, but you may also invoke it like this:

$cell = $readline->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:

$readline->moveCursorTo(0);

The moveCursorBy($offset) method can be used to change the cursor position by the given number of characters relative to the current position. A positive number will move the cursor to the right - a negative number will move the cursor to the left. For example, to move the cursor one character to the left, simply call:

$readline->moveCursorBy(-1);

History

By default, users can access the history of previous commands by using their UP and DOWN cursor keys on the keyboard. The history will start with an empty state, thus this feature is effectively disabled, as the UP and DOWN cursor keys have no function then.

Note that the history is not maintained automatically. Any input the user submits by hitting enter will not be added to the history automatically. This may seem inconvenient at first, but it actually gives you more control over what (and when) lines should be added to the history. If you want to automatically add everything from the user input to the history, you may want to use something like this:

$readline->on('data', function ($line) use ($readline) {
    $all = $readline->listHistory();
    
    // skip empty line and duplicate of previous line
    if (trim($line) !== '' && $line !== end($all)) {
        $readline->addHistory($line);
    }
});

The listHistory(): string[] method can be used to return an array with all lines in the history. This will be an empty array until you add new entries via addHistory().

$list = $readline->listHistory();

assert(count($list) === 0);

The addHistory(string $line): Readline 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.

$readline->addHistory('a');
$readline->addHistory('b');

$list = $readline->listHistory();
assert($list === array('a', 'b'));

The clearHistory(): Readline 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. 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.

$readline->clearHistory();

$list = $readline->listHistory();
assert(count($list) === 0);

The limitHistory(?int $limit): Readline 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. You can use an integer value to limit this to the given number of entries or use null for an unlimited number (not recommended, because everything is kept in RAM). If you set the limit to 0 (int zero), the history will effectively be disabled, as no lines can be added to or returned from the history list. If you're building a CLI application, you may also want to use something like this to obey the HISTSIZE environment variable:

$limit = getenv('HISTSIZE');
if ($limit === '' || $limit < 0) {
    // empty string or negative value means unlimited
    $readline->limitHistory(null);
} elseif ($limit !== false) {
    // apply any other value if given
    $readline->limitHistory($limit);
}

There is no such thing as a readHistory() or writeHistory() method because filesystem operations are inherently blocking and thus beyond the scope of this library. Using your favorite filesystem API and an appropriate number of addHistory() or a single listHistory() call respectively should be fairly straight forward and is left up as an exercise for the reader of this documentation (i.e. you).

Autocomplete

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 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:

$readline->setAutocomplete(function () {
    return array(
        'exit',
        'echo',
        'help',
    );
});

If the user types he [TAB], the first match will be skipped as it does not match the current word prefix and the second one will be picked automatically, so that the resulting input buffer is hello .

If the user types e [TAB], then this will match multiple entries and the user will be presented with a list of up to 8 available word completions to choose from like this:

> e [TAB]
exit  echo
> e

Unless otherwise specified, the matches will be performed against the current word boundaries in the input buffer. This means that if the user types hello [SPACE] ex [TAB], then the resulting input buffer is hello exit , which may or may not be what you need depending on your particular use case.

In order to give your more control over this behavior, the autocomplete function actually receives three arguments (similar to ext-readline's readline_completion_function()): The first argument will be the current incomplete word according to current cursor position and word boundaries, while the second and third argument will be the start and end offset of this word within the complete input buffer measured in (Unicode) characters. The above examples will be invoked as $fn('he', 0, 2), $fn('e', 0, 1) and $fn('ex', 6, 8) respectively. You may want to use this as an $offset argument to check if the current word is an argument or a root command and the $word argument to autocomplete partial filename matches like this:

$readline->setAutocomplete(function ($word, $offset) {
    if ($offset <= 1) {
        // autocomplete root commands at offset=0/1 only
        return array('cat', 'rm', 'stat');
    } else {
        // autocomplete all command arguments as glob pattern
        return glob($word . '*', GLOB_MARK);
    }
});

Note that the user may also use quotes and/or leading whitespace around the root command, for example "hell [TAB], in which case the offset will be advanced such as this will be invoked as $fn('hell', 1, 4). Unless you use a more sophisticated argument parser, a decent approximation may be using $offset <= 1 to check this is a root command.

If you need even more control over autocompletion, you may also want to access and/or manipulate the input buffer and cursor directly like this:

$readline->setAutocomplete(function () use ($readline) {
    if ($readline->getInput() === 'run') {
        $readline->setInput('run --test --value=42');
        $readline->moveCursorBy(-2);
    }

    // return empty array so normal autocompletion doesn't kick in
    return array();
});

You can use a null value to remove the autocomplete function again and thus disable the autocomplete function:

$readline->setAutocomplete(null);

Advanced

Stdout

The Stdout represents a WritableStream and is responsible for handling console output.

Interfacing with it directly is not recommended and considered advanced usage.

If you want to print some text to console output, use the Stdio::write() instead:

$stdio->write('hello');

Should you need to interface with the Stdout, you can access the current instance through the Stdio:

$stdout = $stdio->getOutput();

Stdin

The Stdin represents a ReadableStream and is responsible for handling console input.

Interfacing with it directly is not recommended and considered advanced usage.

If you want to read a line from console input, use the Stdio::on() instead:

$stdio->on('line', function ($line) use ($stdio) {
    $stdio->writeln('You said "' . $line . '"');
});

Should you need to interface with the Stdin, you can access the current instance through the Stdio:

You can access the current instance through the Stdio:

$stdin = $stdio->getInput();

Pitfalls

The Readline 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:

// echo 'hello world!' . PHP_EOL;
$stdio->write('hello world!' . PHP_EOL);

Depending on your program, it may or may not be reasonable to replace all such occurences. As an alternative, you may utilize output buffering that will automatically forward all write events to the Stdio instance like this:

ob_start(function ($chunk) use ($stdio) {
    // forward write event to Stdio instead
    $stdio->write($chunk);

    // discard data from normal output handling
    return '';
}, 1);

Install

The recommended way to install this library is through Composer. New to Composer?

This will install the latest supported version:

$ composer require clue/stdio-react:^1.0

More details and upgrade guides can be found in the CHANGELOG.

Tests

To run the test suite, you first need to clone this repo and then install all dependencies through Composer:

$ composer install

To run the test suite, go to the project root and run:

$ php vendor/bin/phpunit

License

MIT

More

  • If you want to learn more about processing streams of data, refer to the documentation of the underlying react/stream component.
  • If you build an interactive CLI tool that reads a command line from STDIN, you may want to use clue/arguments in order to split this string up into its individual arguments and then use clue/commander to route to registered commands and their required arguments.

php-stdio-react's People

Contributors

clue avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.