Skip to content

Commit ee131bf

Browse files
committed
The Stdio is now a well-behaving duplex stream
1 parent 5c9e765 commit ee131bf

File tree

5 files changed

+354
-17
lines changed

5 files changed

+354
-17
lines changed

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,17 @@ $loop = React\EventLoop\Factory::create();
4141
$stdio = new Stdio($loop);
4242
```
4343

44+
See below for waiting for user input and writing output.
45+
Alternatively, the `Stdio` is also a well-behaving duplex stream
46+
(implementing React's `DuplexStreamInterface`) that emits each complete
47+
line as a `data` event (including the trailing newline). This is considered
48+
advanced usage.
49+
4450
#### Output
4551

52+
The `Stdio` is a well-behaving writable stream
53+
implementing React's `WritableStreamInterface`.
54+
4655
The `writeln($line)` method can be used to print a line to console output.
4756
A trailing newline will be added automatically.
4857

@@ -58,9 +67,23 @@ $stdio->write('hello');
5867
$stdio->write(" world\n");
5968
```
6069

70+
The `overwrite($text)` method can be used to overwrite/replace the last
71+
incomplete line with the given text:
72+
73+
```php
74+
$stdio->write('Loading…');
75+
$stdio->overwrite('Done!');
76+
```
77+
78+
Alternatively, you can also use the `Stdio` as a writable stream.
79+
You can `pipe()` any readable stream into this stream.
80+
6181
#### Input
6282

63-
The `Stdio` will emit a `line` event for every line read from console input.
83+
The `Stdio` is a well-behaving readable stream
84+
implementing React's `ReadableStreamInterface`.
85+
86+
It will emit a `line` event for every line read from console input.
6487
The event will contain the input buffer as-is, without the trailing newline.
6588
You can register any number of event handlers like this:
6689

@@ -79,6 +102,10 @@ Using the `line` event is the recommended way to wait for user input.
79102
Alternatively, using the `Readline` as a readable stream is considered advanced
80103
usage.
81104

105+
Alternatively, you can also use the `Stdio` as a readable stream, which emits
106+
each complete line as a `data` event (including the trailing newline).
107+
This can be used to `pipe()` this stream into other writable streams.
108+
82109
### Readline
83110

84111
The [`Readline`](#readline) class is responsible for reacting to user input and presenting a prompt to the user.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"require": {
1414
"php": ">=5.3",
1515
"react/event-loop": "0.3.*|0.4.*",
16-
"react/stream": "0.3.*|0.4.*"
16+
"react/stream": "^0.4.2"
1717
},
1818
"autoload": {
1919
"psr-4": { "Clue\\React\\Stdio\\": "src/" }

examples/progress.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,16 @@
88
$loop = React\EventLoop\Factory::create();
99

1010
$stdio = new Stdio($loop);
11-
$stdio->getInput()->close();
12-
1311
$stdio->writeln('Will print (fake) progress and then exit');
1412

1513
$progress = new ProgressBar($stdio);
1614
$progress->setMaximum(mt_rand(20, 200));
1715

18-
$loop->addPeriodicTimer(0.2, function ($timer) use ($stdio, $progress) {
16+
$loop->addPeriodicTimer(0.1, function ($timer) use ($stdio, $progress) {
1917
$progress->advance();
2018

2119
if ($progress->isComplete()) {
22-
$stdio->overwrite();
23-
$stdio->writeln("Finished processing nothing!");
20+
$stdio->overwrite("Finished processing nothing!" . PHP_EOL);
2421

2522
$stdio->end();
2623
$timer->cancel();

src/Stdio.php

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
namespace Clue\React\Stdio;
44

5-
use React\Stream\CompositeStream;
5+
use Evenement\EventEmitter;
6+
use React\Stream\DuplexStreamInterface;
67
use React\EventLoop\LoopInterface;
78
use React\Stream\ReadableStreamInterface;
89
use React\Stream\WritableStreamInterface;
10+
use React\Stream\Util;
911

10-
class Stdio extends CompositeStream
12+
class Stdio extends EventEmitter implements DuplexStreamInterface
1113
{
1214
private $input;
1315
private $output;
1416
private $readline;
1517

18+
private $ending = false;
19+
private $closed = false;
1620
private $incompleteLine = '';
1721

1822
public function __construct(LoopInterface $loop, ReadableStreamInterface $input = null, WritableStreamInterface $output = null, Readline $readline = null)
@@ -35,19 +39,27 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input
3539

3640
$that = $this;
3741

38-
// stdin emits single chars
39-
$this->input->on('data', function ($data) use ($that) {
40-
$that->emit('char', array($data, $that));
41-
});
42-
4342
// readline data emits a new line
4443
$incomplete =& $this->incompleteLine;
4544
$this->readline->on('data', function($line) use ($that, &$incomplete) {
4645
// readline emits a new line on enter, so start with a blank line
4746
$incomplete = '';
4847

48+
// emit data with trailing newline in order to preserve readable API
49+
$that->emit('data', array($line . PHP_EOL));
50+
51+
// emit custom line event for ease of use
4952
$that->emit('line', array($line, $that));
5053
});
54+
55+
// handle all input events (readline forwards all input events)
56+
$this->readline->on('error', array($this, 'handleError'));
57+
$this->readline->on('end', array($this, 'handleEnd'));
58+
$this->readline->on('close', array($this, 'handleCloseInput'));
59+
60+
// handle all output events
61+
$this->output->on('error', array($this, 'handleError'));
62+
$this->output->on('close', array($this, 'handleCloseOutput'));
5163
}
5264

5365
public function pause()
@@ -60,6 +72,23 @@ public function resume()
6072
$this->input->resume();
6173
}
6274

75+
public function isReadable()
76+
{
77+
return $this->input->isReadable();
78+
}
79+
80+
public function isWritable()
81+
{
82+
return $this->output->isWritable();
83+
}
84+
85+
public function pipe(WritableStreamInterface $dest, array $options = array())
86+
{
87+
Util::pipe($this, $dest, $options);
88+
89+
return $dest;
90+
}
91+
6392
public function handleBuffer()
6493
{
6594
$that = $this;
@@ -70,7 +99,7 @@ public function handleBuffer()
7099

71100
public function write($data)
72101
{
73-
if ((string)$data === '') {
102+
if ($this->ending || (string)$data === '') {
74103
return;
75104
}
76105

@@ -159,18 +188,33 @@ public function overwrite($data = '')
159188

160189
public function end($data = null)
161190
{
191+
if ($this->ending) {
192+
return;
193+
}
194+
162195
if ($data !== null) {
163196
$this->write($data);
164197
}
165198

199+
$this->ending = true;
200+
201+
// clear readline output, close input and end output
166202
$this->readline->setInput('')->setPrompt('')->clear();
167-
$this->input->pause();
203+
$this->input->close();
168204
$this->output->end();
169205
}
170206

171207
public function close()
172208
{
173-
$this->readline->setInput('')->setPrompt('')->clear();
209+
if ($this->closed) {
210+
return;
211+
}
212+
213+
$this->ending = true;
214+
$this->closed = true;
215+
216+
// clear readline output and then close
217+
$this->readline->setInput('')->setPrompt('')->clear()->close();
174218
$this->input->close();
175219
$this->output->close();
176220
}
@@ -194,4 +238,33 @@ private function width($str)
194238
{
195239
return mb_strwidth($str, 'utf-8') - 2 * substr_count($str, "\x08");
196240
}
241+
242+
/** @internal */
243+
public function handleError(\Exception $e)
244+
{
245+
$this->emit('error', array($e));
246+
$this->close();
247+
}
248+
249+
/** @internal */
250+
public function handleEnd()
251+
{
252+
$this->emit('end', array());
253+
}
254+
255+
/** @internal */
256+
public function handleCloseInput()
257+
{
258+
if (!$this->output->isWritable()) {
259+
$this->close();
260+
}
261+
}
262+
263+
/** @internal */
264+
public function handleCloseOutput()
265+
{
266+
if (!$this->input->isReadable()) {
267+
$this->close();
268+
}
269+
}
197270
}

0 commit comments

Comments
 (0)