Skip to content

Commit eeb7d4f

Browse files
committed
The Readline is now a well behaving readable stream
1 parent dc6294e commit eeb7d4f

File tree

3 files changed

+141
-1
lines changed

3 files changed

+141
-1
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: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
use Evenement\EventEmitter;
66
use React\Stream\ReadableStreamInterface;
77
use React\Stream\WritableStreamInterface;
8+
use React\Stream\Util;
89

9-
class Readline extends EventEmitter
10+
class Readline extends EventEmitter implements ReadableStreamInterface
1011
{
1112
const KEY_BACKSPACE = "\x7f";
1213
const KEY_ENTER = "\n";
@@ -36,11 +37,17 @@ class Readline extends EventEmitter
3637
private $input;
3738
private $output;
3839
private $sequencer;
40+
private $closed = false;
3941

4042
public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
4143
{
44+
$this->input = $input;
4245
$this->output = $output;
4346

47+
if (!$this->input->isReadable()) {
48+
return $this->close();
49+
}
50+
4451
$this->sequencer = new Sequencer();
4552
$this->sequencer->addSequence(self::KEY_ENTER, array($this, 'onKeyEnter'));
4653
$this->sequencer->addSequence(self::KEY_BACKSPACE, array($this, 'onKeyBackspace'));
@@ -90,6 +97,9 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf
9097

9198
// input data emits a single char into readline
9299
$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'));
93103
}
94104

95105
/**
@@ -570,4 +580,55 @@ private function strwidth($str)
570580
{
571581
return mb_strwidth($str, $this->encoding);
572582
}
583+
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()
611+
{
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');
633+
}
573634
}

tests/ReadlineTest.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,75 @@ public function testSetInputDuringEmitKeepsInput()
483483
$this->assertEquals('test', $readline->getInput());
484484
}
485485

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+
486555
private function pushInputBytes(Readline $readline, $bytes)
487556
{
488557
foreach (str_split($bytes, 1) as $byte) {

0 commit comments

Comments
 (0)