Skip to content

Commit

Permalink
The Readline is now a well behaving readable stream
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Jun 10, 2016
1 parent dc6294e commit eeb7d4f
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 1 deletion.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ $stdio->on('line', function ($line) {
You can control various aspects of the console input through the [`Readline`](#readline),
so read on..

Using the `line` event is the recommended way to wait for user input.
Alternatively, using the `Readline` as a readable stream is considered advanced
usage.

### Readline

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

See above for waiting for user input.
Alternatively, the `Readline` is also a well-behaving readable stream
(implementing React's `ReadableStreamInterface`) that emits each complete
line as a `data` event (without the trailing newline). This is considered
advanced usage.

#### Prompt

The *prompt* will be written at the beginning of the *user input line*, right before the *user input buffer*.
Expand Down
63 changes: 62 additions & 1 deletion src/Readline.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
use Evenement\EventEmitter;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableStreamInterface;
use React\Stream\Util;

class Readline extends EventEmitter
class Readline extends EventEmitter implements ReadableStreamInterface
{
const KEY_BACKSPACE = "\x7f";
const KEY_ENTER = "\n";
Expand Down Expand Up @@ -36,11 +37,17 @@ class Readline extends EventEmitter
private $input;
private $output;
private $sequencer;
private $closed = false;

public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output)
{
$this->input = $input;
$this->output = $output;

if (!$this->input->isReadable()) {
return $this->close();
}

$this->sequencer = new Sequencer();
$this->sequencer->addSequence(self::KEY_ENTER, array($this, 'onKeyEnter'));
$this->sequencer->addSequence(self::KEY_BACKSPACE, array($this, 'onKeyBackspace'));
Expand Down Expand Up @@ -90,6 +97,9 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf

// input data emits a single char into readline
$input->on('data', array($this->sequencer, 'push'));
$input->on('end', array($this, 'handleEnd'));
$input->on('error', array($this, 'handleError'));
$input->on('close', array($this, 'close'));
}

/**
Expand Down Expand Up @@ -570,4 +580,55 @@ private function strwidth($str)
{
return mb_strwidth($str, $this->encoding);
}

/** @internal */
public function handleEnd()
{
if (!$this->closed) {
$this->emit('end');
$this->close();
}
}

/** @internal */
public function handleError(\Exception $error)
{
$this->emit('error', array($error));
$this->close();
}

public function isReadable()
{
return !$this->closed && $this->input->isReadable();
}

public function pause()
{
$this->input->pause();
}

public function resume()
{
$this->input->resume();
}

public function pipe(WritableStreamInterface $dest, array $options = array())
{
Util::pipe($this, $dest, $options);

return $dest;
}

public function close()
{
if ($this->closed) {
return;
}

$this->closed = true;

$this->input->close();

$this->emit('close');
}
}
69 changes: 69 additions & 0 deletions tests/ReadlineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,75 @@ public function testSetInputDuringEmitKeepsInput()
$this->assertEquals('test', $readline->getInput());
}

public function testEmitErrorWillEmitErrorAndClose()
{
$this->readline->on('error', $this->expectCallableOnce());
$this->readline->on('close', $this->expectCallableOnce());

$this->input->emit('error', array(new \RuntimeException()));

$this->assertFalse($this->readline->isReadable());
}

public function testEmitEndWillEmitEndAndClose()
{
$this->readline->on('end', $this->expectCallableOnce());
$this->readline->on('close', $this->expectCallableOnce());

$this->input->emit('end');

$this->assertFalse($this->readline->isReadable());
}

public function testEmitCloseWillEmitClose()
{
$this->readline->on('end', $this->expectCallableNever());
$this->readline->on('close', $this->expectCallableOnce());

$this->input->emit('close');

$this->assertFalse($this->readline->isReadable());
}

public function testClosedStdinWillCloseReadline()
{
$this->input = $this->getMock('React\Stream\ReadableStreamInterface');
$this->input->expects($this->once())->method('isReadable')->willReturn(false);

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

$this->assertFalse($this->readline->isReadable());
}

public function testPauseWillBeForwarded()
{
$this->input = $this->getMock('React\Stream\ReadableStreamInterface');
$this->input->expects($this->once())->method('pause');

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

$this->readline->pause();
}

public function testResumeWillBeForwarded()
{
$this->input = $this->getMock('React\Stream\ReadableStreamInterface');
$this->input->expects($this->once())->method('resume');

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

$this->readline->resume();
}

public function testPipeWillReturnDest()
{
$dest = $this->getMock('React\Stream\WritableStreamInterface');

$ret = $this->readline->pipe($dest);

$this->assertEquals($dest, $ret);
}

private function pushInputBytes(Readline $readline, $bytes)
{
foreach (str_split($bytes, 1) as $byte) {
Expand Down

0 comments on commit eeb7d4f

Please sign in to comment.