File Coverage

blib/lib/Data/Printer/Theme.pm
Criterion Covered Total %
statement 102 104 98.0
branch 39 44 88.6
condition 13 16 81.2
subroutine 16 16 100.0
pod 0 6 0.0
total 170 186 91.4


line stmt bran cond sub pod time code
1             package Data::Printer::Theme;
2 36     36   86581 use strict;
  36         66  
  36         1342  
3 36     36   191 use warnings;
  36         89  
  36         1845  
4 36     36   567 use Data::Printer::Common;
  36         65  
  36         28943  
5              
6             # the theme name
7             sub name {
8 8     8 0 46 my ($self) = @_;
9 8         45 return $self->{name};
10             }
11              
12             # true if the theme has at least one color override
13             sub customized {
14 2     2 0 6 my ($self) = @_;
15 2 100       12 return exists $self->{is_custom} ? 1 : 0;
16             }
17              
18             # displays the color as-is
19             sub color_for {
20 5     5 0 17 my ($self, $color_type) = @_;
21 5   50     34 return $self->{colors}{$color_type} || '';
22             }
23              
24             # prints the SGR (terminal) color modifier
25             sub sgr_color_for {
26 12     12 0 4333 my ($self, $color_type) = @_;
27 12 100       54 return unless exists $self->{sgr_colors}{$color_type};
28 9   100     47 return $self->{sgr_colors}{$color_type} || ''
29             }
30              
31             # prints the SGR (terminal) color reset modifier
32 1     1 0 4 sub color_reset { return "\e[m" }
33              
34             sub new {
35 274     274 0 164908 my ($class, %params) = @_;
36              
37 274         683 my $color_level = $params{color_level};
38 274         584 my $colors_to_override = $params{color_overrides};
39 274         533 my $theme_name = $params{name};
40              
41             # before we put user info on string eval, make sure
42             # it's just a module name:
43 274         1093 $theme_name =~ s/[^a-zA-Z0-9:]+//gsm;
44              
45 274         1562 my $theme = bless {
46             name => $theme_name,
47             color_level => $color_level,
48             colors => {},
49             sgr_colors => {},
50             }, $class;
51 274 100       1087 $theme->_load_theme($params{ddp}) or delete $theme->{name};
52 274         1268 $theme->_maybe_override_theme_colors($colors_to_override, $params{ddp});
53 274         1300 return $theme;
54             }
55              
56             sub _maybe_override_theme_colors {
57 274     274   752 my ($self, $colors_to_override, $ddp) = @_;
58              
59 274 50 66     993 return unless $colors_to_override
      66        
60             && ref $colors_to_override eq 'HASH'
61             && keys %$colors_to_override;
62              
63             my $error = Data::Printer::Common::_tryme(sub {
64 13     13   32 foreach my $kind (keys %$colors_to_override ) {
65 17         28 my $override = $colors_to_override->{$kind};
66 17 100       45 die "invalid color for '$kind': must be scalar not ref" if ref $override;
67 16         33 my $parsed = $self->_parse_color($override, $ddp);
68 16 100       57 if (defined $parsed) {
69 6         14 $self->{colors}{$kind} = $override;
70 6         14 $self->{sgr_colors}{$kind} = $parsed;
71 6         23 $self->{is_custom}{$kind} = 1;
72             }
73             }
74 13         75 });
75 13 100       99 if ($error) {
76 1         6 Data::Printer::Common::_warn($ddp, "error overriding color: $error. Skipping!");
77             }
78 13         927 return;
79             }
80              
81             sub _load_theme {
82 274     274   666 my ($self, $ddp) = @_;
83 274         729 my $theme_name = $self->{name};
84              
85 274         639 my $class = 'Data::Printer::Theme::' . $theme_name;
86 274         1237 my $error = Data::Printer::Common::_tryme("use $class; 1;");
87 274 100       886 if ($error) {
88 1         7 Data::Printer::Common::_warn($ddp, "error loading theme '$theme_name': $error.");
89 1         21 return;
90             }
91 273         706 my $loaded_colors = {};
92 273         468 my $loaded_colors_sgr = {};
93             $error = Data::Printer::Common::_tryme(sub {
94 273     273   489 my $class_colors;
95 36     36   374 { no strict 'refs'; $class_colors = &{ $class . '::colors'}(); }
  36         114  
  36         44161  
  273         475  
  273         490  
  273         2212  
96 273 100       1179 die "${class}::colors() did not return a hash reference"
97             unless ref $class_colors eq 'HASH';
98              
99 272         1768 foreach my $kind (keys %$class_colors) {
100 6800         12587 my $loaded_color = $class_colors->{$kind};
101 6800 50       13969 die "color for '$kind' must be a scalar in theme '$theme_name'"
102             if ref $loaded_color;
103 6800         14596 my $parsed_color = $self->_parse_color($loaded_color, $ddp);
104 6800 50       14789 if (defined $parsed_color) {
105 6800         16976 $loaded_colors->{$kind} = $loaded_color;
106 6800         22393 $loaded_colors_sgr->{$kind} = $parsed_color;
107             }
108             }
109 273         2388 });
110 273 100       2773 if ($error) {
111 1         7 Data::Printer::Common::_warn($ddp, "error loading theme '$theme_name': $error. Output will have no colors");
112 1         7 return;
113             }
114 272         767 $self->{colors} = $loaded_colors;
115 272         764 $self->{sgr_colors} = $loaded_colors_sgr;
116 272         1049 return 1;
117             }
118              
119             sub _parse_color {
120 6817     6817   15267 my ($self, $color_label, $ddp) = @_;
121 6817 50       13849 return unless defined $color_label;
122 6817 100       13504 return '' unless $color_label;
123              
124 6803         9714 my $color_code;
125 6803 100       27554 if ($color_label =~ /\Argb\((\d+),(\d+),(\d+)\)\z/) {
    100          
    100          
    100          
126 4         17 my ($r, $g, $b) = ($1, $2, $3);
127 4 100 100     47 if ($r < 256 && $g < 256 && $b < 256) {
      100        
128 1 50       4 if ($self->{color_level} == 3) {
129 1         4 $color_code = "\e[0;38;2;$r;$g;${b}m";
130             }
131             else {
132 0         0 my $reduced = _rgb2short($r,$g,$b);
133 0         0 $color_code = "\e[0;38;5;${reduced}m";
134             }
135             }
136             else {
137 3         12 Data::Printer::Common::_warn($ddp, "invalid color '$color_label': all colors must be between 0 and 255");
138             }
139             }
140             elsif ($color_label =~ /\A#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})\z/i) {
141 6770         29143 my ($r, $g, $b) = map hex($_), ($1, $2, $3);
142 6770 100       15441 if ($self->{color_level} == 3) {
143 374         606 $color_code = "\e[0;38;2;$r;$g;${b}m";
144             }
145             else {
146 6396         13052 my $reduced = _rgb2short($r,$g,$b);
147 6396         16795 $color_code = "\e[0;38;5;${reduced}m";
148             }
149             }
150             elsif ($color_label =~ /\A\e\[\d+(:?;\d+)*m\z/) {
151 2         5 $color_code = $color_label;
152             }
153             elsif ($color_label =~ /\A
154             (?:
155             \s*
156             (?:on_)?
157             (?:bright_)?
158             (?:black|red|green|yellow|blue|magenta|cyan|white)
159             )+
160             \s*\z/x
161             ) {
162 20         214 my %ansi_colors = (
163             'black' => 30, 'on_black' => 40,
164             'red' => 31, 'on_red' => 41,
165             'green' => 32, 'on_green' => 42,
166             'yellow' => 33, 'on_yellow' => 43,
167             'blue' => 34, 'on_blue' => 44,
168             'magenta' => 35, 'on_magenta' => 45,
169             'cyan' => 36, 'on_cyan' => 46,
170             'white' => 37, 'on_white' => 47,
171             'bright_black' => 90, 'on_bright_black' => 100,
172             'bright_red' => 91, 'on_bright_red' => 101,
173             'bright_green' => 92, 'on_bright_green' => 102,
174             'bright_yellow' => 93, 'on_bright_yellow' => 103,
175             'bright_blue' => 94, 'on_bright_blue' => 104,
176             'bright_magenta' => 95, 'on_bright_magenta' => 105,
177             'bright_cyan' => 96, 'on_bright_cyan' => 106,
178             'bright_white' => 97, 'on_bright_white' => 107,
179             );
180             $color_code = "\e["
181 20         167 . join(';' => map $ansi_colors{$_}, split(/\s+/, $color_label))
182             . 'm'
183             ;
184             }
185             else {
186 7         20 Data::Printer::Common::_warn($ddp, "invalid color '$color_label'");
187             }
188 6803         21832 return $color_code;
189             }
190              
191             sub _rgb2short {
192 6397     6397   11867 my ($r,$g,$b) = @_;
193 6397         13792 my @snaps = (47, 115, 155, 195, 235);
194 6397         8965 my @new;
195 6397         12164 foreach my $color ($r,$g,$b) {
196 19191         30908 my $big = 0;
197 19191         30639 foreach my $s (@snaps) {
198 95955 100       199819 $big++ if $s < $color;
199             }
200 19191         34700 push @new, $big
201             }
202 6397         17942 return $new[0]*36 + $new[1]*6 + $new[2] + 16
203             }
204              
205             1;
206             __END__
207              
208             =head1 NAME
209              
210             Data::Printer::Theme - create your own color themes for DDP!
211              
212             =head1 SYNOPSIS
213              
214             package Data::Printer::Theme::MyCustomTheme;
215              
216             sub colors {
217             return {
218             array => '#aabbcc', # array index numbers
219             number => '#aabbcc', # numbers
220             string => '#aabbcc', # strings
221             class => '#aabbcc', # class names
222             method => '#aabbcc', # method names
223             undef => '#aabbcc', # the 'undef' value
224             hash => '#aabbcc', # hash keys
225             regex => '#aabbcc', # regular expressions
226             code => '#aabbcc', # code references
227             glob => '#aabbcc', # globs (usually file handles)
228             vstring => '#aabbcc', # version strings (v5.30.1, etc)
229             lvalue => '#aabbcc', # lvalue label
230             format => '#aabbcc', # format type
231             true => '#aabbcc', # boolean type (true)
232             false => '#aabbcc', # boolean type (false)
233             repeated => '#aabbcc', # references to seen values
234             caller_info => '#aabbcc', # details on what's being printed
235             weak => '#aabbcc', # weak references flag
236             tainted => '#aabbcc', # tainted flag
237             unicode => '#aabbcc', # utf8 flag
238             escaped => '#aabbcc', # escaped characters (\t, \n, etc)
239             brackets => '#aabbcc', # (), {}, []
240             separator => '#aabbcc', # the "," between hash pairs, array elements, etc
241             quotes => '#aabbcc', # q(")
242             unknown => '#aabbcc', # any (potential) data type unknown to Data::Printer
243             };
244             }
245             1;
246              
247             Then in your C<.dataprinter> file:
248              
249             theme = MyCustomTheme
250              
251             That's it! Alternatively, you can load it at runtime:
252              
253             use DDP theme => 'MyCustomTheme';
254              
255              
256             =head1 DESCRIPTION
257              
258             Data::Printer colorizes your output by default. Originally, the only way to
259             customize colors was to override the default ones. Data::Printer 1.0 introduced
260             themes, and now you can pick a theme or create your own.
261              
262             Data::Printer comes with several themes for you to choose from:
263              
264             =over 4
265              
266             =item * L<Material|Data::Printer::Theme::Material> I<(the default)>
267              
268             =for html <a href="https://metacpan.org/pod/Data::Printer::Theme::Material"><img style="height:50%" src="https://raw.githubusercontent.com/garu/Data-Printer/master/examples/theme-material.png" alt="Material Theme" /></a>
269              
270             =item * L<Monokai|Data::Printer::Theme::Monokai>
271              
272             =for html <a href="https://metacpan.org/pod/Data::Printer::Theme::Monokai"><img style="height:50%" src="https://raw.githubusercontent.com/garu/Data-Printer/master/examples/theme-monokai.png" alt="Monokai Theme" /></a>
273              
274             =item * L<Solarized|Data::Printer::Theme::Solarized>
275              
276             =for html <a href="https://metacpan.org/pod/Data::Printer::Theme::Solarized"><img style="height:50%" src="https://raw.githubusercontent.com/garu/Data-Printer/master/examples/theme-solarized.png" alt="Solarized Theme" /></a>
277              
278             =item * L<Classic|Data::Printer::Theme::Classic> I<(original pre-1.0 colors)>
279              
280             =for html <a href="https://metacpan.org/pod/Data::Printer::Theme::Classic"><img style="height:50%" src="https://raw.githubusercontent.com/garu/Data-Printer/master/examples/theme-classic.png" alt="Classic Theme" /></a>
281              
282             =back
283              
284             Run C<< examples/try_me.pl >> to see them in action on your own terminal!
285              
286              
287             =head1 CREATING YOUR THEMES
288              
289             A theme is a module in the C<Data::Printer::Theme> namespace. It doesn't have
290             to inherit or load any module. All you have to do is implement a single
291             function, C<colors>, that returns a hash reference where keys are the
292             expected color labels, and values are the colors you want to use.
293              
294             Feel free to copy & paste the code from the SYNOPSIS and customize at will :)
295              
296             =head2 Customizing Colors
297              
298             Setting any color to C<undef> means I<< "Don't colorize this" >>.
299             Otherwise, the color is a string which can be one of the following:
300              
301             =head3 Named colors, Term::ANSIColor style (discouraged)
302              
303             Only 8 named colors are supported:
304              
305             black, red, green, yellow, blue, magenta, cyan, white
306              
307             and their C<bright_XXX>, C<on_XXX> and C<on_bright_XXX> variants.
308              
309             Those are provided only as backards compatibility with older versions
310             of Data::Printer and, because of their limitation, we encourage you
311             to try and use one of the other representations.
312              
313             =head3 SGR Escape code (Terminal style)
314              
315             You may provide any SGR escape sequence, and they will be honored
316             as long as you use double quotes (e.g. C<"\e[38;5;196m">). You may
317             use this to achieve extra control like blinking, etc. Note, however,
318             that some terminals may not support them.
319              
320             =head3 An RGB value in one of those formats (Recommended)
321              
322             'rgb(0,255,30)'
323             '#00FF3B'
324              
325             B<NOTE:> There may not be a real 1:1 conversion between RGB and
326             terminal colors. In those cases we use approximation to achieve the
327             closest option.
328              
329             =head1 SEE ALSO
330              
331             L<Data::Printer>