Passed
Push — master ( 032384...6ce998 )
by Fabio
05:25
created

TShellWriter::isRunningOnWindows()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 2
b 1
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * TShellWriter class file
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Shell;
11
12
use Prado\TPropertyValue;
13
use Prado\Util\Helpers\TProcessHelper;
14
15
/**
16
 * TShellWriter class.
17
 *
18
 * Similar to the {@see \Prado\Web\UI\THtmlWriter}, the TShellWriter writes and formats text
19
 * with color, and processes other commands to the terminal to another ITextWriter.
20
 *
21
 * @author Brad Anderson <[email protected]>
22
 * @since 4.2.0
23
 */
24
class TShellWriter extends \Prado\TComponent implements \Prado\IO\ITextWriter
25
{
26
	public const BOLD = 1;
27
	public const DARK = 2;
28
	public const ITALIC = 3;
29
	public const UNDERLINE = 4;
30
	public const BLINK = 5;
31
	public const REVERSE = 7;
32
	public const CONCEALED = 8;
33
	public const CROSSED = 9;
34
	public const FRAMED = 51;
35
	public const ENCIRCLED = 52;
36
	public const OVERLINED = 53;
37
38
	public const BLACK = 30;
39
	public const RED = 31;
40
	public const GREEN = 32;
41
	public const YELLOW = 33;
42
	public const BLUE = 34;
43
	public const MAGENTA = 35;
44
	public const CYAN = 36;
45
	public const LIGHT_GRAY = 37;
46
	// '256' => '38', //  38:2:<red>:<green>:<blue> or 38:5:<256-color>
47
	public const DEFAULT = 39;
48
49
	public const DARK_GRAY = 90;
50
	public const LIGHT_RED = 91;
51
	public const LIGHT_GREEN = 92;
52
	public const LIGHT_YELLOW = 93;
53
	public const LIGHT_BLUE = 94;
54
	public const LIGHT_MAGENTA = 95;
55
	public const LIGHT_CYAN = 96;
56
	public const WHITE = 97;
57
58
	public const BG_BLACK = 40;
59
	public const BG_RED = 41;
60
	public const BG_GREEN = 42;
61
	public const BG_YELLOW = 43;
62
	public const BG_BLUE = 44;
63
	public const BG_MAGENTA = 45;
64
	public const BG_CYAN = 46;
65
	public const BG_LIGHT_GRAY = 47;
66
	//'256' => '48', // 48:2:<red>:<green>:<blue>   48:5:<256-color>
67
	public const BG_DEFAULT = 49;
68
69
	public const BG_DARK_GRAY = 100;
70
	public const BG_LIGHT_RED = 101;
71
	public const BG_LIGHT_GREEN = 102;
72
	public const BG_LIGHT_YELLOW = 103;
73
	public const BG_LIGHT_BLUE = 104;
74
	public const BG_LIGHT_MAGENTA = 105;
75
	public const BG_LIGHT_CYAN = 106;
76
	public const BG_WHITE = 107;
77
78
	/**
79
	 * @var \Prado\IO\ITextWriter writer
80
	 */
81
	protected $_writer;
82
83
	/** @var bool is color supported on tty */
84
	protected $_color;
85
86
	/**
87
	 * Constructor.
88
	 * @param \Prado\IO\ITextWriter $writer a writer that THtmlWriter will pass its rendering result to
89
	 */
90
	public function __construct($writer)
91
	{
92
		$this->_writer = $writer;
93
		$this->_color = $this->isColorSupported();
94
		parent::__construct();
95
	}
96
97
	/**
98
	 * @return bool is color supported
99
	 */
100
	public function getColorSupported()
101
	{
102
		return $this->_color;
103
	}
104
105
	/**
106
	 * @param bool $color is color supported
107
	 */
108
	public function setColorSupported($color)
109
	{
110
		$this->_color = TPropertyValue::ensureBoolean($color);
111
	}
112
113
	/**
114
	 * @return \Prado\IO\ITextWriter the writer output to this class
115
	 */
116
	public function getWriter()
117
	{
118
		return $this->_writer;
119
	}
120
121
	/**
122
	 * @param \Prado\IO\ITextWriter $writer the writer output to this class
123
	 */
124
	public function setWriter($writer)
125
	{
126
		$this->_writer = $writer;
127
	}
128
129
	/**
130
	 * Flushes the rendering result.
131
	 * This will invoke the underlying writer's flush method.
132
	 * @return string the content being flushed
133
	 */
134
	public function flush()
135
	{
136
		return $this->_writer->flush();
137
	}
138
139
	/**
140
	 * Renders a string.
141
	 * @param string $str string to be rendered
142
	 * @param null|mixed $attr
143
	 */
144
	public function write($str, $attr = null)
145
	{
146
		if ($this->_color && $attr) {
147
			if (!is_array($attr)) {
148
				$attr = [$attr];
149
			}
150
			$this->_writer->write("\033[" . implode(';', $attr) . 'm');
151
		}
152
		$this->_writer->write($str);
153
		if ($this->_color && $attr) {
154
			$this->_writer->write("\033[0m");
155
		}
156
	}
157
158
	/**
159
	 * Renders a string and appends a newline to it.
160
	 * @param string $str string to be rendered
161
	 * @param null|mixed $attr
162
	 */
163
	public function writeLine($str = '', $attr = null)
164
	{
165
		if ($this->_color && $attr) {
166
			if (!is_array($attr)) {
167
				$attr = [$attr];
168
			}
169
			$this->_writer->write("\033[" . implode(';', $attr) . 'm');
170
		}
171
		$this->_writer->write($str);
172
		if ($this->_color && $attr) {
173
			$this->_writer->write("\033[0m");
174
		}
175
		$this->_writer->write("\n");
176
	}
177
178
	/**
179
	 * Writes an error block to the writer with color
180
	 * @param string $text
181
	 */
182
	public function writeError($text)
183
	{
184
		$len = 78;
185
		$lines = explode("\n", wordwrap($text, $len - 4, "\n"));
186
		$this->writeLine();
187
		$this->writeLine("*" . str_pad(' Error ', $len, '-', STR_PAD_BOTH) . "*", [self::BG_RED, self::WHITE, self::BOLD]);
188
		foreach ($lines as $i => $line) {
189
			$this->writeLine('*  ' . str_pad($line, $len - 4, ' ', STR_PAD_BOTH) . '  *', [self::BG_RED, self::WHITE, self::BOLD]);
190
		}
191
		$this->writeLine('*' . str_repeat('-', $len) . "*", [self::BG_RED, self::WHITE, self::BOLD]);
192
		$this->writeLine();
193
	}
194
195
	/**
196
	 * @param string $str the string to ANSI format.
197
	 * @param mixed $len
198
	 * @param mixed $pad
199
	 * @param mixed $place
200
	 * @return string $str in the format of $attr.
201
	 */
202
	public function pad($str, $len, $pad = ' ', $place = STR_PAD_RIGHT)
203
	{
204
		$len -= strlen($this->unformat($str));
205
		$pad = $pad[0];
206
		if ($place === STR_PAD_RIGHT) {
207
			while ($len-- > 0) {
208
				$str .= $pad;
209
			}
210
		} elseif ($place === STR_PAD_LEFT) {
211
			while ($len-- > 0) {
212
				$str = $pad . $str;
213
			}
214
		} elseif ($place === STR_PAD_BOTH) {
215
			while ($len-- > 0) {
216
				if ($len % 2) {
217
					$str .= $pad;
218
				} else {
219
					$str = $pad . $str;
220
				}
221
			}
222
		}
223
		return $str;
224
	}
225
226
	/**
227
	 * renders a table widget.
228
	 * ```php
229
	 *  $writer->tableWidget(
230
	 *		'headers' => ['title 1', 'title 2', 'count'],
231
	 *		'rows' => [['a', 'b', 1], ['s', 't', 2], ['c', 'd', 3], ['e', 'f', 10],
232
	 *			['span' => 'text spanning all columns']]
233
	 * );
234
	 * ```
235
	 *
236
	 * @param array $table
237
	 */
238
	public function tableWidget($table)
239
	{
240
		$lengths = [];
241
242
		foreach ($table['headers'] ?? $table['rows'][0] as $i => $header) {
243
			$lengths[$i] = strlen($this->unformat($header)) + 1;
244
			foreach ($table['rows'] as $row => $data) {
245
				if (isset($data['span'])) {
246
					continue;
247
				}
248
				$len = strlen($this->unformat($data[$i])) + 1;
249
				if ($lengths[$i] < $len) {
250
					$lengths[$i] = $len;
251
				}
252
			}
253
		}
254
		$str = '';
255
256
		if (isset($table['headers'])) {
257
			foreach ($table['headers'] as $i => $header) {
258
				$str .= $this->pad($this->format($header, [TShellWriter::UNDERLINE]), $lengths[$i], ' ', STR_PAD_RIGHT);
259
			}
260
			$str .= PHP_EOL;
261
		}
262
		$last = count($table['headers'] ?? $table['rows'][0]) - 1;
263
		foreach ($table['rows'] as $row => $data) {
264
			$lastcolumn = 0;
265
			if (isset($data['span'])) {
266
				$str .= $data['span'];
267
			} else {
268
				foreach ($data as $i => $value) {
269
					if ($last == $i) {
270
						$str .= $this->wrapText($value, $lastcolumn);
271
					} else {
272
						$lastcolumn += $lengths[$i];
273
						$str .= $this->pad($value, $lengths[$i]);
274
					}
275
				}
276
			}
277
			$str .= PHP_EOL;
278
		}
279
		return $str;
280
	}
281
	/**
282
	 * @param string $str the string to ANSI format.
283
	 * @param string|string[] $attr the attributes to format.
284
	 * @return string $str in the format of $attr.
285
	 */
286
	public function format($str, $attr)
287
	{
288
		if (!$this->_color) {
289
			return $str;
290
		}
291
		if (!is_array($attr)) {
292
			$attr = [$attr];
293
		}
294
		return "\033[" . implode(';', $attr) . 'm' . $str . "\033[0m";
295
	}
296
297
	/**
298
	 * removes ANSI formatting
299
	 * @param mixed $str
300
	 */
301
	public function unformat($str)
302
	{
303
		return preg_replace("/\033\[[\?\d;:]*[usmA-HJKSTlh]/", '', $str ?? '');
304
	}
305
306
	/**
307
	 * is color TTY supported
308
	 * @return bool color is supported
309
	 */
310
	protected function isColorSupported()
311
	{
312
		if (TProcessHelper::isSystemWindows()) {
313
			return getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON';
314
		}
315
316
		return function_exists('posix_isatty') && @posix_isatty(STDOUT) && strpos(getenv('TERM'), '256color') !== false;
317
	}
318
319
	/**
320
	 * Moves the terminal cursor up by sending ANSI control code CUU to the terminal.
321
	 * If the cursor is already at the edge of the screen, this has no effect.
322
	 * @param int $rows number of rows the cursor should be moved up
323
	 */
324
	public function moveCursorUp($rows = 1)
325
	{
326
		$this->_writer->write("\033[" . (int) $rows . 'A');
327
	}
328
329
	/**
330
	 * Moves the terminal cursor down by sending ANSI control code CUD to the terminal.
331
	 * If the cursor is already at the edge of the screen, this has no effect.
332
	 * @param int $rows number of rows the cursor should be moved down
333
	 */
334
	public function moveCursorDown($rows = 1)
335
	{
336
		$this->_writer->write("\033[" . (int) $rows . 'B');
337
	}
338
339
	/**
340
	 * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal.
341
	 * If the cursor is already at the edge of the screen, this has no effect.
342
	 * @param int $steps number of steps the cursor should be moved forward
343
	 */
344
	public function moveCursorForward($steps = 1)
345
	{
346
		$this->_writer->write("\033[" . (int) $steps . 'C');
347
	}
348
349
	/**
350
	 * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal.
351
	 * If the cursor is already at the edge of the screen, this has no effect.
352
	 * @param int $steps number of steps the cursor should be moved backward
353
	 */
354
	public function moveCursorBackward($steps = 1)
355
	{
356
		$this->_writer->write("\033[" . (int) $steps . 'D');
357
	}
358
359
	/**
360
	 * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal.
361
	 * @param int $lines number of lines the cursor should be moved down
362
	 */
363
	public function moveCursorNextLine($lines = 1)
364
	{
365
		$this->_writer->write("\033[" . (int) $lines . 'E');
366
	}
367
368
	/**
369
	 * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal.
370
	 * @param int $lines number of lines the cursor should be moved up
371
	 */
372
	public function moveCursorPrevLine($lines = 1)
373
	{
374
		$this->_writer->write("\033[" . (int) $lines . 'F');
375
	}
376
377
	/**
378
	 * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal.
379
	 * @param int $column 1-based column number, 1 is the left edge of the screen.
380
	 * @param null|int $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line.
381
	 */
382
	public function moveCursorTo($column, $row = null)
383
	{
384
		if ($row === null) {
385
			$this->_writer->write("\033[" . (int) $column . 'G');
386
		} else {
387
			$this->_writer->write("\033[" . (int) $row . ';' . (int) $column . 'H');
388
		}
389
	}
390
391
	/**
392
	 * Scrolls whole page up by sending ANSI control code SU to the terminal.
393
	 * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows.
394
	 * @param int $lines number of lines to scroll up
395
	 */
396
	public function scrollUp($lines = 1)
397
	{
398
		$this->_writer->write("\033[" . (int) $lines . 'S');
399
	}
400
401
	/**
402
	 * Scrolls whole page down by sending ANSI control code SD to the terminal.
403
	 * New lines are added at the top. This is not supported by ANSI.SYS used in windows.
404
	 * @param int $lines number of lines to scroll down
405
	 */
406
	public function scrollDown($lines = 1)
407
	{
408
		$this->_writer->write("\033[" . (int) $lines . 'T');
409
	}
410
411
	/**
412
	 * Saves the current cursor position by sending ANSI control code SCP to the terminal.
413
	 * Position can then be restored with {@see restoreCursorPosition}.
414
	 */
415
	public function saveCursorPosition()
416
	{
417
		$this->_writer->write("\033[s");
418
	}
419
420
	/**
421
	 * Restores the cursor position saved with {@see saveCursorPosition} by sending ANSI control code RCP to the terminal.
422
	 */
423
	public function restoreCursorPosition()
424
	{
425
		$this->_writer->write("\033[u");
426
	}
427
428
	/**
429
	 * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal.
430
	 * Use {@see showCursor} to bring it back.
431
	 * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit.
432
	 */
433
	public function hideCursor()
434
	{
435
		$this->_writer->write("\033[?25l");
436
	}
437
438
	/**
439
	 * Will show a cursor again when it has been hidden by {@see hideCursor}  by sending ANSI DECTCEM code ?25h to the terminal.
440
	 */
441
	public function showCursor()
442
	{
443
		$this->_writer->write("\033[?25h");
444
	}
445
446
	/**
447
	 * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal.
448
	 * Cursor position will not be changed.
449
	 * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen.
450
	 */
451
	public function clearScreen()
452
	{
453
		$this->_writer->write("\033[2J");
454
	}
455
456
	/**
457
	 * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal.
458
	 * Cursor position will not be changed.
459
	 */
460
	public function clearScreenBeforeCursor()
461
	{
462
		$this->_writer->write("\033[1J");
463
	}
464
465
	/**
466
	 * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal.
467
	 * Cursor position will not be changed.
468
	 */
469
	public function clearScreenAfterCursor()
470
	{
471
		$this->_writer->write("\033[0J");
472
	}
473
474
	/**
475
	 * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal.
476
	 * Cursor position will not be changed.
477
	 */
478
	public function clearLine()
479
	{
480
		$this->_writer->write("\033[2K");
481
	}
482
483
	/**
484
	 * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal.
485
	 * Cursor position will not be changed.
486
	 */
487
	public function clearLineBeforeCursor()
488
	{
489
		$this->_writer->write("\033[1K");
490
	}
491
492
	/**
493
	 * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal.
494
	 * Cursor position will not be changed.
495
	 */
496
	public function clearLineAfterCursor()
497
	{
498
		$this->_writer->write("\033[0K");
499
	}
500
501
502
	/**
503
	 * Returns terminal screen size.
504
	 *
505
	 * Usage:
506
	 *
507
	 * ```php
508
	 * [$width, $height] = TShellWriter::getScreenSize();
509
	 * ```
510
	 *
511
	 * @param bool $refresh whether to force checking and not re-use cached size value.
512
	 * This is useful to detect changing window size while the application is running but may
513
	 * not get up to date values on every terminal.
514
	 * @return array|bool An array of ($width, $height) or false when it was not able to determine size.
515
	 */
516
	public static function getScreenSize($refresh = false)
517
	{
518
		static $size;
519
		if ($size !== null && !$refresh) {
520
			return $size;
521
		}
522
523
		if (TProcessHelper::isSystemWindows()) {
524
			$output = [];
525
			exec('mode con', $output);
526
			if (isset($output[1]) && strpos($output[1], 'CON') !== false) {
527
				return $size = [(int) preg_replace('~\D~', '', $output[4]), (int) preg_replace('~\D~', '', $output[3])];
528
			}
529
		} else {
530
			// try stty if available
531
			$stty = [];
532
			if (exec('stty -a 2>&1', $stty)) {
533
				$stty = implode(' ', $stty);
534
535
				// Linux stty output
536
				if (preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', $stty, $matches)) {
537
					return $size = [(int) $matches[2], (int) $matches[1]];
538
				}
539
540
				// MacOS stty output
541
				if (preg_match('/(\d+)\s+rows;\s*(\d+)\s+columns;/mi', $stty, $matches)) {
542
					return $size = [(int) $matches[2], (int) $matches[1]];
543
				}
544
			}
545
546
			// fallback to tput, which may not be updated on terminal resize
547
			if (($width = (int) exec('tput cols 2>&1')) > 0 && ($height = (int) exec('tput lines 2>&1')) > 0) {
548
				return $size = [$width, $height];
549
			}
550
551
			// fallback to ENV variables, which may not be updated on terminal resize
552
			if (($width = (int) getenv('COLUMNS')) > 0 && ($height = (int) getenv('LINES')) > 0) {
553
				return $size = [$width, $height];
554
			}
555
		}
556
557
		return $size = false;
558
	}
559
560
	/**
561
	 * Word wrap text with indentation to fit the screen size.
562
	 *
563
	 * If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped.
564
	 *
565
	 * The first line will **not** be indented, so `TShellWriter::wrapText("Lorem ipsum dolor sit amet.", 4)` will result in the
566
	 * following output, given the screen width is 16 characters:
567
	 *
568
	 * ```
569
	 * Lorem ipsum
570
	 *     dolor sit
571
	 *     amet.
572
	 * ```
573
	 *
574
	 * @param string $text the text to be wrapped
575
	 * @param int $indent number of spaces to use for indentation.
576
	 * @param bool $refresh whether to force refresh of screen size.
577
	 * This will be passed to {@see getScreenSize}.
578
	 * @return string the wrapped text.
579
	 */
580
	public function wrapText($text, $indent = 0, $refresh = false)
581
	{
582
		$size = static::getScreenSize($refresh);
583
		if ($size === false || $size[0] <= $indent) {
584
			return $text;
585
		}
586
		$pad = str_repeat(' ', $indent);
587
		$lines = explode("\n", wordwrap($text, $size[0] - $indent, "\n"));
588
		$first = true;
589
		foreach ($lines as $i => $line) {
590
			if ($first) {
591
				$first = false;
592
				continue;
593
			}
594
			$lines[$i] = $pad . $line;
595
		}
596
597
		return implode("\n", $lines);
598
	}
599
}
600