Skip to content

Commit 2796dae

Browse files
authored
Merge pull request #36 from clue-labs/escape
Parse all escape sequences and UTF-8 sequences through external parsers
2 parents 37785c3 + f78b160 commit 2796dae

File tree

4 files changed

+63
-175
lines changed

4 files changed

+63
-175
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"require": {
1414
"php": ">=5.3",
1515
"react/event-loop": "0.3.*|0.4.*",
16-
"react/stream": "^0.4.2"
16+
"react/stream": "^0.4.2",
17+
"clue/utf8-react": "^0.1",
18+
"clue/term-react": "^0.1"
1719
},
1820
"autoload": {
1921
"psr-4": { "Clue\\React\\Stdio\\": "src/" }

src/Readline.php

Lines changed: 39 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,11 @@
66
use React\Stream\ReadableStreamInterface;
77
use React\Stream\WritableStreamInterface;
88
use React\Stream\Util;
9+
use Clue\React\Utf8\Sequencer as Utf8Sequencer;
10+
use Clue\React\Term\ControlCodeParser;
911

1012
class Readline extends EventEmitter implements ReadableStreamInterface
1113
{
12-
const KEY_BACKSPACE = "\x7f";
13-
const KEY_ENTER = "\n";
14-
const KEY_TAB = "\t";
15-
16-
const ESC_SEQUENCE = "\033[";
17-
const ESC_LEFT = "D";
18-
const ESC_RIGHT = "C";
19-
const ESC_UP = "A";
20-
const ESC_DOWN = "B";
21-
const ESC_HOME = "1~";
22-
const ESC_INS = "2~";
23-
const ESC_DEL = "3~";
24-
const ESC_END = "4~";
25-
26-
const ESC_F10 = "20~";
27-
2814
private $prompt = '';
2915
private $linebuffer = '';
3016
private $linepos = 0;
@@ -48,58 +34,46 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf
4834
return $this->close();
4935
}
5036

51-
$this->sequencer = new Sequencer();
52-
$this->sequencer->addSequence(self::KEY_ENTER, array($this, 'onKeyEnter'));
53-
$this->sequencer->addSequence(self::KEY_BACKSPACE, array($this, 'onKeyBackspace'));
54-
$this->sequencer->addSequence(self::KEY_TAB, array($this, 'onKeyTab'));
55-
56-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_LEFT, array($this, 'onKeyLeft'));
57-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_RIGHT, array($this, 'onKeyRight'));
58-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_UP, array($this, 'onKeyUp'));
59-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_DOWN, array($this, 'onKeyDown'));
60-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_HOME, array($this, 'onKeyHome'));
61-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_INS, array($this, 'onKeyInsert'));
62-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_DEL, array($this, 'onKeyDelete'));
63-
$this->sequencer->addSequence(self::ESC_SEQUENCE . self::ESC_END, array($this, 'onKeyEnd'));
64-
65-
$expect = 0;
66-
$char = '';
37+
// push input through control code parser
38+
$parser = new ControlCodeParser($input);
39+
6740
$that = $this;
68-
$this->sequencer->addFallback('', function ($byte) use (&$expect, &$char, $that) {
69-
if ($expect === 0) {
70-
$code = ord($byte);
71-
// count number of bytes expected for this UTF-8 multi-byte character
72-
$expect = 1;
73-
if ($code & 128 && $code & 64) {
74-
++$expect;
75-
if ($code & 32) {
76-
++$expect;
77-
if ($code & 16) {
78-
++$expect;
79-
}
80-
}
81-
}
41+
$codes = array(
42+
"\n" => 'onKeyEnter',
43+
"\x7f" => 'onKeyBackspace',
44+
"\t" => 'onKeyTab',
45+
46+
"\033[A" => 'onKeyUp',
47+
"\033[B" => 'onKeyDown',
48+
"\033[C" => 'onKeyRight',
49+
"\033[D" => 'onKeyLeft',
50+
51+
"\033[1~" => 'onKeyHome',
52+
"\033[2~" => 'onKeyInsert',
53+
"\033[3~" => 'onKeyDelete',
54+
"\033[4~" => 'onKeyEnd',
55+
56+
// "\033[20~" => 'onKeyF10',
57+
);
58+
$decode = function ($code) use ($codes, $that) {
59+
if (isset($codes[$code])) {
60+
$method = $codes[$code];
61+
$that->$method($code);
62+
return;
8263
}
83-
$char .= $byte;
84-
--$expect;
85-
86-
// forward buffered bytes as a single multi byte character once last byte has been read
87-
if ($expect === 0) {
88-
$save = $char;
89-
$char = '';
90-
$that->onFallback($save);
91-
}
92-
});
64+
};
65+
66+
$parser->on('csi', $decode);
67+
$parser->on('c0', $decode);
9368

94-
$this->sequencer->addFallback(self::ESC_SEQUENCE, function ($bytes) {
95-
echo 'unknown sequence: ' . ord($bytes) . PHP_EOL;
96-
});
69+
// push resulting data through utf8 sequencer
70+
$utf8 = new Utf8Sequencer($parser);
71+
$utf8->on('data', array($this, 'onFallback'));
9772

98-
// input data emits a single char into readline
99-
$input->on('data', array($this->sequencer, 'push'));
100-
$input->on('end', array($this, 'handleEnd'));
101-
$input->on('error', array($this, 'handleError'));
102-
$input->on('close', array($this, 'close'));
73+
// process all stream events (forwarded from input stream)
74+
$utf8->on('end', array($this, 'handleEnd'));
75+
$utf8->on('error', array($this, 'handleError'));
76+
$utf8->on('close', array($this, 'close'));
10377
}
10478

10579
/**
@@ -519,7 +493,7 @@ public function onFallback($chars)
519493
$post = $this->substr($this->linebuffer, $this->linepos);
520494

521495
$this->linebuffer = $pre . $chars . $post;
522-
++$this->linepos;
496+
$this->linepos += $this->strlen($chars);
523497

524498
$this->redraw();
525499
}

src/Sequencer.php

Lines changed: 0 additions & 109 deletions
This file was deleted.

tests/ReadlineTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,27 @@ public function testMovingCursorWithoutEchoDoesNotNeedToRedraw()
191191
$this->assertSame($this->readline, $this->readline->moveCursorBy(2));
192192
}
193193

194+
public function testDataEventWillBeEmittedForCompleteLine()
195+
{
196+
$this->readline->on('data', $this->expectCallableOnceWith('hello'));
197+
198+
$this->pushInputBytes($this->readline, "hello\n");
199+
}
200+
201+
public function testDataEventWillNotBeEmittedForIncompleteLine()
202+
{
203+
$this->readline->on('data', $this->expectCallableNever());
204+
205+
$this->pushInputBytes($this->readline, "hello");
206+
}
207+
208+
public function testDataEventWillBeEmittedForEmptyLine()
209+
{
210+
$this->readline->on('data', $this->expectCallableOnceWith(''));
211+
212+
$this->pushInputBytes($this->readline, "\n");
213+
}
214+
194215
public function testWriteSimpleCharWritesOnce()
195216
{
196217
$this->output->expects($this->once())->method('write')->with($this->equalTo("\r\033[K" . "k"));

0 commit comments

Comments
 (0)