Skip to content

Commit 1094b9a

Browse files
authored
Merge pull request #70 from clue-labs/keys
Add support for binding custom functions to any key code
2 parents 5d123dc + 258eeb7 commit 1094b9a

File tree

6 files changed

+225
-1
lines changed

6 files changed

+225
-1
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ any extensions or special installation.
2424
* [Cursor](#cursor)
2525
* [History](#history)
2626
* [Autocomplete](#autocomplete)
27+
* [Keys](#keys)
2728
* [Pitfalls](#pitfalls)
2829
* [Install](#install)
2930
* [Tests](#tests)
@@ -503,6 +504,57 @@ disable the autocomplete function:
503504
$readline->setAutocomplete(null);
504505
```
505506

507+
#### Keys
508+
509+
The `Readline` class is responsible for reading user input from `STDIN` and
510+
registering appropriate key events.
511+
By default, `Readline` uses a hard-coded key mapping that resembles the one
512+
usually found in common terminals.
513+
This means that normal Unicode character keys ("a" and "b", but also "?", "ä",
514+
"µ" etc.) will be processed as user input, while special control keys can be
515+
used for [cursor movement](#cursor), [history](#history) and
516+
[autocomplete](#autocomplete) functions.
517+
Unknown special keys will be ignored and will not processed as part of the user
518+
input by default.
519+
520+
Additionally, you can bind custom functions to any key code you want.
521+
If a custom function is bound to a certain key code, the default behavior will
522+
no longer trigger.
523+
This allows you to register entirely new functions to keys or to overwrite any
524+
of the existing behavior.
525+
526+
For example, you can use the following code to print some help text when the
527+
user hits a certain key:
528+
529+
```php
530+
$readline->on('?', function () use ($stdio) {
531+
$stdio->write('Here\'s some help: …' . PHP_EOL);
532+
});
533+
```
534+
535+
Similarly, this can be used to manipulate the user input and replace some of the
536+
input when the user hits a certain key:
537+
538+
```php
539+
$readline->on('ä', function () use ($readline) {
540+
$readline->addInput('a');
541+
});
542+
```
543+
544+
The `Readline` uses raw binary key codes as emitted by the terminal.
545+
This means that you can use the normal UTF-8 character representation for normal
546+
Unicode characters.
547+
Special keys use binary control code sequences (refer to ANSI / VT100 control
548+
codes for more details).
549+
For example, the following code can be used to register a custom function to the
550+
UP arrow cursor key:
551+
552+
```php
553+
$readline->on("\033[A", function () use ($readline) {
554+
$readline->setInput(strtoupper($readline->getInput()));
555+
});
556+
```
557+
506558
## Pitfalls
507559

508560
The [`Readline`](#readline) has to redraw the current user

examples/04-bindings.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
use Clue\React\Stdio\Stdio;
4+
5+
require __DIR__ . '/../vendor/autoload.php';
6+
7+
$loop = React\EventLoop\Factory::create();
8+
9+
$stdio = new Stdio($loop);
10+
$readline = $stdio->getReadline();
11+
12+
$readline->setPrompt('> ');
13+
14+
// add some special key bindings
15+
$readline->on('a', function () use ($readline) {
16+
$readline->addInput('ä');
17+
});
18+
$readline->on('o', function () use ($readline) {
19+
$readline->addInput('ö');
20+
});
21+
$readline->on('u', function () use ($readline) {
22+
$readline->addInput('ü');
23+
});
24+
25+
$readline->on('?', function () use ($stdio) {
26+
$stdio->write('Do you need help?');
27+
});
28+
29+
// bind CTRL+E
30+
$readline->on("\x05", function () use ($stdio) {
31+
$stdio->write("ignore CTRL+E" . PHP_EOL);
32+
});
33+
// bind CTRL+H
34+
$readline->on("\x08", function () use ($stdio) {
35+
$stdio->write('Use "?" if you need help.' . PHP_EOL);
36+
});
37+
38+
$stdio->write('Welcome to this interactive demo' . PHP_EOL);
39+
40+
// end once the user enters a command
41+
$stdio->on('data', function ($line) use ($stdio, $readline) {
42+
$line = rtrim($line, "\r\n");
43+
$stdio->end('you just said: ' . $line . ' (' . strlen($line) . ')' . PHP_EOL);
44+
});
45+
46+
$loop->run();

examples/05-cursor.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
use Clue\React\Stdio\Stdio;
4+
5+
require __DIR__ . '/../vendor/autoload.php';
6+
7+
$loop = React\EventLoop\Factory::create();
8+
9+
$stdio = new Stdio($loop);
10+
$readline = $stdio->getReadline();
11+
12+
$value = 10;
13+
$readline->on("\033[A", function () use (&$value, $readline) {
14+
$value++;
15+
$readline->setPrompt('Value: ' . $value);
16+
});
17+
$readline->on("\033[B", function () use (&$value, $readline) {
18+
--$value;
19+
$readline->setPrompt('Value: ' . $value);
20+
});
21+
22+
// hijack enter to just print our current value
23+
$readline->on("\n", function () use ($readline, $stdio, &$value) {
24+
$stdio->write("Your choice was $value\n");
25+
});
26+
27+
// quit on "q"
28+
$readline->on('q', function () use ($stdio) {
29+
$stdio->end();
30+
});
31+
32+
// user can still type all keys, but we simply hide user input
33+
$readline->setEcho(false);
34+
35+
// instead of showing user input, we just show a custom prompt
36+
$readline->setPrompt('Value: ' . $value);
37+
38+
$stdio->write('Welcome to this cursor demo
39+
40+
Use cursor UP/DOWN to change value.
41+
42+
Use "q" to quit
43+
');
44+
45+
$loop->run();

src/Readline.php

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf
6363
// "\033[20~" => 'onKeyF10',
6464
);
6565
$decode = function ($code) use ($codes, $that) {
66+
if ($that->listeners($code)) {
67+
$that->emit($code, array($code));
68+
return;
69+
}
70+
6671
if (isset($codes[$code])) {
6772
$method = $codes[$code];
6873
$that->$method($code);
@@ -724,7 +729,26 @@ public function onKeyDown()
724729
*/
725730
public function onFallback($chars)
726731
{
727-
$this->addInput($chars);
732+
// check if there's any special key binding for any of the chars
733+
$buffer = '';
734+
foreach ($this->strsplit($chars) as $char) {
735+
if ($this->listeners($char)) {
736+
// special key binding for this character found
737+
// process all characters before this one before invoking function
738+
if ($buffer !== '') {
739+
$this->addInput($buffer);
740+
$buffer = '';
741+
}
742+
$this->emit($char, array($char));
743+
} else {
744+
$buffer .= $char;
745+
}
746+
}
747+
748+
// process remaining input characters after last special key binding
749+
if ($buffer !== '') {
750+
$this->addInput($buffer);
751+
}
728752
}
729753

730754
/**
@@ -837,6 +861,11 @@ public function strwidth($str)
837861
));
838862
}
839863

864+
private function strsplit($str)
865+
{
866+
return preg_split('//u', $str, null, PREG_SPLIT_NO_EMPTY);
867+
}
868+
840869
/** @internal */
841870
public function handleEnd()
842871
{

tests/FunctionalExampleTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ public function testPeriodicExampleWithClosedInputAndOutputQuitsImmediatelyWitho
5252
$this->assertEquals('', $output);
5353
}
5454

55+
public function testBindingsExampleWithPipedInputEndsBecauseInputEnds()
56+
{
57+
$output = $this->execExample('echo test | php 04-bindings.php');
58+
59+
$this->assertContains('you just said: test (4)' . PHP_EOL, $output);
60+
}
61+
62+
public function testBindingsExampleWithPipedInputEndsWithSpecialBindingsReplacedBecauseInputEnds()
63+
{
64+
$output = $this->execExample('echo hello | php 04-bindings.php');
65+
66+
$this->assertContains('you just said: hellö (6)' . PHP_EOL, $output);
67+
}
68+
5569
public function testStubShowStdinIsReadableByDefault()
5670
{
5771
$output = $this->execExample('php ../tests/stub/01-check-stdin.php');

tests/ReadlineTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,44 @@ public function testAutocompleteShowsLimitedNumberOfAvailableOptionsWhenMultiple
943943
$this->assertContains("\na b c d e f g (+19 others)\n", $buffer);
944944
}
945945

946+
public function testBindCustomFunctionOverwritesInput()
947+
{
948+
$this->readline->on('a', $this->expectCallableOnceWith('a'));
949+
950+
$this->input->emit('data', array("a"));
951+
952+
$this->assertEquals('', $this->readline->getInput());
953+
}
954+
955+
public function testBindCustomFunctionOverwritesInputButKeepsRest()
956+
{
957+
$this->readline->on('e', $this->expectCallableOnceWith('e'));
958+
959+
$this->input->emit('data', array("test"));
960+
961+
$this->assertEquals('tst', $this->readline->getInput());
962+
}
963+
964+
public function testBindCustomFunctionCanOverwriteInput()
965+
{
966+
$readline = $this->readline;
967+
$readline->on('a', function () use ($readline) {
968+
$readline->addInput('ä');
969+
});
970+
971+
$this->input->emit('data', array("hallo"));
972+
973+
$this->assertEquals('hällo', $this->readline->getInput());
974+
}
975+
976+
public function testBindCustomFunctionCanOverwriteAutocompleteBehavior()
977+
{
978+
$this->readline->on("\t", $this->expectCallableOnceWith("\t"));
979+
$this->readline->setAutocomplete($this->expectCallableNever());
980+
981+
$this->input->emit('data', array("\t"));
982+
}
983+
946984
public function testEmitEmptyInputOnEnter()
947985
{
948986
$this->readline->on('data', $this->expectCallableOnceWith("\n"));

0 commit comments

Comments
 (0)