GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 2ab419...940f7c )
by Robert
32:50
created

BaseConsole::ansiToHtml()   D

Complexity

Conditions 17
Paths 2

Size

Total Lines 99
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 82
CRAP Score 17.0127

Importance

Changes 0
Metric Value
dl 0
loc 99
rs 4.8361
c 0
b 0
f 0
ccs 82
cts 85
cp 0.9647
cc 17
eloc 73
nc 2
nop 2
crap 17.0127

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 integer $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 integer $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 integer $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 integer $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 integer $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 integer $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 integer $column 1-based column number, 1 is the left edge of the screen.
116
     * @param integer|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 1
        } 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 integer $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 integer $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 21
    public static function ansiFormat($string, $format = [])
288
    {
289 21
        $code = implode(';', $format);
290
291 21
        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 integer $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 integer $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 4
    public static function stripAnsiFormat($string)
329
    {
330 4
        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 integer 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 14
                    } elseif ($controlCode == self::NEGATIVE) {
399 2
                        $negative = true;
400 11
                    } elseif (isset($styleMap[$controlCode])) {
401 10
                        $style[] = $styleMap[$controlCode];
402 10
                    }
403 14
                }
404
405 14
                $return = '';
406 14
                while ($reset && $tags > 0) {
407 10
                    $return .= '</span>';
408 10
                    $tags--;
409 10
                }
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 10
                }
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 1
                    }
425 1
                    if (isset($currentStyle['background-color'])) {
426 1
                        $bgColor = $currentStyle['background-color'];
427 1
                        unset($currentStyle['background-color']);
428 1
                    }
429 1
                    if (isset($fgColor)) {
430 1
                        $currentStyle['background-color'] = $fgColor;
431 1
                    }
432 1
                    if (isset($bgColor)) {
433 1
                        $currentStyle['color'] = $bgColor;
434 1
                    }
435 1
                }
436
437 10
                $styleString = '';
438 10
                foreach ($currentStyle as $name => $value) {
439 10
                    if (is_array($value)) {
440 1
                        $value = implode(' ', $value);
441 1
                    }
442 10
                    $styleString .= "$name: $value;";
443 10
                }
444 10
                $tags++;
445 10
                return "$return<span style=\"$styleString\">";
446 15
            },
447
            $string
448 15
        );
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 boolean $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
        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_PURPLE],
532
            '%7' => [self::BG_CYAN],
533
            '%0' => [self::BG_GREY],
534
            '%F' => [self::BLINK],
535
            '%U' => [self::UNDERLINE],
536
            '%8' => [self::NEGATIVE],
537
            '%9' => [self::BOLD],
538
            '%_' => [self::BOLD],
539 2
        ];
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
                    $string
548 2
                );
549 2
            }
550 2
            $string = str_replace('% ', '%', $string);
551 2
        } 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
     * @access public
565
     * @return string
566
     */
567
    public static function escape($string)
568
    {
569
        // TODO rework/refactor according to https://github.com/yiisoft/yii2/issues/746
570
        return str_replace('%', '%%', $string);
571
    }
572
573
    /**
574
     * Returns true if the stream supports colorization. ANSI colors are disabled if not supported by the stream.
575
     *
576
     * - windows without ansicon
577
     * - not tty consoles
578
     *
579
     * @param mixed $stream
580
     * @return boolean true if the stream supports ANSI colors, otherwise false.
581
     */
582 3
    public static function streamSupportsAnsiColors($stream)
583
    {
584 3
        return DIRECTORY_SEPARATOR === '\\'
585 3
            ? getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON'
586 3
            : function_exists('posix_isatty') && @posix_isatty($stream);
587
    }
588
589
    /**
590
     * Returns true if the console is running on windows
591
     * @return boolean
592
     */
593 1
    public static function isRunningOnWindows()
594
    {
595 1
        return DIRECTORY_SEPARATOR === '\\';
596
    }
597
598
    /**
599
     * Usage: list($width, $height) = ConsoleHelper::getScreenSize();
600
     *
601
     * @param boolean $refresh whether to force checking and not re-use cached size value.
602
     * This is useful to detect changing window size while the application is running but may
603
     * not get up to date values on every terminal.
604
     * @return array|boolean An array of ($width, $height) or false when it was not able to determine size.
605
     */
606 1
    public static function getScreenSize($refresh = false)
607
    {
608 1
        static $size;
609 1
        if ($size !== null && !$refresh) {
610 1
            return $size;
611
        }
612
613 1
        if (static::isRunningOnWindows()) {
614
            $output = [];
615
            exec('mode con', $output);
616
            if (isset($output, $output[1]) && strpos($output[1], 'CON') !== false) {
617
                return $size = [(int) preg_replace('~\D~', '', $output[4]), (int) preg_replace('~\D~', '', $output[3])];
618
            }
619
        } else {
620
            // try stty if available
621 1
            $stty = [];
622 1
            if (exec('stty -a 2>&1', $stty) && preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', implode(' ', $stty), $matches)) {
623 1
                return $size = [(int)$matches[2], (int)$matches[1]];
624
            }
625
626
            // fallback to tput, which may not be updated on terminal resize
627
            if (($width = (int) exec('tput cols 2>&1')) > 0 && ($height = (int) exec('tput lines 2>&1')) > 0) {
628
                return $size = [$width, $height];
629
            }
630
631
            // fallback to ENV variables, which may not be updated on terminal resize
632
            if (($width = (int) getenv('COLUMNS')) > 0 && ($height = (int) getenv('LINES')) > 0) {
633
                return $size = [$width, $height];
634
            }
635
        }
636
637
        return $size = false;
638
    }
639
640
    /**
641
     * Word wrap text with indentation to fit the screen size
642
     *
643
     * If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped.
644
     *
645
     * The first line will **not** be indented, so `Console::wrapText("Lorem ipsum dolor sit amet.", 4)` will result in the
646
     * following output, given the screen width is 16 characters:
647
     *
648
     * ```
649
     * Lorem ipsum
650
     *     dolor sit
651
     *     amet.
652
     * ```
653
     *
654
     * @param string $text the text to be wrapped
655
     * @param integer $indent number of spaces to use for indentation.
656
     * @param boolean $refresh whether to force refresh of screen size.
657
     * This will be passed to [[getScreenSize()]].
658
     * @return string the wrapped text.
659
     * @since 2.0.4
660
     */
661 1
    public static function wrapText($text, $indent = 0, $refresh = false)
662
    {
663 1
        $size = static::getScreenSize($refresh);
664 1
        if ($size === false || $size[0] <= $indent) {
665
            return $text;
666
        }
667 1
        $pad = str_repeat(' ', $indent);
668 1
        $lines = explode("\n", wordwrap($text, $size[0] - $indent, "\n", true));
669 1
        $first = true;
670 1
        foreach ($lines as $i => $line) {
671 1
            if ($first) {
672 1
                $first = false;
673 1
                continue;
674
            }
675 1
            $lines[$i] = $pad . $line;
676 1
        }
677 1
        return implode("\n", $lines);
678
    }
679
680
    /**
681
     * Gets input from STDIN and returns a string right-trimmed for EOLs.
682
     *
683
     * @param boolean $raw If set to true, returns the raw string without trimming
684
     * @return string the string read from stdin
685
     */
686
    public static function stdin($raw = false)
687
    {
688
        return $raw ? fgets(\STDIN) : rtrim(fgets(\STDIN), PHP_EOL);
689
    }
690
691
    /**
692
     * Prints a string to STDOUT.
693
     *
694
     * @param string $string the string to print
695
     * @return integer|boolean Number of bytes printed or false on error
696
     */
697
    public static function stdout($string)
698
    {
699
        return fwrite(\STDOUT, $string);
700
    }
701
702
    /**
703
     * Prints a string to STDERR.
704
     *
705
     * @param string $string the string to print
706
     * @return integer|boolean Number of bytes printed or false on error
707
     */
708
    public static function stderr($string)
709
    {
710
        return fwrite(\STDERR, $string);
711
    }
712
713
    /**
714
     * Asks the user for input. Ends when the user types a carriage return (PHP_EOL). Optionally, It also provides a
715
     * prompt.
716
     *
717
     * @param string $prompt the prompt to display before waiting for input (optional)
718
     * @return string the user's input
719
     */
720
    public static function input($prompt = null)
721
    {
722
        if (isset($prompt)) {
723
            static::stdout($prompt);
724
        }
725
726
        return static::stdin();
727
    }
728
729
    /**
730
     * Prints text to STDOUT appended with a carriage return (PHP_EOL).
731
     *
732
     * @param string $string the text to print
733
     * @return integer|boolean number of bytes printed or false on error.
734
     */
735
    public static function output($string = null)
736
    {
737
        return static::stdout($string . PHP_EOL);
738
    }
739
740
    /**
741
     * Prints text to STDERR appended with a carriage return (PHP_EOL).
742
     *
743
     * @param string $string the text to print
744
     * @return integer|boolean number of bytes printed or false on error.
745
     */
746
    public static function error($string = null)
747
    {
748
        return static::stderr($string . PHP_EOL);
749
    }
750
751
    /**
752
     * Prompts the user for input and validates it
753
     *
754
     * @param string $text prompt string
755
     * @param array $options the options to validate the input:
756
     *
757
     * - `required`: whether it is required or not
758
     * - `default`: default value if no input is inserted by the user
759
     * - `pattern`: regular expression pattern to validate user input
760
     * - `validator`: a callable function to validate input. The function must accept two parameters:
761
     * - `input`: the user input to validate
762
     * - `error`: the error value passed by reference if validation failed.
763
     *
764
     * @return string the user input
765
     */
766
    public static function prompt($text, $options = [])
767
    {
768
        $options = ArrayHelper::merge(
769
            [
770
                'required'  => false,
771
                'default'   => null,
772
                'pattern'   => null,
773
                'validator' => null,
774
                'error'     => 'Invalid input.',
775
            ],
776
            $options
777
        );
778
        $error   = null;
779
780
        top:
781
        $input = $options['default']
782
            ? static::input("$text [" . $options['default'] . '] ')
783
            : static::input("$text ");
784
785
        if ($input === '') {
786
            if (isset($options['default'])) {
787
                $input = $options['default'];
788
            } elseif ($options['required']) {
789
                static::output($options['error']);
790
                goto top;
791
            }
792
        } elseif ($options['pattern'] && !preg_match($options['pattern'], $input)) {
793
            static::output($options['error']);
794
            goto top;
795
        } elseif ($options['validator'] &&
796
            !call_user_func_array($options['validator'], [$input, &$error])
797
        ) {
798
            static::output(isset($error) ? $error : $options['error']);
799
            goto top;
800
        }
801
802
        return $input;
803
    }
804
805
    /**
806
     * Asks user to confirm by typing y or n.
807
     *
808
     * @param string $message to print out before waiting for user input
809
     * @param boolean $default this value is returned if no selection is made.
810
     * @return boolean whether user confirmed
811
     */
812
    public static function confirm($message, $default = false)
813
    {
814
        while (true) {
815
            static::stdout($message . ' (yes|no) [' . ($default ? 'yes' : 'no') . ']:');
816
            $input = trim(static::stdin());
817
818
            if (empty($input)) {
819
                return $default;
820
            }
821
822
            if (!strcasecmp($input, 'y') || !strcasecmp($input, 'yes')) {
823
                return true;
824
            }
825
826
            if (!strcasecmp($input, 'n') || !strcasecmp($input, 'no')) {
827
                return false;
828
            }
829
        }
830
    }
831
832
    /**
833
     * Gives the user an option to choose from. Giving '?' as an input will show
834
     * a list of options to choose from and their explanations.
835
     *
836
     * @param string $prompt the prompt message
837
     * @param array $options Key-value array of options to choose from
838
     *
839
     * @return string An option character the user chose
840
     */
841
    public static function select($prompt, $options = [])
842
    {
843
        top:
844
        static::stdout("$prompt [" . implode(',', array_keys($options)) . ',?]: ');
845
        $input = static::stdin();
846
        if ($input === '?') {
847
            foreach ($options as $key => $value) {
848
                static::output(" $key - $value");
849
            }
850
            static::output(' ? - Show help');
851
            goto top;
852
        } elseif (!array_key_exists($input, $options)) {
853
            goto top;
854
        }
855
856
        return $input;
857
    }
858
859
    private static $_progressStart;
860
    private static $_progressWidth;
861
    private static $_progressPrefix;
862
    private static $_progressEta;
863
    private static $_progressEtaLastDone = 0;
864
    private static $_progressEtaLastUpdate;
865
866
    /**
867
     * Starts display of a progress bar on screen.
868
     *
869
     * This bar will be updated by [[updateProgress()]] and my be ended by [[endProgress()]].
870
     *
871
     * The following example shows a simple usage of a progress bar:
872
     *
873
     * ```php
874
     * Console::startProgress(0, 1000);
875
     * for ($n = 1; $n <= 1000; $n++) {
876
     *     usleep(1000);
877
     *     Console::updateProgress($n, 1000);
878
     * }
879
     * Console::endProgress();
880
     * ```
881
     *
882
     * Git clone like progress (showing only status information):
883
     * ```php
884
     * Console::startProgress(0, 1000, 'Counting objects: ', false);
885
     * for ($n = 1; $n <= 1000; $n++) {
886
     *     usleep(1000);
887
     *     Console::updateProgress($n, 1000);
888
     * }
889
     * Console::endProgress("done." . PHP_EOL);
890
     * ```
891
     *
892
     * @param integer $done the number of items that are completed.
893
     * @param integer $total the total value of items that are to be done.
894
     * @param string $prefix an optional string to display before the progress bar.
895
     * Default to empty string which results in no prefix to be displayed.
896
     * @param integer|boolean $width optional width of the progressbar. This can be an integer representing
897
     * the number of characters to display for the progress bar or a float between 0 and 1 representing the
898
     * percentage of screen with the progress bar may take. It can also be set to false to disable the
899
     * bar and only show progress information like percent, number of items and ETA.
900
     * If not set, the bar will be as wide as the screen. Screen size will be detected using [[getScreenSize()]].
901
     * @see startProgress
902
     * @see updateProgress
903
     * @see endProgress
904
     */
905
    public static function startProgress($done, $total, $prefix = '', $width = null)
906
    {
907
        self::$_progressStart = time();
908
        self::$_progressWidth = $width;
909
        self::$_progressPrefix = $prefix;
910
        self::$_progressEta = null;
911
        self::$_progressEtaLastDone = 0;
912
        self::$_progressEtaLastUpdate = time();
913
914
        static::updateProgress($done, $total);
915
    }
916
917
    /**
918
     * Updates a progress bar that has been started by [[startProgress()]].
919
     *
920
     * @param integer $done the number of items that are completed.
921
     * @param integer $total the total value of items that are to be done.
922
     * @param string $prefix an optional string to display before the progress bar.
923
     * Defaults to null meaning the prefix specified by [[startProgress()]] will be used.
924
     * If prefix is specified it will update the prefix that will be used by later calls.
925
     * @see startProgress
926
     * @see endProgress
927
     */
928
    public static function updateProgress($done, $total, $prefix = null)
929
    {
930
        $width = self::$_progressWidth;
931
        if ($width === false) {
932
            $width = 0;
933
        } else {
934
            $screenSize = static::getScreenSize(true);
935
            if ($screenSize === false && $width < 1) {
936
                $width = 0;
937
            } elseif ($width === null) {
938
                $width = $screenSize[0];
939
            } elseif ($width > 0 && $width < 1) {
940
                $width = floor($screenSize[0] * $width);
941
            }
942
        }
943
        if ($prefix === null) {
944
            $prefix = self::$_progressPrefix;
945
        } else {
946
            self::$_progressPrefix = $prefix;
947
        }
948
        $width -= static::ansiStrlen($prefix);
949
950
        $percent = ($total == 0) ? 1 : $done / $total;
951
        $info = sprintf('%d%% (%d/%d)', $percent * 100, $done, $total);
952
953
        if ($done > $total || $done == 0) {
954
            self::$_progressEta = null;
955
            self::$_progressEtaLastUpdate = time();
956
        } elseif ($done < $total) {
957
            // update ETA once per second to avoid flapping
958
            if (time() - self::$_progressEtaLastUpdate > 1 && $done > self::$_progressEtaLastDone) {
959
                $rate = (time() - (self::$_progressEtaLastUpdate ?: self::$_progressStart)) / ($done - self::$_progressEtaLastDone);
960
                self::$_progressEta = $rate * ($total - $done);
961
                self::$_progressEtaLastUpdate = time();
962
                self::$_progressEtaLastDone = $done;
963
            }
964
        }
965
        if (self::$_progressEta === null) {
966
            $info .= ' ETA: n/a';
967
        } else {
968
            $info .= sprintf(' ETA: %d sec.', self::$_progressEta);
969
        }
970
971
        // Number extra characters outputted. These are opening [, closing ], and space before info
972
        // Since Windows uses \r\n\ for line endings, there's one more in the case
973
        $extraChars = static::isRunningOnWindows() ? 4 : 3;
974
        $width -= $extraChars + static::ansiStrlen($info);
975
        // skipping progress bar on very small display or if forced to skip
976
        if ($width < 5) {
977
            static::stdout("\r$prefix$info   ");
978
        } else {
979
            if ($percent < 0) {
980
                $percent = 0;
981
            } elseif ($percent > 1) {
982
                $percent = 1;
983
            }
984
            $bar = floor($percent * $width);
985
            $status = str_repeat('=', $bar);
986
            if ($bar < $width) {
987
                $status .= '>';
988
                $status .= str_repeat(' ', $width - $bar - 1);
989
            }
990
            static::stdout("\r$prefix" . "[$status] $info");
991
        }
992
        flush();
993
    }
994
995
    /**
996
     * Ends a progress bar that has been started by [[startProgress()]].
997
     *
998
     * @param string|boolean $remove This can be `false` to leave the progress bar on screen and just print a newline.
999
     * If set to `true`, the line of the progress bar will be cleared. This may also be a string to be displayed instead
1000
     * of the progress bar.
1001
     * @param boolean $keepPrefix whether to keep the prefix that has been specified for the progressbar when progressbar
1002
     * gets removed. Defaults to true.
1003
     * @see startProgress
1004
     * @see updateProgress
1005
     */
1006
    public static function endProgress($remove = false, $keepPrefix = true)
1007
    {
1008
        if ($remove === false) {
1009
            static::stdout(PHP_EOL);
1010
        } else {
1011
            if (static::streamSupportsAnsiColors(STDOUT)) {
1012
                static::clearLine();
1013
            }
1014
            static::stdout("\r" . ($keepPrefix ? self::$_progressPrefix : '') . (is_string($remove) ? $remove : ''));
1015
        }
1016
        flush();
1017
1018
        self::$_progressStart = null;
1019
        self::$_progressWidth = null;
1020
        self::$_progressPrefix = '';
1021
        self::$_progressEta = null;
1022
        self::$_progressEtaLastDone = 0;
1023
        self::$_progressEtaLastUpdate = null;
1024
    }
1025
}
1026