Skip to content

Commit 9379345

Browse files
authored
Merge pull request #32 from clue-labs/readline
The Readline is now a well behaving readable stream
2 parents a7a2b8f + eeb7d4f commit 9379345

File tree

5 files changed

+181
-21
lines changed

5 files changed

+181
-21
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ $stdio->on('line', function ($line) {
7575
You can control various aspects of the console input through the [`Readline`](#readline),
7676
so read on..
7777

78+
Using the `line` event is the recommended way to wait for user input.
79+
Alternatively, using the `Readline` as a readable stream is considered advanced
80+
usage.
81+
7882
### Readline
7983

8084
The [`Readline`](#readline) class is responsible for reacting to user input and presenting a prompt to the user.
@@ -89,6 +93,12 @@ You can access the current instance through the [`Stdio`](#stdio):
8993
$readline = $stdio->getReadline();
9094
```
9195

96+
See above for waiting for user input.
97+
Alternatively, the `Readline` is also a well-behaving readable stream
98+
(implementing React's `ReadableStreamInterface`) that emits each complete
99+
line as a `data` event (without the trailing newline). This is considered
100+
advanced usage.
101+
92102
#### Prompt
93103

94104
The *prompt* will be written at the beginning of the *user input line*, right before the *user input buffer*.

src/Readline.php

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
namespace Clue\React\Stdio;
44

55
use Evenement\EventEmitter;
6+
use React\Stream\ReadableStreamInterface;
7+
use React\Stream\WritableStreamInterface;
8+
use React\Stream\Util;
69

7-
class Readline extends EventEmitter
10+
class Readline extends EventEmitter implements ReadableStreamInterface
811
{
912
const KEY_BACKSPACE = "\x7f";
1013
const KEY_ENTER = "\n";
@@ -31,13 +34,20 @@ class Readline extends EventEmitter
3134
private $history = null;
3235
private $encoding = 'utf-8';
3336

37+
private $input;
3438
private $output;
3539
private $sequencer;
40+
private $closed = false;
3641

37-
public function __construct($output)
42+
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
3843
{
44+
$this->input = $input;
3945
$this->output = $output;
4046

47+
if (!$this->input->isReadable()) {
48+
return $this->close();
49+
}
50+
4151
$this->sequencer = new Sequencer();
4252
$this->sequencer->addSequence(self::KEY_ENTER, array($this, 'onKeyEnter'));
4353
$this->sequencer->addSequence(self::KEY_BACKSPACE, array($this, 'onKeyBackspace'));
@@ -84,6 +94,12 @@ public function __construct($output)
8494
$this->sequencer->addFallback(self::ESC_SEQUENCE, function ($bytes) {
8595
echo 'unknown sequence: ' . ord($bytes) . PHP_EOL;
8696
});
97+
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'));
87103
}
88104

89105
/**
@@ -369,7 +385,7 @@ public function redraw()
369385
// write output, then move back $reverse chars (by sending backspace)
370386
$output .= $buffer . str_repeat("\x08", $this->strwidth($buffer) - $this->getCursorCell());
371387
}
372-
$this->write($output);
388+
$this->output->write($output);
373389

374390
return $this;
375391
}
@@ -389,18 +405,12 @@ public function redraw()
389405
public function clear()
390406
{
391407
if ($this->prompt !== '' || ($this->echo !== false && $this->linebuffer !== '')) {
392-
$this->write("\r\033[K");
408+
$this->output->write("\r\033[K");
393409
}
394410

395411
return $this;
396412
}
397413

398-
/** @internal */
399-
public function onChar($char)
400-
{
401-
$this->sequencer->push($char);
402-
}
403-
404414
/** @internal */
405415
public function onKeyBackspace()
406416
{
@@ -449,7 +459,7 @@ public function onKeyTab()
449459
public function onKeyEnter()
450460
{
451461
if ($this->echo !== false) {
452-
$this->write("\n");
462+
$this->output->write("\n");
453463
}
454464
$this->processLine();
455465
}
@@ -571,8 +581,54 @@ private function strwidth($str)
571581
return mb_strwidth($str, $this->encoding);
572582
}
573583

574-
protected function write($data)
584+
/** @internal */
585+
public function handleEnd()
586+
{
587+
if (!$this->closed) {
588+
$this->emit('end');
589+
$this->close();
590+
}
591+
}
592+
593+
/** @internal */
594+
public function handleError(\Exception $error)
595+
{
596+
$this->emit('error', array($error));
597+
$this->close();
598+
}
599+
600+
public function isReadable()
601+
{
602+
return !$this->closed && $this->input->isReadable();
603+
}
604+
605+
public function pause()
606+
{
607+
$this->input->pause();
608+
}
609+
610+
public function resume()
575611
{
576-
$this->output->write($data);
612+
$this->input->resume();
613+
}
614+
615+
public function pipe(WritableStreamInterface $dest, array $options = array())
616+
{
617+
Util::pipe($this, $dest, $options);
618+
619+
return $dest;
620+
}
621+
622+
public function close()
623+
{
624+
if ($this->closed) {
625+
return;
626+
}
627+
628+
$this->closed = true;
629+
630+
$this->input->close();
631+
632+
$this->emit('close');
577633
}
578634
}

src/Stdio.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,17 @@ public function __construct(LoopInterface $loop, $input = true)
2222

2323
$this->output = new Stdout(STDOUT);
2424

25-
$this->readline = $readline = new Readline($this->output);
25+
$this->readline = new Readline($this->input, $this->output);
2626

2727
$that = $this;
2828

29-
// input data emits a single char into readline
30-
$this->input->on('data', function ($data) use ($that, $readline) {
29+
// stdin emits single chars
30+
$this->input->on('data', function ($data) use ($that) {
3131
$that->emit('char', array($data, $that));
32-
$readline->onChar($data);
3332
});
3433

3534
// readline data emits a new line
36-
$readline->on('data', function($line) use ($that) {
35+
$this->readline->on('data', function($line) use ($that) {
3736
$that->emit('line', array($line, $that));
3837
});
3938

tests/ReadlineTest.php

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
<?php
22

33
use Clue\React\Stdio\Readline;
4+
use React\Stream\ReadableStream;
45

56
class ReadlineTest extends TestCase
67
{
8+
private $input;
9+
private $output;
10+
private $readline;
11+
712
public function setUp()
813
{
9-
$this->output = $this->getMockBuilder('Clue\React\Stdio\Stdout')->disableOriginalConstructor()->getMock();
10-
$this->readline = new Readline($this->output);
14+
$this->input = new ReadableStream();
15+
$this->output = $this->getMock('React\Stream\WritableStreamInterface');
16+
17+
$this->readline = new Readline($this->input, $this->output);
1118
}
1219

1320
public function testSettersReturnSelf()
@@ -476,10 +483,79 @@ public function testSetInputDuringEmitKeepsInput()
476483
$this->assertEquals('test', $readline->getInput());
477484
}
478485

486+
public function testEmitErrorWillEmitErrorAndClose()
487+
{
488+
$this->readline->on('error', $this->expectCallableOnce());
489+
$this->readline->on('close', $this->expectCallableOnce());
490+
491+
$this->input->emit('error', array(new \RuntimeException()));
492+
493+
$this->assertFalse($this->readline->isReadable());
494+
}
495+
496+
public function testEmitEndWillEmitEndAndClose()
497+
{
498+
$this->readline->on('end', $this->expectCallableOnce());
499+
$this->readline->on('close', $this->expectCallableOnce());
500+
501+
$this->input->emit('end');
502+
503+
$this->assertFalse($this->readline->isReadable());
504+
}
505+
506+
public function testEmitCloseWillEmitClose()
507+
{
508+
$this->readline->on('end', $this->expectCallableNever());
509+
$this->readline->on('close', $this->expectCallableOnce());
510+
511+
$this->input->emit('close');
512+
513+
$this->assertFalse($this->readline->isReadable());
514+
}
515+
516+
public function testClosedStdinWillCloseReadline()
517+
{
518+
$this->input = $this->getMock('React\Stream\ReadableStreamInterface');
519+
$this->input->expects($this->once())->method('isReadable')->willReturn(false);
520+
521+
$this->readline = new Readline($this->input, $this->output);
522+
523+
$this->assertFalse($this->readline->isReadable());
524+
}
525+
526+
public function testPauseWillBeForwarded()
527+
{
528+
$this->input = $this->getMock('React\Stream\ReadableStreamInterface');
529+
$this->input->expects($this->once())->method('pause');
530+
531+
$this->readline = new Readline($this->input, $this->output);
532+
533+
$this->readline->pause();
534+
}
535+
536+
public function testResumeWillBeForwarded()
537+
{
538+
$this->input = $this->getMock('React\Stream\ReadableStreamInterface');
539+
$this->input->expects($this->once())->method('resume');
540+
541+
$this->readline = new Readline($this->input, $this->output);
542+
543+
$this->readline->resume();
544+
}
545+
546+
public function testPipeWillReturnDest()
547+
{
548+
$dest = $this->getMock('React\Stream\WritableStreamInterface');
549+
550+
$ret = $this->readline->pipe($dest);
551+
552+
$this->assertEquals($dest, $ret);
553+
}
554+
479555
private function pushInputBytes(Readline $readline, $bytes)
480556
{
481557
foreach (str_split($bytes, 1) as $byte) {
482-
$readline->onChar($byte);
558+
$this->input->emit('data', array($byte));
483559
}
484560
}
485561
}

tests/StdioTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
use React\EventLoop\Factory;
4+
use Clue\React\Stdio\Stdio;
5+
6+
class StdioTest extends TestCase
7+
{
8+
private $loop;
9+
10+
public function setUp()
11+
{
12+
$this->loop = Factory::create();
13+
}
14+
15+
public function testCtor()
16+
{
17+
$stdio = new Stdio($this->loop);
18+
}
19+
}

0 commit comments

Comments
 (0)