From b6cf7f357c935f3ef3ea1ae42d5faa5ae93db72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 4 Jun 2016 13:12:25 +0200 Subject: [PATCH 1/3] The Stdin is now internal to the Readline --- src/Readline.php | 7 ++++++- src/Stdio.php | 9 ++++----- tests/ReadlineTest.php | 3 ++- tests/StdioTest.php | 19 +++++++++++++++++++ 4 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 tests/StdioTest.php diff --git a/src/Readline.php b/src/Readline.php index 1b22964..ac475f6 100644 --- a/src/Readline.php +++ b/src/Readline.php @@ -3,6 +3,8 @@ namespace Clue\React\Stdio; use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\WritableStreamInterface; class Readline extends EventEmitter { @@ -34,8 +36,11 @@ class Readline extends EventEmitter private $output; private $sequencer; - public function __construct($output) + public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output) { + // input data emits a single char into readline + $input->on('data', array($this, 'onChar')); + $this->output = $output; $this->sequencer = new Sequencer(); diff --git a/src/Stdio.php b/src/Stdio.php index 2d4f002..37e79d9 100644 --- a/src/Stdio.php +++ b/src/Stdio.php @@ -22,18 +22,17 @@ public function __construct(LoopInterface $loop, $input = true) $this->output = new Stdout(STDOUT); - $this->readline = $readline = new Readline($this->output); + $this->readline = new Readline($this->input, $this->output); $that = $this; - // input data emits a single char into readline - $this->input->on('data', function ($data) use ($that, $readline) { + // stdin emits single chars + $this->input->on('data', function ($data) use ($that) { $that->emit('char', array($data, $that)); - $readline->onChar($data); }); // readline data emits a new line - $readline->on('data', function($line) use ($that) { + $this->readline->on('data', function($line) use ($that) { $that->emit('line', array($line, $that)); }); diff --git a/tests/ReadlineTest.php b/tests/ReadlineTest.php index 132977c..c4782ff 100644 --- a/tests/ReadlineTest.php +++ b/tests/ReadlineTest.php @@ -6,8 +6,9 @@ class ReadlineTest extends TestCase { public function setUp() { + $this->input = $this->getMock('React\Stream\ReadableStreamInterface'); $this->output = $this->getMockBuilder('Clue\React\Stdio\Stdout')->disableOriginalConstructor()->getMock(); - $this->readline = new Readline($this->output); + $this->readline = new Readline($this->input, $this->output); } public function testSettersReturnSelf() diff --git a/tests/StdioTest.php b/tests/StdioTest.php new file mode 100644 index 0000000..abe2d6f --- /dev/null +++ b/tests/StdioTest.php @@ -0,0 +1,19 @@ +loop = Factory::create(); + } + + public function testCtor() + { + $stdio = new Stdio($this->loop); + } +} From dc6294e99a62eb622472b53e1f095273f8736e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Jun 2016 13:04:47 +0200 Subject: [PATCH 2/3] Use proper streaming interfaces for Readline input and output --- src/Readline.php | 24 +++++++----------------- tests/ReadlineTest.php | 12 +++++++++--- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/Readline.php b/src/Readline.php index ac475f6..043a45a 100644 --- a/src/Readline.php +++ b/src/Readline.php @@ -33,14 +33,12 @@ class Readline extends EventEmitter private $history = null; private $encoding = 'utf-8'; + private $input; private $output; private $sequencer; public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output) { - // input data emits a single char into readline - $input->on('data', array($this, 'onChar')); - $this->output = $output; $this->sequencer = new Sequencer(); @@ -89,6 +87,9 @@ public function __construct(ReadableStreamInterface $input, WritableStreamInterf $this->sequencer->addFallback(self::ESC_SEQUENCE, function ($bytes) { echo 'unknown sequence: ' . ord($bytes) . PHP_EOL; }); + + // input data emits a single char into readline + $input->on('data', array($this->sequencer, 'push')); } /** @@ -374,7 +375,7 @@ public function redraw() // write output, then move back $reverse chars (by sending backspace) $output .= $buffer . str_repeat("\x08", $this->strwidth($buffer) - $this->getCursorCell()); } - $this->write($output); + $this->output->write($output); return $this; } @@ -394,18 +395,12 @@ public function redraw() public function clear() { if ($this->prompt !== '' || ($this->echo !== false && $this->linebuffer !== '')) { - $this->write("\r\033[K"); + $this->output->write("\r\033[K"); } return $this; } - /** @internal */ - public function onChar($char) - { - $this->sequencer->push($char); - } - /** @internal */ public function onKeyBackspace() { @@ -454,7 +449,7 @@ public function onKeyTab() public function onKeyEnter() { if ($this->echo !== false) { - $this->write("\n"); + $this->output->write("\n"); } $this->processLine(); } @@ -575,9 +570,4 @@ private function strwidth($str) { return mb_strwidth($str, $this->encoding); } - - protected function write($data) - { - $this->output->write($data); - } } diff --git a/tests/ReadlineTest.php b/tests/ReadlineTest.php index c4782ff..a36c22d 100644 --- a/tests/ReadlineTest.php +++ b/tests/ReadlineTest.php @@ -1,13 +1,19 @@ input = $this->getMock('React\Stream\ReadableStreamInterface'); - $this->output = $this->getMockBuilder('Clue\React\Stdio\Stdout')->disableOriginalConstructor()->getMock(); + $this->input = new ReadableStream(); + $this->output = $this->getMock('React\Stream\WritableStreamInterface'); + $this->readline = new Readline($this->input, $this->output); } @@ -480,7 +486,7 @@ public function testSetInputDuringEmitKeepsInput() private function pushInputBytes(Readline $readline, $bytes) { foreach (str_split($bytes, 1) as $byte) { - $readline->onChar($byte); + $this->input->emit('data', array($byte)); } } } From eeb7d4fcbd2a6c1afba401aa13e8fb1dbc1da0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 6 Jun 2016 22:15:31 +0200 Subject: [PATCH 3/3] The Readline is now a well behaving readable stream --- README.md | 10 ++++++ src/Readline.php | 63 +++++++++++++++++++++++++++++++++++++- tests/ReadlineTest.php | 69 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0288a5c..e6c99d2 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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*. diff --git a/src/Readline.php b/src/Readline.php index 043a45a..bac4209 100644 --- a/src/Readline.php +++ b/src/Readline.php @@ -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"; @@ -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')); @@ -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')); } /** @@ -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'); + } } diff --git a/tests/ReadlineTest.php b/tests/ReadlineTest.php index a36c22d..d7eea80 100644 --- a/tests/ReadlineTest.php +++ b/tests/ReadlineTest.php @@ -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) {