File Coverage

blib/lib/Color/ANSI/Util.pm
Criterion Covered Total %
statement 88 111 79.2
branch 36 62 58.0
condition 8 11 72.7
subroutine 25 27 92.5
pod 21 21 100.0
total 178 232 76.7


line stmt bran cond sub pod time code
1             package Color::ANSI::Util;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-06-09'; # DATE
5             our $DIST = 'Color-ANSI-Util'; # DIST
6             our $VERSION = '0.164'; # VERSION
7              
8 1     1   55023 use 5.010001;
  1         9  
9 1     1   5 use strict;
  1         2  
  1         17  
10 1     1   4 use warnings;
  1         7  
  1         25  
11              
12 1     1   489 use Color::RGB::Util qw(rgb_diff);
  1         3005  
  1         1920  
13              
14             require Exporter;
15             our @ISA = qw(Exporter);
16             our @EXPORT_OK = qw(
17             ansi16_to_rgb
18             rgb_to_ansi16
19             rgb_to_ansi16_fg_code
20             ansi16fg
21             rgb_to_ansi16_bg_code
22             ansi16bg
23              
24             ansi256_to_rgb
25             rgb_to_ansi256
26             rgb_to_ansi256_fg_code
27             ansi256fg
28             rgb_to_ansi256_bg_code
29             ansi256bg
30              
31             rgb_to_ansi24b_fg_code
32             ansi24bfg
33             rgb_to_ansi24b_bg_code
34             ansi24bbg
35              
36             rgb_to_ansi_fg_code
37             ansifg
38             rgb_to_ansi_bg_code
39             ansibg
40              
41             ansi_reset
42             );
43              
44             our %SPEC;
45              
46             my %ansi16 = (
47             0 => '000000',
48             1 => '800000',
49             2 => '008000',
50             3 => '808000',
51             4 => '000080',
52             5 => '800080',
53             6 => '008080',
54             7 => 'c0c0c0',
55             8 => '808080',
56             9 => 'ff0000',
57             10 => '00ff00',
58             11 => 'ffff00',
59             12 => '0000ff',
60             13 => 'ff00ff',
61             14 => '00ffff',
62             15 => 'ffffff',
63             );
64             my @revansi16;
65             for my $idx (sort {$a<=>$b} keys %ansi16) {
66             push @revansi16, [$ansi16{$idx}, $idx];
67             }
68              
69             my %ansi256 = (
70             %ansi16,
71              
72             16 => '000000', 17 => '00005f', 18 => '000087', 19 => '0000af', 20 => '0000d7', 21 => '0000ff',
73             22 => '005f00', 23 => '005f5f', 24 => '005f87', 25 => '005faf', 26 => '005fd7', 27 => '005fff',
74             28 => '008700', 29 => '00875f', 30 => '008787', 31 => '0087af', 32 => '0087d7', 33 => '0087ff',
75             34 => '00af00', 35 => '00af5f', 36 => '00af87', 37 => '00afaf', 38 => '00afd7', 39 => '00afff',
76             40 => '00d700', 41 => '00d75f', 42 => '00d787', 43 => '00d7af', 44 => '00d7d7', 45 => '00d7ff',
77             46 => '00ff00', 47 => '00ff5f', 48 => '00ff87', 49 => '00ffaf', 50 => '00ffd7', 51 => '00ffff',
78             52 => '5f0000', 53 => '5f005f', 54 => '5f0087', 55 => '5f00af', 56 => '5f00d7', 57 => '5f00ff',
79             58 => '5f5f00', 59 => '5f5f5f', 60 => '5f5f87', 61 => '5f5faf', 62 => '5f5fd7', 63 => '5f5fff',
80             64 => '5f8700', 65 => '5f875f', 66 => '5f8787', 67 => '5f87af', 68 => '5f87d7', 69 => '5f87ff',
81             70 => '5faf00', 71 => '5faf5f', 72 => '5faf87', 73 => '5fafaf', 74 => '5fafd7', 75 => '5fafff',
82             76 => '5fd700', 77 => '5fd75f', 78 => '5fd787', 79 => '5fd7af', 80 => '5fd7d7', 81 => '5fd7ff',
83             82 => '5fff00', 83 => '5fff5f', 84 => '5fff87', 85 => '5fffaf', 86 => '5fffd7', 87 => '5fffff',
84             88 => '870000', 89 => '87005f', 90 => '870087', 91 => '8700af', 92 => '8700d7', 93 => '8700ff',
85             94 => '875f00', 95 => '875f5f', 96 => '875f87', 97 => '875faf', 98 => '875fd7', 99 => '875fff',
86             100 => '878700', 101 => '87875f', 102 => '878787', 103 => '8787af', 104 => '8787d7', 105 => '8787ff',
87             106 => '87af00', 107 => '87af5f', 108 => '87af87', 109 => '87afaf', 110 => '87afd7', 111 => '87afff',
88             112 => '87d700', 113 => '87d75f', 114 => '87d787', 115 => '87d7af', 116 => '87d7d7', 117 => '87d7ff',
89             118 => '87ff00', 119 => '87ff5f', 120 => '87ff87', 121 => '87ffaf', 122 => '87ffd7', 123 => '87ffff',
90             124 => 'af0000', 125 => 'af005f', 126 => 'af0087', 127 => 'af00af', 128 => 'af00d7', 129 => 'af00ff',
91             130 => 'af5f00', 131 => 'af5f5f', 132 => 'af5f87', 133 => 'af5faf', 134 => 'af5fd7', 135 => 'af5fff',
92             136 => 'af8700', 137 => 'af875f', 138 => 'af8787', 139 => 'af87af', 140 => 'af87d7', 141 => 'af87ff',
93             142 => 'afaf00', 143 => 'afaf5f', 144 => 'afaf87', 145 => 'afafaf', 146 => 'afafd7', 147 => 'afafff',
94             148 => 'afd700', 149 => 'afd75f', 150 => 'afd787', 151 => 'afd7af', 152 => 'afd7d7', 153 => 'afd7ff',
95             154 => 'afff00', 155 => 'afff5f', 156 => 'afff87', 157 => 'afffaf', 158 => 'afffd7', 159 => 'afffff',
96             160 => 'd70000', 161 => 'd7005f', 162 => 'd70087', 163 => 'd700af', 164 => 'd700d7', 165 => 'd700ff',
97             166 => 'd75f00', 167 => 'd75f5f', 168 => 'd75f87', 169 => 'd75faf', 170 => 'd75fd7', 171 => 'd75fff',
98             172 => 'd78700', 173 => 'd7875f', 174 => 'd78787', 175 => 'd787af', 176 => 'd787d7', 177 => 'd787ff',
99             178 => 'd7af00', 179 => 'd7af5f', 180 => 'd7af87', 181 => 'd7afaf', 182 => 'd7afd7', 183 => 'd7afff',
100             184 => 'd7d700', 185 => 'd7d75f', 186 => 'd7d787', 187 => 'd7d7af', 188 => 'd7d7d7', 189 => 'd7d7ff',
101             190 => 'd7ff00', 191 => 'd7ff5f', 192 => 'd7ff87', 193 => 'd7ffaf', 194 => 'd7ffd7', 195 => 'd7ffff',
102             196 => 'ff0000', 197 => 'ff005f', 198 => 'ff0087', 199 => 'ff00af', 200 => 'ff00d7', 201 => 'ff00ff',
103             202 => 'ff5f00', 203 => 'ff5f5f', 204 => 'ff5f87', 205 => 'ff5faf', 206 => 'ff5fd7', 207 => 'ff5fff',
104             208 => 'ff8700', 209 => 'ff875f', 210 => 'ff8787', 211 => 'ff87af', 212 => 'ff87d7', 213 => 'ff87ff',
105             214 => 'ffaf00', 215 => 'ffaf5f', 216 => 'ffaf87', 217 => 'ffafaf', 218 => 'ffafd7', 219 => 'ffafff',
106             220 => 'ffd700', 221 => 'ffd75f', 222 => 'ffd787', 223 => 'ffd7af', 224 => 'ffd7d7', 225 => 'ffd7ff',
107             226 => 'ffff00', 227 => 'ffff5f', 228 => 'ffff87', 229 => 'ffffaf', 230 => 'ffffd7', 231 => 'ffffff',
108              
109             232 => '080808', 233 => '121212', 234 => '1c1c1c', 235 => '262626', 236 => '303030', 237 => '3a3a3a',
110             238 => '444444', 239 => '4e4e4e', 240 => '585858', 241 => '606060', 242 => '666666', 243 => '767676',
111             244 => '808080', 245 => '8a8a8a', 246 => '949494', 247 => '9e9e9e', 248 => 'a8a8a8', 249 => 'b2b2b2',
112             250 => 'bcbcbc', 251 => 'c6c6c6', 252 => 'd0d0d0', 253 => 'dadada', 254 => 'e4e4e4', 255 => 'eeeeee',
113             );
114             my @revansi256;
115             for my $idx (sort {$a<=>$b} keys %ansi256) {
116             push @revansi256, [$ansi256{$idx}, $idx];
117             }
118              
119             $SPEC{ansi16_to_rgb} = {
120             v => 1.1,
121             summary => 'Convert ANSI-16 color to RGB',
122             description => <<'_',
123              
124             Returns 6-hexdigit, e.g. 'ff00cc'.
125              
126             _
127             args => {
128             color => {
129             schema => 'color::ansi16*',
130             req => 1,
131             pos => 0,
132             },
133             },
134             args_as => 'array',
135             result => {
136             schema => 'color::rgb24*',
137             },
138             result_naked => 1,
139             };
140             sub ansi16_to_rgb {
141 4     4 1 956 my ($input) = @_;
142              
143 4 100       28 if ($input =~ /^\d+$/) {
    50          
144 2 50 33     9 if ($input >= 0 && $input <= 15) {
145 2         12 return $ansi16{$input + 0}; # to remove prefix zero e.g. "06"
146             } else {
147 0         0 die "Invalid ANSI 16-color number '$input'";
148             }
149             } elsif ($input =~ /^(?:(bold|bright) \s )?(black|red|green|yellow|blue|magenta|cyan|white)$/ix) {
150 2   100     13 my ($bold, $col) = (lc($1 // ""), lc($2));
151 2         3 my $i;
152 2 50       5 if ($col eq 'black') {
    50          
    0          
    0          
    0          
    0          
    0          
    0          
153 0         0 $i = 0;
154             } elsif ($col eq 'red') {
155 2         3 $i = 1;
156             } elsif ($col eq 'green') {
157 0         0 $i = 2;
158             } elsif ($col eq 'yellow') {
159 0         0 $i = 3;
160             } elsif ($col eq 'blue') {
161 0         0 $i = 4;
162             } elsif ($col eq 'magenta') {
163 0         0 $i = 5;
164             } elsif ($col eq 'cyan') {
165 0         0 $i = 6;
166             } elsif ($col eq 'white') {
167 0         0 $i = 7;
168             }
169 2 100       4 $i += 8 if $bold;
170 2         11 return $ansi16{$i};
171             } else {
172 0         0 die "Invalid ANSI 16-color name '$input'";
173             }
174             }
175              
176             sub _rgb_to_indexed {
177 16     16   26 my ($rgb, $table) = @_;
178              
179 16         23 my ($smallest_diff, $res);
180 16         27 for my $e (@$table) {
181 730         1298 my $diff = rgb_diff($rgb, $e->[0], 'hsv_hue1');
182             # exact match, return immediately
183 730 100       69446 return $e->[1] if $diff == 0;
184 729 100 100     2239 if (!defined($smallest_diff) || $smallest_diff > $diff) {
185 50         59 $smallest_diff = $diff;
186 50         94 $res = $e->[1];
187             }
188             }
189 15         43 return $res;
190             }
191              
192             $SPEC{ansi256_to_rgb} = {
193             v => 1.1,
194             summary => 'Convert ANSI-256 color to RGB',
195             args => {
196             color => {
197             schema => 'color::ansi256*',
198             req => 1,
199             pos => 0,
200             },
201             },
202             args_as => 'array',
203             result => {
204             schema => 'color::rgb24',
205             },
206             result_naked => 1,
207             };
208             sub ansi256_to_rgb {
209 1     1 1 2287 my ($input) = @_;
210              
211 1         3 $input += 0;
212 1 50       16 exists($ansi256{$input}) or die "Invalid ANSI 256-color index '$input'";
213 1         7 $ansi256{$input};
214             }
215              
216             $SPEC{rgb_to_ansi16} = {
217             v => 1.1,
218             summary => 'Convert RGB to ANSI-16 color',
219             args => {
220             color => {
221             schema => 'color::rgb24*',
222             req => 1,
223             pos => 0,
224             },
225             },
226             args_as => 'array',
227             result => {
228             schema => 'color::ansi16*',
229             },
230             result_naked => 1,
231             };
232             sub rgb_to_ansi16 {
233 2     2 1 5 my ($input) = @_;
234 2         5 _rgb_to_indexed($input, \@revansi16);
235             }
236              
237             $SPEC{rgb_to_ansi256} = {
238             v => 1.1,
239             summary => 'Convert RGB to ANSI-256 color',
240             args => {
241             color => {
242             schema => 'color::rgb24*',
243             req => 1,
244             pos => 0,
245             },
246             },
247             args_as => 'array',
248             result => {
249             schema => 'color::ansi256*',
250             },
251             result_naked => 1,
252             };
253             sub rgb_to_ansi256 {
254 3     3 1 8 my ($input) = @_;
255 3         8 _rgb_to_indexed($input, \@revansi256);
256             }
257              
258             $SPEC{rgb_to_ansi16_fg_code} = {
259             v => 1.1,
260             summary => 'Convert RGB to ANSI-16 color escape sequence to change foreground color',
261             args => {
262             color => {
263             schema => 'color::rgb24*',
264             req => 1,
265             pos => 0,
266             },
267             },
268             args_as => 'array',
269             result => {
270             schema => 'str*',
271             },
272             result_naked => 1,
273             };
274             sub rgb_to_ansi16_fg_code {
275 4     4 1 9 my ($input) = @_;
276              
277 4         9 my $res = _rgb_to_indexed($input, \@revansi16);
278 4 100       26 return "\e[" . ($res >= 8 ? ($res+30-8) . ";1" : ($res+30)) . "m";
279             }
280              
281 1     1 1 5 sub ansi16fg { goto &rgb_to_ansi16_fg_code }
282              
283             $SPEC{rgb_to_ansi16_bg_code} = {
284             v => 1.1,
285             summary => 'Convert RGB to ANSI-16 color escape sequence to change background color',
286             args => {
287             color => {
288             schema => 'color::rgb24*',
289             req => 1,
290             pos => 0,
291             },
292             },
293             args_as => 'array',
294             result => {
295             schema => 'str*',
296             },
297             result_naked => 1,
298             };
299             sub rgb_to_ansi16_bg_code {
300 2     2 1 4 my ($input) = @_;
301              
302 2         6 my $res = _rgb_to_indexed($input, \@revansi16);
303 2 100       14 return "\e[" . ($res >= 8 ? ($res+40-8) : ($res+40)) . "m";
304             }
305              
306 1     1 1 4 sub ansi16bg { goto &rgb_to_ansi16_bg_code }
307              
308             $SPEC{rgb_to_ansi256_fg_code} = {
309             v => 1.1,
310             summary => 'Convert RGB to ANSI-256 color escape sequence to change foreground color',
311             args => {
312             color => {
313             schema => 'color::rgb24*',
314             req => 1,
315             pos => 0,
316             },
317             },
318             args_as => 'array',
319             result => {
320             schema => 'str*',
321             },
322             result_naked => 1,
323             };
324             sub rgb_to_ansi256_fg_code {
325 3     3 1 7 my ($input) = @_;
326              
327 3         8 my $res = _rgb_to_indexed($input, \@revansi16);
328 3         14 return "\e[38;5;${res}m";
329             }
330              
331 1     1 1 5 sub ansi256fg { goto &rgb_to_ansi256_fg_code }
332              
333             $SPEC{rgb_to_ansi256_bg_code} = {
334             v => 1.1,
335             summary => 'Convert RGB to ANSI-256 color escape sequence to change background color',
336             args => {
337             color => {
338             schema => 'color::rgb24*',
339             req => 1,
340             pos => 0,
341             },
342             },
343             args_as => 'array',
344             result => {
345             schema => 'str*',
346             },
347             result_naked => 1,
348             };
349             sub rgb_to_ansi256_bg_code {
350 2     2 1 5 my ($input) = @_;
351              
352 2         6 my $res = _rgb_to_indexed($input, \@revansi16);
353 2         10 return "\e[48;5;${res}m";
354             }
355              
356 1     1 1 4 sub ansi256bg { goto &rgb_to_ansi256_bg_code }
357              
358             $SPEC{rgb_to_ansi24b_fg_code} = {
359             v => 1.1,
360             summary => 'Convert RGB to ANSI 24bit-color escape sequence to change foreground color',
361             args => {
362             color => {
363             schema => 'color::rgb24*',
364             req => 1,
365             pos => 0,
366             },
367             },
368             args_as => 'array',
369             result => {
370             schema => 'str*',
371             },
372             result_naked => 1,
373             };
374             sub rgb_to_ansi24b_fg_code {
375 4     4 1 2286 my ($rgb) = @_;
376              
377 4         30 return sprintf("\e[38;2;%d;%d;%dm",
378             hex(substr($rgb, 0, 2)),
379             hex(substr($rgb, 2, 2)),
380             hex(substr($rgb, 4, 2)));
381             }
382              
383 1     1 1 3 sub ansi24bfg { goto &rgb_to_ansi24b_fg_code }
384              
385             $SPEC{rgb_to_ansi24b_bg_code} = {
386             v => 1.1,
387             summary => 'Convert RGB to ANSI 24bit-color escape sequence to change background color',
388             args => {
389             color => {
390             schema => 'color::rgb24*',
391             req => 1,
392             pos => 0,
393             },
394             },
395             args_as => 'array',
396             result => {
397             schema => 'str*',
398             },
399             result_naked => 1,
400             };
401             sub rgb_to_ansi24b_bg_code {
402 2     2 1 4 my ($rgb) = @_;
403              
404 2         15 return sprintf("\e[48;2;%d;%d;%dm",
405             hex(substr($rgb, 0, 2)),
406             hex(substr($rgb, 2, 2)),
407             hex(substr($rgb, 4, 2)));
408             }
409              
410 1     1 1 4 sub ansi24bbg { goto &rgb_to_ansi24b_bg_code }
411              
412             our $_use_termdetsw = 1;
413             our $_color_depth; # cache, can be set during testing
414             sub _color_depth {
415 8 100   8   19 unless (defined $_color_depth) {
416             {
417 7 50       8 if (exists $ENV{NO_COLOR}) {
  7         15  
418 0         0 $_color_depth = 0;
419 0         0 last;
420             }
421 7 100 66     19 if (defined $ENV{COLOR} && !$ENV{COLOR}) {
422 1         2 $_color_depth = 0;
423 1         2 last;
424             }
425 6 100       13 if (defined $ENV{COLOR_DEPTH}) {
426 4         7 $_color_depth = $ENV{COLOR_DEPTH};
427 4         6 last;
428             }
429 2 50       5 if ($_use_termdetsw) {
430 0         0 eval { require Term::Detect::Software };
  0         0  
431 0 0       0 if (!$@) {
432 0         0 $_color_depth = Term::Detect::Software::detect_terminal_cached()->{color_depth};
433 0         0 last;
434             }
435             }
436             # simple heuristic
437 2 100       5 if ($ENV{KONSOLE_DBUS_SERVICE}) {
438 1         2 $_color_depth = 2**24;
439 1         2 last;
440             }
441             # safe value
442 1         2 $_color_depth = 16;
443             }
444             };
445 8         13 $_color_depth;
446             }
447              
448             $SPEC{rgb_to_ansi_fg_code} = {
449             v => 1.1,
450             summary => 'Convert RGB to ANSI color escape sequence to change foreground color',
451             description => <<'_',
452              
453             Autodetect terminal capability and can return either empty string, 16-color,
454             256-color, or 24bit-code.
455              
456             Color depth used is determined by `COLOR_DEPTH` environment setting or from
457             if that module is available. In other words, this
458             function automatically chooses rgb_to_ansi{24b,256,16}_fg_code().
459              
460             _
461             args => {
462             color => {
463             schema => 'color::rgb24*',
464             req => 1,
465             pos => 0,
466             },
467             },
468             args_as => 'array',
469             result => {
470             schema => 'str*',
471             },
472             result_naked => 1,
473             };
474             sub rgb_to_ansi_fg_code {
475 7     7 1 13 my ($rgb) = @_;
476 7         12 my $cd = _color_depth();
477 7 100       24 if ($cd >= 2**24) {
    100          
    100          
478 2         5 rgb_to_ansi24b_fg_code($rgb);
479             } elsif ($cd >= 256) {
480 1         3 rgb_to_ansi256_fg_code($rgb);
481             } elsif ($cd >= 16) {
482 2         6 rgb_to_ansi16_fg_code($rgb);
483             } else {
484 2         6 "";
485             }
486             }
487              
488 7     7 1 4554 sub ansifg { goto &rgb_to_ansi_fg_code }
489              
490             $SPEC{rgb_to_ansi_bg_code} = {
491             v => 1.1,
492             summary => 'Convert RGB to ANSI color escape sequence to change background color',
493             description => <<'_',
494              
495             Autodetect terminal capability and can return either empty string, 16-color,
496             256-color, or 24bit-code.
497              
498             Which color depth used is determined by `COLOR_DEPTH` environment setting or
499             from if that module is available). In other words,
500             this function automatically chooses rgb_to_ansi{24b,256,16}_bg_code().
501              
502             _
503             args => {
504             color => {
505             schema => 'color::rgb24*',
506             req => 1,
507             pos => 0,
508             },
509             },
510             args_as => 'array',
511             result => {
512             schema => 'str*',
513             },
514             result_naked => 1,
515             };
516             sub rgb_to_ansi_bg_code {
517 0     0 1 0 my ($rgb) = @_;
518 0         0 my $cd = _color_depth();
519 0 0       0 if ($cd >= 2**24) {
    0          
520 0         0 rgb_to_ansi24b_bg_code($rgb);
521             } elsif ($cd >= 256) {
522 0         0 rgb_to_ansi256_bg_code($rgb);
523             } else {
524 0         0 rgb_to_ansi16_bg_code($rgb);
525             }
526             }
527              
528 0     0 1 0 sub ansibg { goto &rgb_to_ansi_bg_code }
529              
530             sub ansi_reset {
531 2     2 1 5 my $conditional = shift;
532 2 100       6 if ($conditional) {
533 1         2 my $cd = _color_depth();
534 1 50       7 return "" if $cd < 16;
535             }
536 1         4 "\e[0m";
537             }
538              
539             1;
540             # ABSTRACT: Routines for dealing with ANSI colors
541              
542             __END__