Completed
Push — php7-travis-apcu ( 9bbcee...fd63c3 )
by Alexander
14:47
created

BaseConsole   D

Complexity

Total Complexity 131

Size/Duplication

Total Lines 1025
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 53.89%

Importance

Changes 0
Metric Value
wmc 131
lcom 1
cbo 2
dl 0
loc 1025
ccs 180
cts 334
cp 0.5389
rs 4.4353
c 0
b 0
f 0

47 Methods

Rating   Name   Duplication   Size   Complexity  
A moveCursorUp() 0 4 1
A moveCursorDown() 0 4 1
A moveCursorForward() 0 4 1
A moveCursorBackward() 0 4 1
A moveCursorNextLine() 0 4 1
A moveCursorPrevLine() 0 4 1
A scrollUp() 0 4 1
A scrollDown() 0 4 1
A saveCursorPosition() 0 4 1
A restoreCursorPosition() 0 4 1
A hideCursor() 0 4 1
A showCursor() 0 4 1
A clearScreen() 0 4 1
A clearScreenBeforeCursor() 0 4 1
A clearScreenAfterCursor() 0 4 1
A clearLine() 0 4 1
A clearLineBeforeCursor() 0 4 1
A clearLineAfterCursor() 0 4 1
A beginAnsiFormat() 0 4 1
A endAnsiFormat() 0 4 1
A ansiFormat() 0 6 2
A xtermFgColor() 0 4 1
A xtermBgColor() 0 4 1
A stripAnsiFormat() 0 4 1
A ansiStrlen() 0 4 1
A ansiFormatCode() 0 4 1
A markdownToAnsi() 0 5 1
A moveCursorTo() 0 8 2
A escape() 0 5 1
A streamSupportsAnsiColors() 0 6 4
A isRunningOnWindows() 0 4 1
C getScreenSize() 0 43 13
B wrapText() 0 18 5
A stdin() 0 4 2
A stdout() 0 4 1
A stderr() 0 4 1
A input() 0 8 2
A output() 0 4 1
A error() 0 4 1
A select() 0 17 4
A startProgress() 0 11 1
F updateProgress() 0 66 21
B endProgress() 0 19 5
A renderColoredString() 0 55 3
F prompt() 0 38 10
D ansiToHtml() 0 99 17
B confirm() 0 19 8

How to fix   Complexity   

Complex Class

Complex classes like BaseConsole often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseConsole, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\helpers;
9
10
use yii\console\Markdown as ConsoleMarkdown;
11
12
/**
13
 * BaseConsole provides concrete implementation for [[Console]].
14
 *
15
 * Do not use BaseConsole. Use [[Console]] instead.
16
 *
17
 * @author Carsten Brandt <[email protected]>
18
 * @since 2.0
19
 */
20
class BaseConsole
21
{
22
    // foreground color control codes
23
    const FG_BLACK = 30;
24
    const FG_RED = 31;
25
    const FG_GREEN = 32;
26
    const FG_YELLOW = 33;
27
    const FG_BLUE = 34;
28
    const FG_PURPLE = 35;
29
    const FG_CYAN = 36;
30
    const FG_GREY = 37;
31
    // background color control codes
32
    const BG_BLACK = 40;
33
    const BG_RED = 41;
34
    const BG_GREEN = 42;
35
    const BG_YELLOW = 43;
36
    const BG_BLUE = 44;
37
    const BG_PURPLE = 45;
38
    const BG_CYAN = 46;
39
    const BG_GREY = 47;
40
    // fonts style control codes
41
    const RESET = 0;
42
    const NORMAL = 0;
43
    const BOLD = 1;
44
    const ITALIC = 3;
45
    const UNDERLINE = 4;
46
    const BLINK = 5;
47
    const NEGATIVE = 7;
48
    const CONCEALED = 8;
49
    const CROSSED_OUT = 9;
50
    const FRAMED = 51;
51
    const ENCIRCLED = 52;
52
    const OVERLINED = 53;
53
54
55
    /**
56
     * Moves the terminal cursor up by sending ANSI control code CUU to the terminal.
57
     * If the cursor is already at the edge of the screen, this has no effect.
58
     * @param int $rows number of rows the cursor should be moved up
59
     */
60 1
    public static function moveCursorUp($rows = 1)
61
    {
62 1
        echo "\033[" . (int) $rows . 'A';
63 1
    }
64
65
    /**
66
     * Moves the terminal cursor down by sending ANSI control code CUD to the terminal.
67
     * If the cursor is already at the edge of the screen, this has no effect.
68
     * @param int $rows number of rows the cursor should be moved down
69
     */
70 1
    public static function moveCursorDown($rows = 1)
71
    {
72 1
        echo "\033[" . (int) $rows . 'B';
73 1
    }
74
75
    /**
76
     * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal.
77
     * If the cursor is already at the edge of the screen, this has no effect.
78
     * @param int $steps number of steps the cursor should be moved forward
79
     */
80 1
    public static function moveCursorForward($steps = 1)
81
    {
82 1
        echo "\033[" . (int) $steps . 'C';
83 1
    }
84
85
    /**
86
     * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal.
87
     * If the cursor is already at the edge of the screen, this has no effect.
88
     * @param int $steps number of steps the cursor should be moved backward
89
     */
90 1
    public static function moveCursorBackward($steps = 1)
91
    {
92 1
        echo "\033[" . (int) $steps . 'D';
93 1
    }
94
95
    /**
96
     * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal.
97
     * @param int $lines number of lines the cursor should be moved down
98
     */
99 1
    public static function moveCursorNextLine($lines = 1)
100
    {
101 1
        echo "\033[" . (int) $lines . 'E';
102 1
    }
103
104
    /**
105
     * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal.
106
     * @param int $lines number of lines the cursor should be moved up
107
     */
108 1
    public static function moveCursorPrevLine($lines = 1)
109
    {
110 1
        echo "\033[" . (int) $lines . 'F';
111 1
    }
112
113
    /**
114
     * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal.
115
     * @param int $column 1-based column number, 1 is the left edge of the screen.
116
     * @param int|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line.
117
     */
118 1
    public static function moveCursorTo($column, $row = null)
119
    {
120 1
        if ($row === null) {
121 1
            echo "\033[" . (int) $column . 'G';
122
        } else {
123 1
            echo "\033[" . (int) $row . ';' . (int) $column . 'H';
124
        }
125 1
    }
126
127
    /**
128
     * Scrolls whole page up by sending ANSI control code SU to the terminal.
129
     * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows.
130
     * @param int $lines number of lines to scroll up
131
     */
132 1
    public static function scrollUp($lines = 1)
133
    {
134 1
        echo "\033[" . (int) $lines . 'S';
135 1
    }
136
137
    /**
138
     * Scrolls whole page down by sending ANSI control code SD to the terminal.
139
     * New lines are added at the top. This is not supported by ANSI.SYS used in windows.
140
     * @param int $lines number of lines to scroll down
141
     */
142 1
    public static function scrollDown($lines = 1)
143
    {
144 1
        echo "\033[" . (int) $lines . 'T';
145 1
    }
146
147
    /**
148
     * Saves the current cursor position by sending ANSI control code SCP to the terminal.
149
     * Position can then be restored with [[restoreCursorPosition()]].
150
     */
151 1
    public static function saveCursorPosition()
152
    {
153 1
        echo "\033[s";
154 1
    }
155
156
    /**
157
     * Restores the cursor position saved with [[saveCursorPosition()]] by sending ANSI control code RCP to the terminal.
158
     */
159 1
    public static function restoreCursorPosition()
160
    {
161 1
        echo "\033[u";
162 1
    }
163
164
    /**
165
     * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal.
166
     * Use [[showCursor()]] to bring it back.
167
     * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit.
168
     */
169 1
    public static function hideCursor()
170
    {
171 1
        echo "\033[?25l";
172 1
    }
173
174
    /**
175
     * Will show a cursor again when it has been hidden by [[hideCursor()]]  by sending ANSI DECTCEM code ?25h to the terminal.
176
     */
177 1
    public static function showCursor()
178
    {
179 1
        echo "\033[?25h";
180 1
    }
181
182
    /**
183
     * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal.
184
     * Cursor position will not be changed.
185
     * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen.
186
     */
187 1
    public static function clearScreen()
188
    {
189 1
        echo "\033[2J";
190 1
    }
191
192
    /**
193
     * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal.
194
     * Cursor position will not be changed.
195
     */
196 1
    public static function clearScreenBeforeCursor()
197
    {
198 1
        echo "\033[1J";
199 1
    }
200
201
    /**
202
     * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal.
203
     * Cursor position will not be changed.
204
     */
205 1
    public static function clearScreenAfterCursor()
206
    {
207 1
        echo "\033[0J";
208 1
    }
209
210
    /**
211
     * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal.
212
     * Cursor position will not be changed.
213
     */
214 1
    public static function clearLine()
215
    {
216 1
        echo "\033[2K";
217 1
    }
218
219
    /**
220
     * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal.
221
     * Cursor position will not be changed.
222
     */
223 1
    public static function clearLineBeforeCursor()
224
    {
225 1
        echo "\033[1K";
226 1
    }
227
228
    /**
229
     * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal.
230
     * Cursor position will not be changed.
231
     */
232 1
    public static function clearLineAfterCursor()
233
    {
234 1
        echo "\033[0K";
235 1
    }
236
237
    /**
238
     * Returns the ANSI format code.
239
     *
240
     * @param array $format An array containing formatting values.
241
     * You can pass any of the `FG_*`, `BG_*` and `TEXT_*` constants
242
     * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format.
243
     * @return string The ANSI format code according to the given formatting constants.
244
     */
245 2
    public static function ansiFormatCode($format)
246
    {
247 2
        return "\033[" . implode(';', $format) . 'm';
248
    }
249
250
    /**
251
     * Echoes an ANSI format code that affects the formatting of any text that is printed afterwards.
252
     *
253
     * @param array $format An array containing formatting values.
254
     * You can pass any of the `FG_*`, `BG_*` and `TEXT_*` constants
255
     * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format.
256
     * @see ansiFormatCode()
257
     * @see endAnsiFormat()
258
     */
259 1
    public static function beginAnsiFormat($format)
260
    {
261 1
        echo "\033[" . implode(';', $format) . 'm';
262 1
    }
263
264
    /**
265
     * Resets any ANSI format set by previous method [[beginAnsiFormat()]]
266
     * Any output after this will have default text format.
267
     * This is equal to calling
268
     *
269
     * ```php
270
     * echo Console::ansiFormatCode([Console::RESET])
271
     * ```
272
     */
273 1
    public static function endAnsiFormat()
274
    {
275 1
        echo "\033[0m";
276 1
    }
277
278
    /**
279
     * Will return a string formatted with the given ANSI style
280
     *
281
     * @param string $string the string to be formatted
282
     * @param array $format An array containing formatting values.
283
     * You can pass any of the `FG_*`, `BG_*` and `TEXT_*` constants
284
     * and also [[xtermFgColor]] and [[xtermBgColor]] to specify a format.
285
     * @return string
286
     */
287 16
    public static function ansiFormat($string, $format = [])
288
    {
289 16
        $code = implode(';', $format);
290
291 16
        return "\033[0m" . ($code !== '' ? "\033[" . $code . 'm' : '') . $string . "\033[0m";
292
    }
293
294
    /**
295
     * Returns the ansi format code for xterm foreground color.
296
     * You can pass the return value of this to one of the formatting methods:
297
     * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]]
298
     *
299
     * @param int $colorCode xterm color code
300
     * @return string
301
     * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors
302
     */
303 1
    public static function xtermFgColor($colorCode)
304
    {
305 1
        return '38;5;' . $colorCode;
306
    }
307
308
    /**
309
     * Returns the ansi format code for xterm background color.
310
     * You can pass the return value of this to one of the formatting methods:
311
     * [[ansiFormat]], [[ansiFormatCode]], [[beginAnsiFormat]]
312
     *
313
     * @param int $colorCode xterm color code
314
     * @return string
315
     * @see http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors
316
     */
317 1
    public static function xtermBgColor($colorCode)
318
    {
319 1
        return '48;5;' . $colorCode;
320
    }
321
322
    /**
323
     * Strips ANSI control codes from a string
324
     *
325
     * @param string $string String to strip
326
     * @return string
327
     */
328 7
    public static function stripAnsiFormat($string)
329
    {
330 7
        return preg_replace('/\033\[[\d;?]*\w/', '', $string);
331
    }
332
333
    /**
334
     * Returns the length of the string without ANSI color codes.
335
     * @param string $string the string to measure
336
     * @return int the length of the string not counting ANSI format characters
337
     */
338
    public static function ansiStrlen($string)
339
    {
340
        return mb_strlen(static::stripAnsiFormat($string));
341
    }
342
343
    /**
344
     * Converts an ANSI formatted string to HTML
345
     *
346
     * Note: xTerm 256 bit colors are currently not supported.
347
     *
348
     * @param string $string the string to convert.
349
     * @param array $styleMap an optional mapping of ANSI control codes such as
350
     * FG\_*COLOR* or [[BOLD]] to a set of css style definitions.
351
     * The CSS style definitions are represented as an array where the array keys correspond
352
     * to the css style attribute names and the values are the css values.
353
     * values may be arrays that will be merged and imploded with `' '` when rendered.
354
     * @return string HTML representation of the ANSI formatted string
355
     */
356 15
    public static function ansiToHtml($string, $styleMap = [])
357
    {
358
        $styleMap = [
359
            // http://www.w3.org/TR/CSS2/syndata.html#value-def-color
360 15
            self::FG_BLACK => ['color' => 'black'],
361 15
            self::FG_BLUE => ['color' => 'blue'],
362 15
            self::FG_CYAN => ['color' => 'aqua'],
363 15
            self::FG_GREEN => ['color' => 'lime'],
364 15
            self::FG_GREY => ['color' => 'silver'],
365
            // http://meyerweb.com/eric/thoughts/2014/06/19/rebeccapurple/
366
            // http://dev.w3.org/csswg/css-color/#valuedef-rebeccapurple
367 15
            self::FG_PURPLE => ['color' => 'rebeccapurple'],
368 15
            self::FG_RED => ['color' => 'red'],
369 15
            self::FG_YELLOW => ['color' => 'yellow'],
370 15
            self::BG_BLACK => ['background-color' => 'black'],
371 15
            self::BG_BLUE => ['background-color' => 'blue'],
372 15
            self::BG_CYAN => ['background-color' => 'aqua'],
373 15
            self::BG_GREEN => ['background-color' => 'lime'],
374 15
            self::BG_GREY => ['background-color' => 'silver'],
375 15
            self::BG_PURPLE => ['background-color' => 'rebeccapurple'],
376 15
            self::BG_RED => ['background-color' => 'red'],
377 15
            self::BG_YELLOW => ['background-color' => 'yellow'],
378 15
            self::BOLD => ['font-weight' => 'bold'],
379 15
            self::ITALIC => ['font-style' => 'italic'],
380 15
            self::UNDERLINE => ['text-decoration' => ['underline']],
381 15
            self::OVERLINED => ['text-decoration' => ['overline']],
382 15
            self::CROSSED_OUT => ['text-decoration' => ['line-through']],
383 15
            self::BLINK => ['text-decoration' => ['blink']],
384 15
            self::CONCEALED => ['visibility' => 'hidden'],
385 15
        ] + $styleMap;
386
387 15
        $tags = 0;
388 15
        $result = preg_replace_callback(
389 15
            '/\033\[([\d;]+)m/',
390 15
            function ($ansi) use (&$tags, $styleMap) {
391 14
                $style = [];
392 14
                $reset = false;
393 14
                $negative = false;
394 14
                foreach (explode(';', $ansi[1]) as $controlCode) {
395 14
                    if ($controlCode == 0) {
396 14
                        $style = [];
397 14
                        $reset = true;
398 11
                    } elseif ($controlCode == self::NEGATIVE) {
399 2
                        $negative = true;
400 10
                    } elseif (isset($styleMap[$controlCode])) {
401 14
                        $style[] = $styleMap[$controlCode];
402
                    }
403
                }
404
405 14
                $return = '';
406 14
                while ($reset && $tags > 0) {
407 10
                    $return .= '</span>';
408 10
                    $tags--;
409
                }
410 14
                if (empty($style)) {
411 14
                    return $return;
412
                }
413
414 10
                $currentStyle = [];
415 10
                foreach ($style as $content) {
416 10
                    $currentStyle = ArrayHelper::merge($currentStyle, $content);
417
                }
418
419
                // if negative is set, invert background and foreground
420 10
                if ($negative) {
421 1
                    if (isset($currentStyle['color'])) {
422 1
                        $fgColor = $currentStyle['color'];
423 1
                        unset($currentStyle['color']);
424
                    }
425 1
                    if (isset($currentStyle['background-color'])) {
426 1
                        $bgColor = $currentStyle['background-color'];
427 1
                        unset($currentStyle['background-color']);
428
                    }
429 1
                    if (isset($fgColor)) {
430 1
                        $currentStyle['background-color'] = $fgColor;
431
                    }
432 1
                    if (isset($bgColor)) {
433 1
                        $currentStyle['color'] = $bgColor;
434
                    }
435
                }
436
437 10
                $styleString = '';
438 10
                foreach ($currentStyle as $name => $value) {
439 10
                    if (is_array($value)) {
440 1
                        $value = implode(' ', $value);
441
                    }
442 10
                    $styleString .= "$name: $value;";
443
                }
444 10
                $tags++;
445 10
                return "$return<span style=\"$styleString\">";
446 15
            },
447 15
            $string
448
        );
449 15
        while ($tags > 0) {
450
            $result .= '</span>';
451
            $tags--;
452
        }
453 15
        return $result;
454
    }
455
456
    /**
457
     * Converts Markdown to be better readable in console environments by applying some ANSI format
458
     * @param string $markdown the markdown string.
459
     * @return string the parsed result as ANSI formatted string.
460
     */
461 2
    public static function markdownToAnsi($markdown)
462
    {
463 2
        $parser = new ConsoleMarkdown();
464 2
        return $parser->parse($markdown);
465
    }
466
467
    /**
468
     * Converts a string to ansi formatted by replacing patterns like %y (for yellow) with ansi control codes
469
     *
470
     * Uses almost the same syntax as https://github.com/pear/Console_Color2/blob/master/Console/Color2.php
471
     * The conversion table is: ('bold' meaning 'light' on some
472
     * terminals). It's almost the same conversion table irssi uses.
473
     * <pre>
474
     *                  text      text            background
475
     *      ------------------------------------------------
476
     *      %k %K %0    black     dark grey       black
477
     *      %r %R %1    red       bold red        red
478
     *      %g %G %2    green     bold green      green
479
     *      %y %Y %3    yellow    bold yellow     yellow
480
     *      %b %B %4    blue      bold blue       blue
481
     *      %m %M %5    magenta   bold magenta    magenta
482
     *      %p %P       magenta (think: purple)
483
     *      %c %C %6    cyan      bold cyan       cyan
484
     *      %w %W %7    white     bold white      white
485
     *
486
     *      %F     Blinking, Flashing
487
     *      %U     Underline
488
     *      %8     Reverse
489
     *      %_,%9  Bold
490
     *
491
     *      %n     Resets the color
492
     *      %%     A single %
493
     * </pre>
494
     * First param is the string to convert, second is an optional flag if
495
     * colors should be used. It defaults to true, if set to false, the
496
     * color codes will just be removed (And %% will be transformed into %)
497
     *
498
     * @param string $string String to convert
499
     * @param bool $colored Should the string be colored?
500
     * @return string
501
     */
502 2
    public static function renderColoredString($string, $colored = true)
503
    {
504
        // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746
505 2
        static $conversions = [
506
            '%y' => [self::FG_YELLOW],
507
            '%g' => [self::FG_GREEN],
508
            '%b' => [self::FG_BLUE],
509
            '%r' => [self::FG_RED],
510
            '%p' => [self::FG_PURPLE],
511
            '%m' => [self::FG_PURPLE],
512
            '%c' => [self::FG_CYAN],
513
            '%w' => [self::FG_GREY],
514
            '%k' => [self::FG_BLACK],
515
            '%n' => [0], // reset
516
            '%Y' => [self::FG_YELLOW, self::BOLD],
517
            '%G' => [self::FG_GREEN, self::BOLD],
518
            '%B' => [self::FG_BLUE, self::BOLD],
519
            '%R' => [self::FG_RED, self::BOLD],
520
            '%P' => [self::FG_PURPLE, self::BOLD],
521
            '%M' => [self::FG_PURPLE, self::BOLD],
522
            '%C' => [self::FG_CYAN, self::BOLD],
523
            '%W' => [self::FG_GREY, self::BOLD],
524
            '%K' => [self::FG_BLACK, self::BOLD],
525
            '%N' => [0, self::BOLD],
526
            '%3' => [self::BG_YELLOW],
527
            '%2' => [self::BG_GREEN],
528
            '%4' => [self::BG_BLUE],
529
            '%1' => [self::BG_RED],
530
            '%5' => [self::BG_PURPLE],
531
            '%6' => [self::BG_CYAN],
532
            '%7' => [self::BG_GREY],
533
            '%0' => [self::BG_BLACK],
534
            '%F' => [self::BLINK],
535
            '%U' => [self::UNDERLINE],
536
            '%8' => [self::NEGATIVE],
537
            '%9' => [self::BOLD],
538
            '%_' => [self::BOLD],
539
        ];
540
541 2
        if ($colored) {
542 2
            $string = str_replace('%%', '% ', $string);
543 2
            foreach ($conversions as $key => $value) {
544 2
                $string = str_replace(
545 2
                    $key,
546 2
                    static::ansiFormatCode($value),
547 2
                    $string
548
                );
549
            }
550 2
            $string = str_replace('% ', '%', $string);
551
        } else {
552
            $string = preg_replace('/%((%)|.)/', '$2', $string);
553
        }
554
555 2
        return $string;
556
    }
557
558
    /**
559
     * Escapes % so they don't get interpreted as color codes when
560
     * the string is parsed by [[renderColoredString]]
561
     *
562
     * @param string $string String to escape
563
     *
564
     * @return string
565
     */
566
    public static function escape($string)
567
    {
568
        // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746
569
        return str_replace('%', '%%', $string);
570
    }
571
572
    /**
573
     * Returns true if the stream supports colorization. ANSI colors are disabled if not supported by the stream.
574
     *
575
     * - windows without ansicon
576
     * - not tty consoles
577
     *
578
     * @param mixed $stream
579
     * @return bool true if the stream supports ANSI colors, otherwise false.
580
     */
581 4
    public static function streamSupportsAnsiColors($stream)
582
    {
583 4
        return DIRECTORY_SEPARATOR === '\\'
584
            ? getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON'
585 4
            : function_exists('posix_isatty') && @posix_isatty($stream);
586
    }
587
588
    /**
589
     * Returns true if the console is running on windows
590
     * @return bool
591
     */
592 1
    public static function isRunningOnWindows()
593
    {
594 1
        return DIRECTORY_SEPARATOR === '\\';
595
    }
596
597
    /**
598
     * Usage: [$width, $height] = ConsoleHelper::getScreenSize();
599
     *
600
     * @param bool $refresh whether to force checking and not re-use cached size value.
601
     * This is useful to detect changing window size while the application is running but may
602
     * not get up to date values on every terminal.
603
     * @return array|bool An array of ($width, $height) or false when it was not able to determine size.
604
     */
605 1
    public static function getScreenSize($refresh = false)
606
    {
607 1
        static $size;
608 1
        if ($size !== null && !$refresh) {
609 1
            return $size;
610
        }
611
612 1
        if (static::isRunningOnWindows()) {
613
            $output = [];
614
            exec('mode con', $output);
615
            if (isset($output, $output[1]) && strpos($output[1], 'CON') !== false) {
616
                return $size = [(int) preg_replace('~\D~', '', $output[4]), (int) preg_replace('~\D~', '', $output[3])];
617
            }
618
        } else {
619
            // try stty if available
620 1
            $stty = [];
621 1
            if (exec('stty -a 2>&1', $stty)) {
622 1
                $stty = implode(' ', $stty);
623
624
                // Linux stty output
625 1
                if (preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', $stty, $matches)) {
626 1
                    return $size = [(int) $matches[2], (int) $matches[1]];
627
                }
628
629
                // MacOS stty output
630
                if (preg_match('/(\d+)\s+rows;\s*(\d+)\s+columns;/mi', $stty, $matches)) {
631
                    return $size = [(int) $matches[2], (int) $matches[1]];
632
                }
633
            }
634
635
            // fallback to tput, which may not be updated on terminal resize
636
            if (($width = (int) exec('tput cols 2>&1')) > 0 && ($height = (int) exec('tput lines 2>&1')) > 0) {
637
                return $size = [$width, $height];
638
            }
639
640
            // fallback to ENV variables, which may not be updated on terminal resize
641
            if (($width = (int) getenv('COLUMNS')) > 0 && ($height = (int) getenv('LINES')) > 0) {
642
                return $size = [$width, $height];
643
            }
644
        }
645
646
        return $size = false;
647
    }
648
649
    /**
650
     * Word wrap text with indentation to fit the screen size
651
     *
652
     * If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped.
653
     *
654
     * The first line will **not** be indented, so `Console::wrapText("Lorem ipsum dolor sit amet.", 4)` will result in the
655
     * following output, given the screen width is 16 characters:
656
     *
657
     * ```
658
     * Lorem ipsum
659
     *     dolor sit
660
     *     amet.
661
     * ```
662
     *
663
     * @param string $text the text to be wrapped
664
     * @param int $indent number of spaces to use for indentation.
665
     * @param bool $refresh whether to force refresh of screen size.
666
     * This will be passed to [[getScreenSize()]].
667
     * @return string the wrapped text.
668
     * @since 2.0.4
669
     */
670 1
    public static function wrapText($text, $indent = 0, $refresh = false)
671
    {
672 1
        $size = static::getScreenSize($refresh);
673 1
        if ($size === false || $size[0] <= $indent) {
674 1
            return $text;
675
        }
676
        $pad = str_repeat(' ', $indent);
677
        $lines = explode("\n", wordwrap($text, $size[0] - $indent, "\n", true));
678
        $first = true;
679
        foreach ($lines as $i => $line) {
680
            if ($first) {
681
                $first = false;
682
                continue;
683
            }
684
            $lines[$i] = $pad . $line;
685
        }
686
        return implode("\n", $lines);
687
    }
688
689
    /**
690
     * Gets input from STDIN and returns a string right-trimmed for EOLs.
691
     *
692
     * @param bool $raw If set to true, returns the raw string without trimming
693
     * @return string the string read from stdin
694
     */
695
    public static function stdin($raw = false)
696
    {
697
        return $raw ? fgets(\STDIN) : rtrim(fgets(\STDIN), PHP_EOL);
698
    }
699
700
    /**
701
     * Prints a string to STDOUT.
702
     *
703
     * @param string $string the string to print
704
     * @return int|bool Number of bytes printed or false on error
705
     */
706
    public static function stdout($string)
707
    {
708
        return fwrite(\STDOUT, $string);
709
    }
710
711
    /**
712
     * Prints a string to STDERR.
713
     *
714
     * @param string $string the string to print
715
     * @return int|bool Number of bytes printed or false on error
716
     */
717
    public static function stderr($string)
718
    {
719
        return fwrite(\STDERR, $string);
720
    }
721
722
    /**
723
     * Asks the user for input. Ends when the user types a carriage return (PHP_EOL). Optionally, It also provides a
724
     * prompt.
725
     *
726
     * @param string $prompt the prompt to display before waiting for input (optional)
727
     * @return string the user's input
728
     */
729
    public static function input($prompt = null)
730
    {
731
        if (isset($prompt)) {
732
            static::stdout($prompt);
733
        }
734
735
        return static::stdin();
736
    }
737
738
    /**
739
     * Prints text to STDOUT appended with a carriage return (PHP_EOL).
740
     *
741
     * @param string $string the text to print
742
     * @return int|bool number of bytes printed or false on error.
743
     */
744
    public static function output($string = null)
745
    {
746
        return static::stdout($string . PHP_EOL);
747
    }
748
749
    /**
750
     * Prints text to STDERR appended with a carriage return (PHP_EOL).
751
     *
752
     * @param string $string the text to print
753
     * @return int|bool number of bytes printed or false on error.
754
     */
755
    public static function error($string = null)
756
    {
757
        return static::stderr($string . PHP_EOL);
758
    }
759
760
    /**
761
     * Prompts the user for input and validates it
762
     *
763
     * @param string $text prompt string
764
     * @param array $options the options to validate the input:
765
     *
766
     * - `required`: whether it is required or not
767
     * - `default`: default value if no input is inserted by the user
768
     * - `pattern`: regular expression pattern to validate user input
769
     * - `validator`: a callable function to validate input. The function must accept two parameters:
770
     * - `input`: the user input to validate
771
     * - `error`: the error value passed by reference if validation failed.
772
     *
773
     * @return string the user input
774
     */
775
    public static function prompt($text, $options = [])
776
    {
777
        $options = ArrayHelper::merge(
778
            [
779
                'required' => false,
780
                'default' => null,
781
                'pattern' => null,
782
                'validator' => null,
783
                'error' => 'Invalid input.',
784
            ],
785
            $options
786
        );
787
        $error = null;
788
789
        top:
790
        $input = $options['default']
791
            ? static::input("$text [" . $options['default'] . '] ')
792
            : static::input("$text ");
793
794
        if ($input === '') {
795
            if (isset($options['default'])) {
796
                $input = $options['default'];
797
            } elseif ($options['required']) {
798
                static::output($options['error']);
799
                goto top;
800
            }
801
        } elseif ($options['pattern'] && !preg_match($options['pattern'], $input)) {
802
            static::output($options['error']);
803
            goto top;
804
        } elseif ($options['validator'] &&
805
            !call_user_func_array($options['validator'], [$input, &$error])
806
        ) {
807
            static::output(isset($error) ? $error : $options['error']);
808
            goto top;
809
        }
810
811
        return $input;
812
    }
813
814
    /**
815
     * Asks user to confirm by typing y or n.
816
     *
817
     * A typical usage looks like the following:
818
     *
819
     * ```php
820
     * if (Console::confirm("Are you sure?")) {
821
     *     echo "user typed yes\n";
822
     * } else {
823
     *     echo "user typed no\n";
824
     * }
825
     * ```
826
     *
827
     * @param string $message to print out before waiting for user input
828
     * @param bool $default this value is returned if no selection is made.
829
     * @return bool whether user confirmed
830
     */
831
    public static function confirm($message, $default = false)
832
    {
833
        while (true) {
834
            static::stdout($message . ' (yes|no) [' . ($default ? 'yes' : 'no') . ']:');
835
            $input = trim(static::stdin());
836
837
            if (empty($input)) {
838
                return $default;
839
            }
840
841
            if (!strcasecmp($input, 'y') || !strcasecmp($input, 'yes')) {
842
                return true;
843
            }
844
845
            if (!strcasecmp($input, 'n') || !strcasecmp($input, 'no')) {
846
                return false;
847
            }
848
        }
849
    }
850
851
    /**
852
     * Gives the user an option to choose from. Giving '?' as an input will show
853
     * a list of options to choose from and their explanations.
854
     *
855
     * @param string $prompt the prompt message
856
     * @param array $options Key-value array of options to choose from
857
     *
858
     * @return string An option character the user chose
859
     */
860
    public static function select($prompt, $options = [])
861
    {
862
        top:
863
        static::stdout("$prompt [" . implode(',', array_keys($options)) . ',?]: ');
864
        $input = static::stdin();
865
        if ($input === '?') {
866
            foreach ($options as $key => $value) {
867
                static::output(" $key - $value");
868
            }
869
            static::output(' ? - Show help');
870
            goto top;
871
        } elseif (!array_key_exists($input, $options)) {
872
            goto top;
873
        }
874
875
        return $input;
876
    }
877
878
    private static $_progressStart;
879
    private static $_progressWidth;
880
    private static $_progressPrefix;
881
    private static $_progressEta;
882
    private static $_progressEtaLastDone = 0;
883
    private static $_progressEtaLastUpdate;
884
885
    /**
886
     * Starts display of a progress bar on screen.
887
     *
888
     * This bar will be updated by [[updateProgress()]] and my be ended by [[endProgress()]].
889
     *
890
     * The following example shows a simple usage of a progress bar:
891
     *
892
     * ```php
893
     * Console::startProgress(0, 1000);
894
     * for ($n = 1; $n <= 1000; $n++) {
895
     *     usleep(1000);
896
     *     Console::updateProgress($n, 1000);
897
     * }
898
     * Console::endProgress();
899
     * ```
900
     *
901
     * Git clone like progress (showing only status information):
902
     * ```php
903
     * Console::startProgress(0, 1000, 'Counting objects: ', false);
904
     * for ($n = 1; $n <= 1000; $n++) {
905
     *     usleep(1000);
906
     *     Console::updateProgress($n, 1000);
907
     * }
908
     * Console::endProgress("done." . PHP_EOL);
909
     * ```
910
     *
911
     * @param int $done the number of items that are completed.
912
     * @param int $total the total value of items that are to be done.
913
     * @param string $prefix an optional string to display before the progress bar.
914
     * Default to empty string which results in no prefix to be displayed.
915
     * @param int|bool $width optional width of the progressbar. This can be an integer representing
916
     * the number of characters to display for the progress bar or a float between 0 and 1 representing the
917
     * percentage of screen with the progress bar may take. It can also be set to false to disable the
918
     * bar and only show progress information like percent, number of items and ETA.
919
     * If not set, the bar will be as wide as the screen. Screen size will be detected using [[getScreenSize()]].
920
     * @see startProgress
921
     * @see updateProgress
922
     * @see endProgress
923
     */
924
    public static function startProgress($done, $total, $prefix = '', $width = null)
925
    {
926
        self::$_progressStart = time();
927
        self::$_progressWidth = $width;
928
        self::$_progressPrefix = $prefix;
929
        self::$_progressEta = null;
930
        self::$_progressEtaLastDone = 0;
931
        self::$_progressEtaLastUpdate = time();
932
933
        static::updateProgress($done, $total);
934
    }
935
936
    /**
937
     * Updates a progress bar that has been started by [[startProgress()]].
938
     *
939
     * @param int $done the number of items that are completed.
940
     * @param int $total the total value of items that are to be done.
941
     * @param string $prefix an optional string to display before the progress bar.
942
     * Defaults to null meaning the prefix specified by [[startProgress()]] will be used.
943
     * If prefix is specified it will update the prefix that will be used by later calls.
944
     * @see startProgress
945
     * @see endProgress
946
     */
947
    public static function updateProgress($done, $total, $prefix = null)
948
    {
949
        $width = self::$_progressWidth;
950
        if ($width === false) {
951
            $width = 0;
952
        } else {
953
            $screenSize = static::getScreenSize(true);
954
            if ($screenSize === false && $width < 1) {
955
                $width = 0;
956
            } elseif ($width === null) {
957
                $width = $screenSize[0];
958
            } elseif ($width > 0 && $width < 1) {
959
                $width = floor($screenSize[0] * $width);
960
            }
961
        }
962
        if ($prefix === null) {
963
            $prefix = self::$_progressPrefix;
964
        } else {
965
            self::$_progressPrefix = $prefix;
966
        }
967
        $width -= static::ansiStrlen($prefix);
968
969
        $percent = ($total == 0) ? 1 : $done / $total;
970
        $info = sprintf('%d%% (%d/%d)', $percent * 100, $done, $total);
971
972
        if ($done > $total || $done == 0) {
973
            self::$_progressEta = null;
974
            self::$_progressEtaLastUpdate = time();
975
        } elseif ($done < $total) {
976
            // update ETA once per second to avoid flapping
977
            if (time() - self::$_progressEtaLastUpdate > 1 && $done > self::$_progressEtaLastDone) {
978
                $rate = (time() - (self::$_progressEtaLastUpdate ?: self::$_progressStart)) / ($done - self::$_progressEtaLastDone);
979
                self::$_progressEta = $rate * ($total - $done);
980
                self::$_progressEtaLastUpdate = time();
981
                self::$_progressEtaLastDone = $done;
982
            }
983
        }
984
        if (self::$_progressEta === null) {
985
            $info .= ' ETA: n/a';
986
        } else {
987
            $info .= sprintf(' ETA: %d sec.', self::$_progressEta);
988
        }
989
990
        // Number extra characters outputted. These are opening [, closing ], and space before info
991
        // Since Windows uses \r\n\ for line endings, there's one more in the case
992
        $extraChars = static::isRunningOnWindows() ? 4 : 3;
993
        $width -= $extraChars + static::ansiStrlen($info);
994
        // skipping progress bar on very small display or if forced to skip
995
        if ($width < 5) {
996
            static::stdout("\r$prefix$info   ");
997
        } else {
998
            if ($percent < 0) {
999
                $percent = 0;
1000
            } elseif ($percent > 1) {
1001
                $percent = 1;
1002
            }
1003
            $bar = floor($percent * $width);
1004
            $status = str_repeat('=', $bar);
1005
            if ($bar < $width) {
1006
                $status .= '>';
1007
                $status .= str_repeat(' ', $width - $bar - 1);
1008
            }
1009
            static::stdout("\r$prefix" . "[$status] $info");
1010
        }
1011
        flush();
1012
    }
1013
1014
    /**
1015
     * Ends a progress bar that has been started by [[startProgress()]].
1016
     *
1017
     * @param string|bool $remove This can be `false` to leave the progress bar on screen and just print a newline.
1018
     * If set to `true`, the line of the progress bar will be cleared. This may also be a string to be displayed instead
1019
     * of the progress bar.
1020
     * @param bool $keepPrefix whether to keep the prefix that has been specified for the progressbar when progressbar
1021
     * gets removed. Defaults to true.
1022
     * @see startProgress
1023
     * @see updateProgress
1024
     */
1025
    public static function endProgress($remove = false, $keepPrefix = true)
1026
    {
1027
        if ($remove === false) {
1028
            static::stdout(PHP_EOL);
1029
        } else {
1030
            if (static::streamSupportsAnsiColors(STDOUT)) {
1031
                static::clearLine();
1032
            }
1033
            static::stdout("\r" . ($keepPrefix ? self::$_progressPrefix : '') . (is_string($remove) ? $remove : ''));
1034
        }
1035
        flush();
1036
1037
        self::$_progressStart = null;
1038
        self::$_progressWidth = null;
1039
        self::$_progressPrefix = '';
1040
        self::$_progressEta = null;
1041
        self::$_progressEtaLastDone = 0;
1042
        self::$_progressEtaLastUpdate = null;
1043
    }
1044
}
1045