22
33namespace Clue \React \Stdio ;
44
5- use React \ Stream \ StreamInterface ;
6- use React \Stream \CompositeStream ;
5+ use Evenement \ EventEmitter ;
6+ use React \Stream \DuplexStreamInterface ;
77use React \EventLoop \LoopInterface ;
8- use React \Stream \ReadableStream ;
9- use React \Stream \Stream ;
8+ use React \Stream \ReadableStreamInterface ;
9+ use React \Stream \WritableStreamInterface ;
10+ use React \Stream \Util ;
1011
11- class Stdio extends CompositeStream
12+ class Stdio extends EventEmitter implements DuplexStreamInterface
1213{
1314 private $ input ;
1415 private $ output ;
15-
1616 private $ readline ;
17- private $ needsNewline = false ;
1817
19- public function __construct (LoopInterface $ loop , $ input = true )
18+ private $ ending = false ;
19+ private $ closed = false ;
20+ private $ incompleteLine = '' ;
21+
22+ public function __construct (LoopInterface $ loop , ReadableStreamInterface $ input = null , WritableStreamInterface $ output = null , Readline $ readline = null )
2023 {
21- $ this ->input = new Stdin ($ loop );
24+ if ($ input === null ) {
25+ $ input = new Stdin ($ loop );
26+ }
27+
28+ if ($ output === null ) {
29+ $ output = new Stdout (STDOUT );
30+ }
2231
23- $ this ->output = new Stdout (STDOUT );
32+ if ($ readline === null ) {
33+ $ readline = new Readline ($ input , $ output );
34+ }
2435
25- $ this ->readline = new Readline ($ this ->input , $ this ->output );
36+ $ this ->input = $ input ;
37+ $ this ->output = $ output ;
38+ $ this ->readline = $ readline ;
2639
2740 $ that = $ this ;
2841
29- // stdin emits single chars
30- $ this ->input ->on ('data ' , function ($ data ) use ($ that ) {
31- $ that ->emit ('char ' , array ($ data , $ that ));
32- });
33-
3442 // readline data emits a new line
35- $ this ->readline ->on ('data ' , function ($ line ) use ($ that ) {
43+ $ incomplete =& $ this ->incompleteLine ;
44+ $ this ->readline ->on ('data ' , function ($ line ) use ($ that , &$ incomplete ) {
45+ // readline emits a new line on enter, so start with a blank line
46+ $ incomplete = '' ;
47+
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
3652 $ that ->emit ('line ' , array ($ line , $ that ));
3753 });
3854
39- if (!$ input ) {
40- $ this ->pause ();
41- }
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 ' ));
4263 }
4364
4465 public function pause ()
@@ -51,6 +72,23 @@ public function resume()
5172 $ this ->input ->resume ();
5273 }
5374
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+
5492 public function handleBuffer ()
5593 {
5694 $ that = $ this ;
@@ -61,26 +99,75 @@ public function handleBuffer()
6199
62100 public function write ($ data )
63101 {
64- // switch back to last output position
65- $ this ->readline ->clear ();
102+ if ($ this ->ending || (string )$ data === '' ) {
103+ return ;
104+ }
105+
106+ $ out = $ data ;
107+
108+ $ lastNewline = strrpos ($ data , "\n" );
109+
110+ $ restoreReadline = false ;
111+
112+ if ($ this ->incompleteLine !== '' ) {
113+ // the last write did not end with a newline => append to existing row
114+
115+ // move one line up and move cursor to last position before writing data
116+ $ out = "\033[A " . "\r\033[ " . $ this ->width ($ this ->incompleteLine ) . "C " . $ out ;
117+
118+ // data contains a newline, so this will overwrite the readline prompt
119+ if ($ lastNewline !== false ) {
120+ // move cursor to beginning of readline prompt and clear line
121+ // clearing is important because $data may not overwrite the whole line
122+ $ out = "\r\033[K " . $ out ;
123+
124+ // make sure to restore readline after this output
125+ $ restoreReadline = true ;
126+ }
127+ } else {
128+ // here, we're writing to a new line => overwrite readline prompt
66129
67- // Erase characters from cursor to end of line
68- $ this -> output -> write ( "\r\033[K " ) ;
130+ // move cursor to beginning of readline prompt and clear line
131+ $ out = "\r\033[K " . $ out ;
69132
70- // move one line up?
71- if ($ this ->needsNewline ) {
72- $ this ->output ->write ("\033[A " );
133+ // we always overwrite the readline prompt, so restore it on next line
134+ $ restoreReadline = true ;
73135 }
74136
75- $ this ->output ->write ($ data );
137+ // following write will have have to append to this line if it does not end with a newline
138+ $ endsWithNewline = substr ($ data , -1 ) === "\n" ;
76139
77- $ this ->needsNewline = substr ($ data , -1 ) !== "\n" ;
140+ if ($ endsWithNewline ) {
141+ // line ends with newline, so this is line is considered complete
142+ $ this ->incompleteLine = '' ;
143+ } else {
144+ // always end data with newline in order to append readline on next line
145+ $ out .= "\n" ;
78146
79- // repeat current prompt + linebuffer
80- if ($ this ->needsNewline ) {
81- $ this ->output ->write ("\n" );
147+ if ($ lastNewline === false ) {
148+ // contains no newline at all, everything is incomplete
149+ $ this ->incompleteLine .= $ data ;
150+ } else {
151+ // contains a newline, everything behind it is incomplete
152+ $ this ->incompleteLine = (string )substr ($ data , $ lastNewline + 1 );
153+ }
154+ }
155+
156+ if ($ restoreReadline ) {
157+ // write output and restore original readline prompt and line buffer
158+ $ this ->output ->write ($ out );
159+ $ this ->readline ->redraw ();
160+ } else {
161+ // restore original cursor position in readline prompt
162+ $ pos = $ this ->width ($ this ->readline ->getPrompt ()) + $ this ->readline ->getCursorCell ();
163+ if ($ pos !== 0 ) {
164+ // we always start at beginning of line, move right by X
165+ $ out .= "\033[ " . $ pos . "C " ;
166+ }
167+
168+ // write to actual output stream
169+ $ this ->output ->write ($ out );
82170 }
83- $ this ->readline ->redraw ();
84171 }
85172
86173 public function writeln ($ line )
@@ -90,25 +177,44 @@ public function writeln($line)
90177
91178 public function overwrite ($ data = '' )
92179 {
93- // TODO: remove existing characters
180+ if ($ this ->incompleteLine !== '' ) {
181+ // move one line up, move to start of line and clear everything
182+ $ data = "\033[A \r\033[K " . $ data ;
183+ $ this ->incompleteLine = '' ;
184+ }
94185
95- $ this ->write ("\r" . $ data );
186+ $ this ->write ($ data );
96187 }
97188
98189 public function end ($ data = null )
99190 {
191+ if ($ this ->ending ) {
192+ return ;
193+ }
194+
100195 if ($ data !== null ) {
101196 $ this ->write ($ data );
102197 }
103198
199+ $ this ->ending = true ;
200+
201+ // clear readline output, close input and end output
104202 $ this ->readline ->setInput ('' )->setPrompt ('' )->clear ();
105- $ this ->input ->pause ();
203+ $ this ->input ->close ();
106204 $ this ->output ->end ();
107205 }
108206
109207 public function close ()
110208 {
111- $ 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 ();
112218 $ this ->input ->close ();
113219 $ this ->output ->close ();
114220 }
@@ -127,4 +233,38 @@ public function getReadline()
127233 {
128234 return $ this ->readline ;
129235 }
236+
237+ private function width ($ str )
238+ {
239+ return mb_strwidth ($ str , 'utf-8 ' ) - 2 * substr_count ($ str , "\x08" );
240+ }
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+ }
130270}
0 commit comments