Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
37 / 37
95.83% covered (success)
95.83%
46 / 48
36.96% covered (danger)
36.96%
17 / 46
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Output
100.00% covered (success)
100.00%
37 / 37
95.83% covered (success)
95.83%
46 / 48
36.96% covered (danger)
36.96%
17 / 46
100.00% covered (success)
100.00%
9 / 9
323.65
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 echo
100.00% covered (success)
100.00%
1 / 1
83.33% covered (warning)
83.33%
5 / 6
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 echoln
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
6 / 6
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 write
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 color
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
11 / 11
29.41% covered (danger)
29.41%
5 / 17
100.00% covered (success)
100.00%
1 / 1
18.66
 indent
100.00% covered (success)
100.00%
8 / 8
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 8
100.00% covered (success)
100.00%
1 / 1
4
 formatText
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
9 / 9
50.00% covered (danger)
50.00%
4 / 8
100.00% covered (success)
100.00%
1 / 1
8.12
 getStream
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 hasColorSupport
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Cli;
6
7final class Output
8{
9    private mixed $stream = null;
10    private array $fg = [
11        'black' => [0, 30],
12        'gray' => [1, 30],
13        'grey' => [1, 30],
14        'red' => [0, 31],
15        'lightred' => [1, 31],
16        'green' => [0, 32],
17        'lightgreen' => [1, 32],
18        'brown' => [0, 33],
19        'yellow' => [1, 33],
20        'blue' => [0, 34],
21        'lightblue' => [1, 34],
22        'purple' => [0, 35],
23        'lightpurple' => [1, 35],
24        'magenta' => [0, 35],
25        'lightmagenta' => [1, 35],
26        'cyan' => [0, 36],
27        'lightcyan' => [1, 36],
28        'lightgray' => [0, 37],
29        'lightgrey' => [0, 37],
30        'white' => [1, 37],
31    ];
32    private array $bg = [
33        'black' => 40,
34        'red' => 41,
35        'green' => 42,
36        'yellow' => 43,
37        'blue' => 44,
38        'purple' => 45,
39        'magenta' => 45,
40        'cyan' => 46,
41        'gray' => 47,
42        'grey' => 47,
43        'white' => 47,
44    ];
45
46    public function __construct(
47        protected readonly string $target,
48    ) {}
49
50    public function echo(string $text, string $color = '', string $background = ''): void
51    {
52        $this->write($color || $background ? $this->color($text, $color, $background) : $text);
53    }
54
55    public function echoln(string $text, string $color = '', string $background = ''): void
56    {
57        $this->write(
58            ($color || $background ? $this->color($text, $color, $background) : $text) . PHP_EOL,
59        );
60    }
61
62    private function write(string $text): void
63    {
64        fwrite($this->getStream(), $text);
65        fflush($this->stream);
66    }
67
68    public function color(string $text, string $color = '', string $background = ''): string
69    {
70        if (!$this->hasColorSupport()) {
71            return $text;
72        }
73
74        $colorCode = '';
75        $backgroundCode = '';
76
77        if ($color && array_key_exists($color, $this->fg)) {
78            [$first, $second] = $this->fg[$color];
79            $colorCode = "{$first};{$second}";
80        }
81
82        if ($background && array_key_exists($background, $this->bg)) {
83            $backgroundCode = $this->bg[$background];
84        }
85
86        return $this->formatText($text, $colorCode, $backgroundCode);
87    }
88
89    public function indent(
90        string $text,
91        int $indent,
92        ?int $max = null,
93    ): string {
94        $spaces = str_repeat(' ', $indent);
95
96        /** @psalm-suppress ForbiddenCode */
97        $width = shell_exec('tput cols');
98
99        if ($width === null) {
100            // Need a way to force $width to be null in a sane way
101            // @codeCoverageIgnoreStart
102            $width = 80;
103
104            // @codeCoverageIgnoreEnd
105        }
106
107        $width = (int) $width - $indent;
108
109        if ($max !== null && $max < $width) {
110            $width = $max;
111        }
112
113        $lines = explode("\n", wordwrap($text, $width, break: "\n"));
114
115        return implode("\n", array_map(static fn($line) => $spaces . $line, $lines));
116    }
117
118    private function formatText(string $text, string $colorCode, string|int $backgroundCode): string
119    {
120        if ($colorCode && $backgroundCode) {
121            return "\033[{$colorCode};{$backgroundCode}m{$text}\033[0m";
122        }
123
124        if ($colorCode) {
125            return "\033[{$colorCode}m{$text}\033[0m";
126        }
127
128        if ($backgroundCode) {
129            return "\033[{$backgroundCode}m{$text}\033[0m";
130        }
131
132        return $text;
133    }
134
135    private function getStream(): mixed
136    {
137        if ($this->stream === null) {
138            $this->stream = fopen($this->target, mode: 'w');
139        }
140
141        return $this->stream;
142    }
143
144    private function hasColorSupport(): bool
145    {
146        if (getenv('NO_COLOR') !== false) {
147            return false;
148        }
149
150        // @codeCoverageIgnoreStart
151        if (getenv('FORCE_COLOR') !== false || getenv('COLORTERM') !== false) {
152            return true;
153        }
154
155        // Windows
156        if (DIRECTORY_SEPARATOR === '\\') {
157            if (function_exists('sapi_windows_vt100_support')) {
158                return sapi_windows_vt100_support(STDOUT);
159            }
160
161            return (
162                getenv('ANSICON') !== false
163                || getenv('ConEmuANSI') === 'ON'
164                || getenv('TERM') === 'xterm'
165            );
166        }
167
168        if (function_exists('stream_isatty')) {
169            return stream_isatty(STDOUT);
170        }
171
172        return false;
173
174        // @codeCoverageIgnoreEnd
175    }
176}