# clue/reactphp-stdio

[![CI status](https://github.com/clue/reactphp-stdio/workflows/CI/badge.svg)](https://github.com/clue/reactphp-stdio/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/stdio-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/stdio-react)

Async, event-driven and UTF-8 aware console input & output (STDIN, STDOUT) for
truly interactive CLI applications, built on top of [ReactPHP](https://reactphp.org/).

You can use this library to build truly interactive and responsive command
line (CLI) applications, that immediately react when the user types in
a line or hits a certain key. Inspired by `ext-readline`, but supports UTF-8
and interleaved I/O (typing while output is being printed), history and
autocomplete support and takes care of proper TTY settings under the hood
without requiring any extensions or special installation.

**Table of contents**

* [Support us](#support-us)
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
    * [Stdio](#stdio)
        * [Output](#output)
        * [Input](#input)
        * [Prompt](#prompt)
        * [Echo](#echo)
        * [Input buffer](#input-buffer)
        * [Cursor](#cursor)
        * [History](#history)
        * [Autocomplete](#autocomplete)
        * [Keys](#keys)
        * [Bell](#bell)
    * [~~Readline~~](#readline)
* [Pitfalls](#pitfalls)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
* [More](#more)

## Support us

We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.

Let's take these projects to the next level together! 🚀

## Quickstart example

Once [installed](#install), you can use the following code to present a prompt in a CLI program:

```php
<?php

require __DIR__ . '/vendor/autoload.php';

$stdio = new Clue\React\Stdio\Stdio();
$stdio->setPrompt('Input > ');

$stdio->on('data', function ($line) use ($stdio) {
    $line = rtrim($line, "\r\n");
    $stdio->write('Your input: ' . $line . PHP_EOL);

    if ($line === 'quit') {
        $stdio->end();
    }
});
```

See also the [examples](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.

```php
$stdio = new Clue\React\Stdio\Stdio();
```

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.

See below for waiting for user input and writing output.
The `Stdio` class is a well-behaving duplex stream
(implementing ReactPHP's `DuplexStreamInterface`) that emits each complete
line as a `data` event, including the trailing newline.

#### Output

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

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:

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

Because the `Stdio` is a well-behaving writable stream,
you can also `pipe()` any readable stream into this stream.

```php
$logger->pipe($stdio);
```

#### Input

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

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

```php
$stdio->on('data', function ($line) {
    if ($line === "start\n") {
        doSomething();
    }
});
```

Note that this class takes care of buffering incomplete lines and will only emit
complete lines.
This means that the line will usually end with the trailing newline character.
If the stream ends without a trailing newline character, it will not be present
in the `data` event.
As such, it's usually recommended to remove the trailing newline character
before processing command line input like this:

```php
$stdio->on('data', function ($line) {
    $line = rtrim($line, "\r\n");
    if ($line === "start") {
        doSomething();
    }
});
```

Similarly, if you copy and paste a larger chunk of text, it will properly emit
multiple complete lines with a separate `data` event for each line.

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 this interface,
so read on..

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

```php
$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
$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($stdio->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:

```php
$stdio->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:

```php
$stdio->setEcho('*');
```

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

```php
$stdio->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 `addInput($input)` method can be used to add text to the *user input
buffer* at the current cursor position.
The given text will be inserted just like the user would type in a text and as
such adjusts the current cursor position accordingly.
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:

```php
$stdio->addInput('hello');
```

The `setInput($buffer)` method can be used to control the *user input buffer*.
The given text will be used to replace the entire current *user input buffer*
and as such adjusts the current cursor position to the end of the new 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:

```php
$stdio->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:

```php
$buffer = $stdio->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:

```php
$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
$stdio->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:

```php
$position = $stdio->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:

```php
$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
$stdio->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:

```php
$stdio->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:

```php
$stdio->on('data', function ($line) use ($stdio) {
    $line = rtrim($line);
    $all = $stdio->listHistory();

    // skip empty line and duplicate of previous line
    if ($line !== '' && $line !== end($all)) {
        $stdio->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()`.

```php
$list = $stdio->listHistory();

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

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
$stdio->addHistory('a');
$stdio->addHistory('b');

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

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.
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
$stdio->clearHistory();

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

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

```php
$limit = getenv('HISTSIZE');
if ($limit === '' || $limit < 0) {
    // empty string or negative value means unlimited
    $stdio->limitHistory(null);
} elseif ($limit !== false) {
    // apply any other value if given
    $stdio->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): 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
$stdio->setAutocomplete(function () {
    return array(
        'exit',
        'echo',
        'help',
    );
});
```

If the user types `he [TAB]`, the first two matches will be skipped as they do
not match the current word prefix and the last 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:

```php
> 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 you more control over this behavior, the autocomplete function
actually receives three arguments (similar to `ext-readline`'s
[`readline_completion_function()`](https://www.php.net/manual/en/function.readline-completion-function.php)):
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:

```php
$stdio->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](#input-buffer) and [cursor](#cursor)
directly like this:

```php
$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
    return array();
});
```

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

```php
$stdio->setAutocomplete(null);
```

#### Keys

The `Readline` class is responsible for reading user input from `STDIN` and
registering appropriate key events.
By default, `Readline` uses a hard-coded key mapping that resembles the one
usually found in common terminals.
This means that normal Unicode character keys ("a" and "b", but also "?", "ä",
"µ" etc.) will be processed as user input, while special control keys can be
used for [cursor movement](#cursor), [history](#history) and
[autocomplete](#autocomplete) functions.
Unknown special keys will be ignored and will not processed as part of the user
input by default.

Additionally, you can bind custom functions to any key code you want.
If a custom function is bound to a certain key code, the default behavior will
no longer trigger.
This allows you to register entirely new functions to keys or to overwrite any
of the existing behavior.

For example, you can use the following code to print some help text when the
user hits a certain key:

```php
$stdio->on('?', function () use ($stdio) {
     $stdio->write('Here\'s some help: …' . PHP_EOL);
});
```

Similarly, this can be used to manipulate the user input and replace some of the
input when the user hits a certain key:

```php
$stdio->on('ä', function () use ($stdio) {
     $stdio->addInput('a');
});
```

The `Readline` uses raw binary key codes as emitted by the terminal.
This means that you can use the normal UTF-8 character representation for normal
Unicode characters.
Special keys use binary control code sequences (refer to ANSI / VT100 control
codes for more details).
For example, the following code can be used to register a custom function to the
UP arrow cursor key:

```php
$stdio->on("\033[A", function () use ($stdio) {
     $stdio->setInput(strtoupper($stdio->getInput()));
});
```

#### Bell

By default, this project will emit an audible/visible BELL signal when the user
tries to execute an otherwise disabled 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.

The `setBell(bool $bell): void` method can be used to
enable or disable emitting the BELL signal when using a disabled function:

```php
$stdio->setBell(false);
```

### ~~Readline~~

> Deprecated since v2.3.0, see [`Stdio`](#stdio) instead.

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

```php
// 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`](#stdio)
instance like this:

```php
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](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)

This project follows [SemVer](https://semver.org/).
This will install the latest supported version:

```bash
$ composer require clue/stdio-react:^2.6
```

See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.

This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use the latest supported PHP version* for this project.

Internally, it will use the `ext-mbstring` to count and measure string sizes.
If this extension is missing, then this library will use a slighty slower Regex
work-around that should otherwise work equally well.
Installing `ext-mbstring` is highly recommended.

Internally, it will use the `ext-readline` to enable raw terminal input mode.
If this extension is missing, then this library will manually set the required
TTY settings on start and will try to restore previous settings on exit.
Input line editing is handled entirely within this library and does not rely on
`ext-readline`.
Installing `ext-readline` is entirely optional.

Note that *Microsoft Windows is not supported*.
Due to platform constraints, PHP does not provide support for reading from
standard console input without blocking on Windows.
Unfortunately, until the underlying PHP feature request is implemented (which
is unlikely to happen any time soon), there's little we can do in this library.
However, this package does work on Windows Subsystem for Linux (or WSL) without
issues. We suggest [installing WSL](https://msdn.microsoft.com/en-us/commandline/wsl/install_guide) 
when you want to run this package on Windows.
See also [#18](https://github.com/clue/reactphp-stdio/issues/18) for more details.

## Tests

To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):

```bash
$ composer install
```

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

```bash
$ vendor/bin/phpunit
```

## License

This project is released under the permissive [MIT license](LICENSE).

> Did you know that I offer custom development services and issuing invoices for
  sponsorships of releases and for contributions? Contact me (@clue) for details.

## More

* If you want to learn more about processing streams of data, refer to the documentation of
  the underlying [react/stream](https://github.com/reactphp/stream) component.

* If you build an interactive CLI tool that reads a command line from STDIN, you
  may want to use [clue/arguments](https://github.com/clue/php-arguments) in
  order to split this string up into its individual arguments and then use
  [clue/commander](https://github.com/clue/php-commander) to route to registered
  commands and their required arguments.
