Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for binding custom functions to any key code #70

Merged
merged 1 commit into from
Feb 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ any extensions or special installation.
* [Cursor](#cursor)
* [History](#history)
* [Autocomplete](#autocomplete)
* [Keys](#keys)
* [Pitfalls](#pitfalls)
* [Install](#install)
* [Tests](#tests)
Expand Down Expand Up @@ -503,6 +504,57 @@ disable the autocomplete function:
$readline->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
$readline->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
$readline->on('ä', function () use ($readline) {
$readline->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
$readline->on("\033[A", function () use ($readline) {
$readline->setInput(strtoupper($readline->getInput()));
});
```

## Pitfalls

The [`Readline`](#readline) has to redraw the current user
Expand Down
46 changes: 46 additions & 0 deletions examples/04-bindings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

use Clue\React\Stdio\Stdio;

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

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

$stdio = new Stdio($loop);
$readline = $stdio->getReadline();

$readline->setPrompt('> ');

// add some special key bindings
$readline->on('a', function () use ($readline) {
$readline->addInput('ä');
});
$readline->on('o', function () use ($readline) {
$readline->addInput('ö');
});
$readline->on('u', function () use ($readline) {
$readline->addInput('ü');
});

$readline->on('?', function () use ($stdio) {
$stdio->write('Do you need help?');
});

// bind CTRL+E
$readline->on("\x05", function () use ($stdio) {
$stdio->write("ignore CTRL+E" . PHP_EOL);
});
// bind CTRL+H
$readline->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) {
$line = rtrim($line, "\r\n");
$stdio->end('you just said: ' . $line . ' (' . strlen($line) . ')' . PHP_EOL);
});

$loop->run();
45 changes: 45 additions & 0 deletions examples/05-cursor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

use Clue\React\Stdio\Stdio;

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

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

$stdio = new Stdio($loop);
$readline = $stdio->getReadline();

$value = 10;
$readline->on("\033[A", function () use (&$value, $readline) {
$value++;
$readline->setPrompt('Value: ' . $value);
});
$readline->on("\033[B", function () use (&$value, $readline) {
--$value;
$readline->setPrompt('Value: ' . $value);
});

// hijack enter to just print our current value
$readline->on("\n", function () use ($readline, $stdio, &$value) {
$stdio->write("Your choice was $value\n");
});

// quit on "q"
$readline->on('q', function () use ($stdio) {
$stdio->end();
});

// user can still type all keys, but we simply hide user input
$readline->setEcho(false);

// instead of showing user input, we just show a custom prompt
$readline->setPrompt('Value: ' . $value);

$stdio->write('Welcome to this cursor demo

Use cursor UP/DOWN to change value.

Use "q" to quit
');

$loop->run();
31 changes: 30 additions & 1 deletion src/Readline.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf
// "\033[20~" => 'onKeyF10',
);
$decode = function ($code) use ($codes, $that) {
if ($that->listeners($code)) {
$that->emit($code, array($code));
return;
}

if (isset($codes[$code])) {
$method = $codes[$code];
$that->$method($code);
Expand Down Expand Up @@ -724,7 +729,26 @@ public function onKeyDown()
*/
public function onFallback($chars)
{
$this->addInput($chars);
// check if there's any special key binding for any of the chars
$buffer = '';
foreach ($this->strsplit($chars) as $char) {
if ($this->listeners($char)) {
// 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));
} else {
$buffer .= $char;
}
}

// process remaining input characters after last special key binding
if ($buffer !== '') {
$this->addInput($buffer);
}
}

/**
Expand Down Expand Up @@ -837,6 +861,11 @@ public function strwidth($str)
));
}

private function strsplit($str)
{
return preg_split('//u', $str, null, PREG_SPLIT_NO_EMPTY);
}

/** @internal */
public function handleEnd()
{
Expand Down
14 changes: 14 additions & 0 deletions tests/FunctionalExampleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ public function testPeriodicExampleWithClosedInputAndOutputQuitsImmediatelyWitho
$this->assertEquals('', $output);
}

public function testBindingsExampleWithPipedInputEndsBecauseInputEnds()
{
$output = $this->execExample('echo test | php 04-bindings.php');

$this->assertContains('you just said: test (4)' . PHP_EOL, $output);
}

public function testBindingsExampleWithPipedInputEndsWithSpecialBindingsReplacedBecauseInputEnds()
{
$output = $this->execExample('echo hello | php 04-bindings.php');

$this->assertContains('you just said: hellö (6)' . PHP_EOL, $output);
}

public function testStubShowStdinIsReadableByDefault()
{
$output = $this->execExample('php ../tests/stub/01-check-stdin.php');
Expand Down
38 changes: 38 additions & 0 deletions tests/ReadlineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,44 @@ public function testAutocompleteShowsLimitedNumberOfAvailableOptionsWhenMultiple
$this->assertContains("\na b c d e f g (+19 others)\n", $buffer);
}

public function testBindCustomFunctionOverwritesInput()
{
$this->readline->on('a', $this->expectCallableOnceWith('a'));

$this->input->emit('data', array("a"));

$this->assertEquals('', $this->readline->getInput());
}

public function testBindCustomFunctionOverwritesInputButKeepsRest()
{
$this->readline->on('e', $this->expectCallableOnceWith('e'));

$this->input->emit('data', array("test"));

$this->assertEquals('tst', $this->readline->getInput());
}

public function testBindCustomFunctionCanOverwriteInput()
{
$readline = $this->readline;
$readline->on('a', function () use ($readline) {
$readline->addInput('ä');
});

$this->input->emit('data', array("hallo"));

$this->assertEquals('hällo', $this->readline->getInput());
}

public function testBindCustomFunctionCanOverwriteAutocompleteBehavior()
{
$this->readline->on("\t", $this->expectCallableOnceWith("\t"));
$this->readline->setAutocomplete($this->expectCallableNever());

$this->input->emit('data', array("\t"));
}

public function testEmitEmptyInputOnEnter()
{
$this->readline->on('data', $this->expectCallableOnceWith("\n"));
Expand Down