Passed
Push — master ( 5fd064...896ccb )
by Fabio
04:59
created

TShellWriter   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 584
Duplicated Lines 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 187
c 3
b 1
f 0
dl 0
loc 584
rs 2
wmc 87

36 Methods

Rating   Name   Duplication   Size   Complexity  
A setColorSupported() 0 3 1
A getColorSupported() 0 3 1
A setWriter() 0 3 1
A writeLine() 0 13 6
A __construct() 0 5 1
A getWriter() 0 3 1
A flush() 0 3 1
A write() 0 11 6
A clearScreenAfterCursor() 0 3 1
A hideCursor() 0 3 1
A clearScreenBeforeCursor() 0 3 1
A scrollDown() 0 3 1
A moveCursorUp() 0 3 1
A saveCursorPosition() 0 3 1
B pad() 0 22 8
A wrapText() 0 18 5
A clearLineAfterCursor() 0 3 1
A isRunningOnWindows() 0 3 1
A isColorSupported() 0 7 5
C getScreenSize() 0 42 13
A moveCursorNextLine() 0 3 1
A clearScreen() 0 3 1
A moveCursorPrevLine() 0 3 1
A moveCursorForward() 0 3 1
A clearLineBeforeCursor() 0 3 1
A showCursor() 0 3 1
A moveCursorDown() 0 3 1
A moveCursorTo() 0 6 2
A format() 0 9 3
A moveCursorBackward() 0 3 1
A unformat() 0 3 1
A restoreCursorPosition() 0 3 1
B tableWidget() 0 42 11
A scrollUp() 0 3 1
A clearLine() 0 3 1
A writeError() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like TShellWriter 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.

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 TShellWriter, and based on these observations, apply Extract Interface, too.

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