Passed
Push — master ( cc20de...06f7cb )
by Fabio
06:28
created

TShellWriter::write()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 6
eloc 7
c 1
b 1
f 0
nc 6
nop 2
dl 0
loc 11
rs 9.2222
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 ITextWriter writer
0 ignored issues
show
Bug introduced by
The type Prado\Shell\ITextWriter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
81
	 */
82
	protected $_writer;
83
	
84
	/** @var bool is color supported on tty */
85
	protected $_color;
86
	
87
	/**
88
	 * Constructor.
89
	 * @param 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 ITextWriter the writer output to this class
116
	 */
117
	public function getWriter()
118
	{
119
		return $this->_writer;
120
	}
121
	
122
	/**
123
	 * @param 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
	 * @param string $str the string to ANSI format.
181
	 * @param string|string[] $attr the attributes to format.
182
	 * @return string $str in the format of $attr.
183
	 */
184
	public function format($str, $attr)
185
	{
186
		if (!$this->_color) {
187
			return $str;
188
		}
189
		if (!is_array($attr)) {
190
			$attr = [$attr];
191
		}
192
		return "\033[" . implode(';', $attr) . 'm' . $str . "\033[0m";
193
	}
194
	
195
	public function unformat($str)
196
	{
197
		return preg_replace("/\033\[[\?\d;:]*[usmA-HJKSTlh]/", '', $str);
198
	}
199
	
200
	/**
201
	 * is color TTY supported
202
	 * @return bool color is supported
203
	 */
204
	protected function isColorSupported()
205
	{
206
		if (static::isRunningOnWindows()) {
207
			return getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON';
208
		}
209
	
210
		return function_exists('posix_isatty') && @posix_isatty(STDOUT) && strpos(getenv('TERM'), '256color') !== false;
211
	}
212
	
213
	/**
214
	 * Moves the terminal cursor up by sending ANSI control code CUU to the terminal.
215
	 * If the cursor is already at the edge of the screen, this has no effect.
216
	 * @param int $rows number of rows the cursor should be moved up
217
	 */
218
	public function moveCursorUp($rows = 1)
219
	{
220
		$this->_writer->write("\033[" . (int) $rows . 'A');
221
	}
222
223
	/**
224
	 * Moves the terminal cursor down by sending ANSI control code CUD to the terminal.
225
	 * If the cursor is already at the edge of the screen, this has no effect.
226
	 * @param int $rows number of rows the cursor should be moved down
227
	 */
228
	public function moveCursorDown($rows = 1)
229
	{
230
		$this->_writer->write("\033[" . (int) $rows . 'B');
231
	}
232
233
	/**
234
	 * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal.
235
	 * If the cursor is already at the edge of the screen, this has no effect.
236
	 * @param int $steps number of steps the cursor should be moved forward
237
	 */
238
	public function moveCursorForward($steps = 1)
239
	{
240
		$this->_writer->write("\033[" . (int) $steps . 'C');
241
	}
242
243
	/**
244
	 * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal.
245
	 * If the cursor is already at the edge of the screen, this has no effect.
246
	 * @param int $steps number of steps the cursor should be moved backward
247
	 */
248
	public function moveCursorBackward($steps = 1)
249
	{
250
		$this->_writer->write("\033[" . (int) $steps . 'D');
251
	}
252
253
	/**
254
	 * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal.
255
	 * @param int $lines number of lines the cursor should be moved down
256
	 */
257
	public function moveCursorNextLine($lines = 1)
258
	{
259
		$this->_writer->write("\033[" . (int) $lines . 'E');
260
	}
261
262
	/**
263
	 * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal.
264
	 * @param int $lines number of lines the cursor should be moved up
265
	 */
266
	public function moveCursorPrevLine($lines = 1)
267
	{
268
		$this->_writer->write("\033[" . (int) $lines . 'F');
269
	}
270
271
	/**
272
	 * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal.
273
	 * @param int $column 1-based column number, 1 is the left edge of the screen.
274
	 * @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.
275
	 */
276
	public function moveCursorTo($column, $row = null)
277
	{
278
		if ($row === null) {
279
			$this->_writer->write("\033[" . (int) $column . 'G');
280
		} else {
281
			$this->_writer->write("\033[" . (int) $row . ';' . (int) $column . 'H');
282
		}
283
	}
284
285
	/**
286
	 * Scrolls whole page up by sending ANSI control code SU to the terminal.
287
	 * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows.
288
	 * @param int $lines number of lines to scroll up
289
	 */
290
	public function scrollUp($lines = 1)
291
	{
292
		$this->_writer->write("\033[" . (int) $lines . 'S');
293
	}
294
295
	/**
296
	 * Scrolls whole page down by sending ANSI control code SD to the terminal.
297
	 * New lines are added at the top. This is not supported by ANSI.SYS used in windows.
298
	 * @param int $lines number of lines to scroll down
299
	 */
300
	public function scrollDown($lines = 1)
301
	{
302
		$this->_writer->write("\033[" . (int) $lines . 'T');
303
	}
304
305
	/**
306
	 * Saves the current cursor position by sending ANSI control code SCP to the terminal.
307
	 * Position can then be restored with {@link restoreCursorPosition}.
308
	 */
309
	public function saveCursorPosition()
310
	{
311
		$this->_writer->write("\033[s");
312
	}
313
314
	/**
315
	 * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal.
316
	 */
317
	public function restoreCursorPosition()
318
	{
319
		$this->_writer->write("\033[u");
320
	}
321
322
	/**
323
	 * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal.
324
	 * Use {@link showCursor} to bring it back.
325
	 * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit.
326
	 */
327
	public function hideCursor()
328
	{
329
		$this->_writer->write("\033[?25l");
330
	}
331
332
	/**
333
	 * Will show a cursor again when it has been hidden by {@link hideCursor}  by sending ANSI DECTCEM code ?25h to the terminal.
334
	 */
335
	public function showCursor()
336
	{
337
		$this->_writer->write("\033[?25h");
338
	}
339
340
	/**
341
	 * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal.
342
	 * Cursor position will not be changed.
343
	 * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen.
344
	 */
345
	public function clearScreen()
346
	{
347
		$this->_writer->write("\033[2J");
348
	}
349
350
	/**
351
	 * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal.
352
	 * Cursor position will not be changed.
353
	 */
354
	public function clearScreenBeforeCursor()
355
	{
356
		$this->_writer->write("\033[1J");
357
	}
358
359
	/**
360
	 * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal.
361
	 * Cursor position will not be changed.
362
	 */
363
	public function clearScreenAfterCursor()
364
	{
365
		$this->_writer->write("\033[0J");
366
	}
367
368
	/**
369
	 * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal.
370
	 * Cursor position will not be changed.
371
	 */
372
	public function clearLine()
373
	{
374
		$this->_writer->write("\033[2K");
375
	}
376
377
	/**
378
	 * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal.
379
	 * Cursor position will not be changed.
380
	 */
381
	public function clearLineBeforeCursor()
382
	{
383
		$this->_writer->write("\033[1K");
384
	}
385
386
	/**
387
	 * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal.
388
	 * Cursor position will not be changed.
389
	 */
390
	public function clearLineAfterCursor()
391
	{
392
		$this->_writer->write("\033[0K");
393
	}
394
	
395
396
	/**
397
	 * Returns terminal screen size.
398
	 *
399
	 * Usage:
400
	 *
401
	 * <code>
402
	 * [$width, $height] = TShellWriter::getScreenSize();
403
	 * </code>
404
	 *
405
	 * @param bool $refresh whether to force checking and not re-use cached size value.
406
	 * This is useful to detect changing window size while the application is running but may
407
	 * not get up to date values on every terminal.
408
	 * @return array|bool An array of ($width, $height) or false when it was not able to determine size.
409
	 */
410
	public static function getScreenSize($refresh = false)
411
	{
412
		static $size;
413
		if ($size !== null && !$refresh) {
414
			return $size;
415
		}
416
417
		if (static::isRunningOnWindows()) {
418
			$output = [];
419
			exec('mode con', $output);
420
			if (isset($output[1]) && strpos($output[1], 'CON') !== false) {
421
				return $size = [(int) preg_replace('~\D~', '', $output[4]), (int) preg_replace('~\D~', '', $output[3])];
422
			}
423
		} else {
424
			// try stty if available
425
			$stty = [];
426
			if (exec('stty -a 2>&1', $stty)) {
427
				$stty = implode(' ', $stty);
428
429
				// Linux stty output
430
				if (preg_match('/rows\s+(\d+);\s*columns\s+(\d+);/mi', $stty, $matches)) {
431
					return $size = [(int) $matches[2], (int) $matches[1]];
432
				}
433
434
				// MacOS stty output
435
				if (preg_match('/(\d+)\s+rows;\s*(\d+)\s+columns;/mi', $stty, $matches)) {
436
					return $size = [(int) $matches[2], (int) $matches[1]];
437
				}
438
			}
439
440
			// fallback to tput, which may not be updated on terminal resize
441
			if (($width = (int) exec('tput cols 2>&1')) > 0 && ($height = (int) exec('tput lines 2>&1')) > 0) {
442
				return $size = [$width, $height];
443
			}
444
445
			// fallback to ENV variables, which may not be updated on terminal resize
446
			if (($width = (int) getenv('COLUMNS')) > 0 && ($height = (int) getenv('LINES')) > 0) {
447
				return $size = [$width, $height];
448
			}
449
		}
450
451
		return $size = false;
452
	}
453
454
	/**
455
	 * Word wrap text with indentation to fit the screen size.
456
	 *
457
	 * If screen size could not be detected, or the indentation is greater than the screen size, the text will not be wrapped.
458
	 *
459
	 * The first line will **not** be indented, so `TShellWriter::wrapText("Lorem ipsum dolor sit amet.", 4)` will result in the
460
	 * following output, given the screen width is 16 characters:
461
	 *
462
	 * <code>
463
	 * Lorem ipsum
464
	 *     dolor sit
465
	 *     amet.
466
	 * </code>
467
	 *
468
	 * @param string $text the text to be wrapped
469
	 * @param int $indent number of spaces to use for indentation.
470
	 * @param bool $refresh whether to force refresh of screen size.
471
	 * This will be passed to {@link getScreenSize}.
472
	 * @return string the wrapped text.
473
	 */
474
	public function wrapText($text, $indent = 0, $refresh = false)
475
	{
476
		$size = static::getScreenSize($refresh);
477
		if ($size === false || $size[0] <= $indent) {
478
			return $text;
479
		}
480
		$pad = str_repeat(' ', $indent);
481
		$lines = explode("\n", wordwrap($text, $size[0] - $indent, "\n"));
482
		$first = true;
483
		foreach ($lines as $i => $line) {
484
			if ($first) {
485
				$first = false;
486
				continue;
487
			}
488
			$lines[$i] = $pad . $line;
489
		}
490
491
		return implode("\n", $lines);
492
	}
493
494
	/**
495
	 * Returns true if the console is running on windows.
496
	 * @return bool
497
	 */
498
	public static function isRunningOnWindows()
499
	{
500
		return DIRECTORY_SEPARATOR === '\\';
501
	}
502
}
503