Skip to content

Commit 6a3f864

Browse files
committed
Support incomplete lines for write(), writeln() and overwrite()
1 parent a6928b1 commit 6a3f864

File tree

2 files changed

+255
-14
lines changed

2 files changed

+255
-14
lines changed

src/Stdio.php

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ class Stdio extends CompositeStream
1111
{
1212
private $input;
1313
private $output;
14-
1514
private $readline;
16-
private $needsNewline = false;
15+
16+
private $incompleteLine = '';
1717

1818
public function __construct(LoopInterface $loop, ReadableStreamInterface $input = null, WritableStreamInterface $output = null, Readline $readline = null)
1919
{
@@ -41,7 +41,11 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input
4141
});
4242

4343
// readline data emits a new line
44-
$this->readline->on('data', function($line) use ($that) {
44+
$incomplete =& $this->incompleteLine;
45+
$this->readline->on('data', function($line) use ($that, &$incomplete) {
46+
// readline emits a new line on enter, so start with a blank line
47+
$incomplete = '';
48+
4549
$that->emit('line', array($line, $that));
4650
});
4751
}
@@ -66,25 +70,40 @@ public function handleBuffer()
6670

6771
public function write($data)
6872
{
69-
// switch back to last output position
73+
// clear readline prompt in order to overwrite with data
7074
$this->readline->clear();
7175

72-
// Erase characters from cursor to end of line
73-
$this->output->write("\r\033[K");
74-
75-
// move one line up?
76-
if ($this->needsNewline) {
76+
// move one line up if the last write did not end with a newline
77+
if ($this->incompleteLine !== '') {
7778
$this->output->write("\033[A");
79+
$this->output->write("\r\033[" . $this->width($this->incompleteLine) . "C");
7880
}
7981

82+
// write actual data
8083
$this->output->write($data);
8184

82-
$this->needsNewline = substr($data, -1) !== "\n";
85+
// following write will have have to append to this line if it does not end with a newline
86+
$endsWithNewline = substr($data, -1) === "\n";
8387

84-
// repeat current prompt + linebuffer
85-
if ($this->needsNewline) {
88+
if ($endsWithNewline) {
89+
// line ends with newline, so this is line is considered complete
90+
$this->incompleteLine = '';
91+
} else {
92+
// always end data with newline in order to append readline on next line
8693
$this->output->write("\n");
94+
95+
$lastNewline = strrpos($data, "\n");
96+
97+
if ($lastNewline === false) {
98+
// contains no newline at all, everything is incomplete
99+
$this->incompleteLine .= $data;
100+
} else {
101+
// contains a newline, everything behind it is incomplete
102+
$this->incompleteLine = (string)substr($data, $lastNewline + 1);
103+
}
87104
}
105+
106+
// restore original readline prompt and line buffer
88107
$this->readline->redraw();
89108
}
90109

@@ -95,9 +114,13 @@ public function writeln($line)
95114

96115
public function overwrite($data = '')
97116
{
98-
// TODO: remove existing characters
117+
if ($this->incompleteLine !== '') {
118+
// move one line up, move to start of line and clear everything
119+
$data = "\033[A\r\033[K" . $data;
120+
$this->incompleteLine = '';
121+
}
99122

100-
$this->write("\r" . $data);
123+
$this->write($data);
101124
}
102125

103126
public function end($data = null)
@@ -132,4 +155,9 @@ public function getReadline()
132155
{
133156
return $this->readline;
134157
}
158+
159+
private function width($str)
160+
{
161+
return mb_strwidth($str, 'utf-8') - 2 * substr_count($str, "\x08");
162+
}
135163
}

tests/StdioTest.php

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,217 @@ public function testCtorArgsWillBeReturnedByGetters()
3333
$this->assertSame($output, $stdio->getOutput());
3434
$this->assertSame($readline, $stdio->getReadline());
3535
}
36+
37+
public function testWriteWillClearReadlineWriteOutputAndRestoreReadline()
38+
{
39+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
40+
$output = $this->getMock('React\Stream\WritableStreamInterface');
41+
42+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
43+
$readline = new Readline($input, $output);
44+
$readline->setPrompt('> ');
45+
$readline->setInput('input');
46+
47+
$stdio = new Stdio($this->loop, $input, $output, $readline);
48+
49+
$buffer = '';
50+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
51+
$buffer .= $data;
52+
}));
53+
54+
$stdio->write('test');
55+
56+
$this->assertEquals("\r\033[K" . "test\n" . "\r\033[K" . "> input", $buffer);
57+
}
58+
59+
public function testWriteAgainWillClearReadlineMoveToPreviousLineWriteOutputAndRestoreReadline()
60+
{
61+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
62+
$output = $this->getMock('React\Stream\WritableStreamInterface');
63+
64+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
65+
$readline = new Readline($input, $output);
66+
$readline->setPrompt('> ');
67+
$readline->setInput('input');
68+
69+
$stdio = new Stdio($this->loop, $input, $output, $readline);
70+
71+
$stdio->write('hello');
72+
73+
$buffer = '';
74+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
75+
$buffer .= $data;
76+
}));
77+
78+
$stdio->write('world');
79+
80+
$this->assertEquals("\r\033[K" . "\033[A" . "\r\033[5C" . "world\n" . "\r\033[K" . "> input", $buffer);
81+
}
82+
83+
public function testWriteAgainWithBackspaceWillClearReadlineMoveToPreviousLineWriteOutputAndRestoreReadline()
84+
{
85+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
86+
$output = $this->getMock('React\Stream\WritableStreamInterface');
87+
88+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
89+
$readline = new Readline($input, $output);
90+
$readline->setPrompt('> ');
91+
$readline->setInput('input');
92+
93+
$stdio = new Stdio($this->loop, $input, $output, $readline);
94+
95+
$stdio->write('hello!');
96+
97+
$buffer = '';
98+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
99+
$buffer .= $data;
100+
}));
101+
102+
$stdio->write("\x08 world!");
103+
104+
$this->assertEquals("\r\033[K" . "\033[A" . "\r\033[6C" . "\x08 world!\n" . "\r\033[K" . "> input", $buffer);
105+
}
106+
107+
public function testWriteAgainWithNewlinesWillClearReadlineMoveToPreviousLineWriteOutputAndRestoreReadline()
108+
{
109+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
110+
$output = $this->getMock('React\Stream\WritableStreamInterface');
111+
112+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
113+
$readline = new Readline($input, $output);
114+
$readline->setPrompt('> ');
115+
$readline->setInput('input');
116+
117+
$stdio = new Stdio($this->loop, $input, $output, $readline);
118+
119+
$stdio->write("first" . "\n" . "sec");
120+
121+
$buffer = '';
122+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
123+
$buffer .= $data;
124+
}));
125+
126+
$stdio->write("ond" . "\n" . "third");
127+
128+
$this->assertEquals("\r\033[K" . "\033[A" . "\r\033[3C" . "ond\nthird\n" . "\r\033[K" . "> input", $buffer);
129+
}
130+
131+
public function testWriteAfterReadlineInputWillClearReadlineWriteOutputAndRestoreReadline()
132+
{
133+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
134+
$output = $this->getMock('React\Stream\WritableStreamInterface');
135+
136+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
137+
$readline = new Readline($input, $output);
138+
$readline->setPrompt('> ');
139+
140+
$stdio = new Stdio($this->loop, $input, $output, $readline);
141+
142+
$stdio->write('incomplete');
143+
144+
$readline->emit('data', array('test'));
145+
$readline->setInput('input');
146+
147+
$buffer = '';
148+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
149+
$buffer .= $data;
150+
}));
151+
152+
$stdio->writeln('test');
153+
154+
$this->assertEquals("\r\033[K" . "test\n" . "\r\033[K" . "> input", $buffer);
155+
}
156+
157+
public function testOverwriteWillClearReadlineMoveToPreviousLineWriteOutputAndRestoreReadline()
158+
{
159+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
160+
$output = $this->getMock('React\Stream\WritableStreamInterface');
161+
162+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
163+
$readline = new Readline($input, $output);
164+
$readline->setPrompt('> ');
165+
$readline->setInput('input');
166+
167+
$stdio = new Stdio($this->loop, $input, $output, $readline);
168+
169+
$stdio->write('first');
170+
171+
$buffer = '';
172+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
173+
$buffer .= $data;
174+
}));
175+
176+
$stdio->overwrite('overwrite');
177+
178+
$this->assertEquals("\r\033[K" . "\033[A" . "\r\033[K" . "overwrite\n" . "\r\033[K" . "> input", $buffer);
179+
}
180+
181+
public function testOverwriteAfterNewlineWillClearReadlineAndWriteOutputAndRestoreReadline()
182+
{
183+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
184+
$output = $this->getMock('React\Stream\WritableStreamInterface');
185+
186+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
187+
$readline = new Readline($input, $output);
188+
$readline->setPrompt('> ');
189+
$readline->setInput('input');
190+
191+
$stdio = new Stdio($this->loop, $input, $output, $readline);
192+
193+
$stdio->write("first\n");
194+
195+
$buffer = '';
196+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
197+
$buffer .= $data;
198+
}));
199+
200+
$stdio->overwrite('overwrite');
201+
202+
$this->assertEquals("\r\033[K" . "overwrite\n" . "\r\033[K" . "> input", $buffer);
203+
}
204+
205+
public function testWriteLineWillClearReadlineWriteOutputAndRestoreReadline()
206+
{
207+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
208+
$output = $this->getMock('React\Stream\WritableStreamInterface');
209+
210+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
211+
$readline = new Readline($input, $output);
212+
$readline->setPrompt('> ');
213+
$readline->setInput('input');
214+
215+
$stdio = new Stdio($this->loop, $input, $output, $readline);
216+
217+
$buffer = '';
218+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
219+
$buffer .= $data;
220+
}));
221+
222+
$stdio->writeln('test');
223+
224+
$this->assertEquals("\r\033[K" . "test\n" . "\r\033[K" . "> input", $buffer);
225+
}
226+
227+
public function testWriteTwoLinesWillClearReadlineWriteOutputAndRestoreReadline()
228+
{
229+
$input = $this->getMock('React\Stream\ReadableStreamInterface');
230+
$output = $this->getMock('React\Stream\WritableStreamInterface');
231+
232+
//$readline = $this->getMockBuilder('Clue\React\Stdio\Readline')->disableOriginalConstructor()->getMock();
233+
$readline = new Readline($input, $output);
234+
$readline->setPrompt('> ');
235+
$readline->setInput('input');
236+
237+
$stdio = new Stdio($this->loop, $input, $output, $readline);
238+
239+
$buffer = '';
240+
$output->expects($this->any())->method('write')->will($this->returnCallback(function ($data) use (&$buffer) {
241+
$buffer .= $data;
242+
}));
243+
244+
$stdio->writeln('hello');
245+
$stdio->writeln('world');
246+
247+
$this->assertEquals("\r\033[K" . "hello\n" . "\r\033[K" . "> input" . "\r\033[K" . "world\n" . "\r\033[K" . "> input", $buffer);
248+
}
36249
}

0 commit comments

Comments
 (0)