File Coverage

blib/lib/App/Colorist/Colorizer.pm
Criterion Covered Total %
statement 155 179 86.5
branch 43 68 63.2
condition 12 18 66.6
subroutine 32 36 88.8
pod 17 17 100.0
total 259 318 81.4


line stmt bran cond sub pod time code
1             package App::Colorist::Colorizer;
2             $App::Colorist::Colorizer::VERSION = '0.150460';
3 1     1   6 use Moose;
  1         2  
  1         15  
4              
5 1     1   4432 use Carp;
  1         2  
  1         62  
6 1     1   4 use IO::Handle;
  1         1  
  1         34  
7 1     1   501 use IO::Select;
  1         1265  
  1         45  
8 1     1   440 use POSIX;
  1         4537  
  1         4  
9 1     1   2509 use Readonly;
  1         2179  
  1         50  
10 1     1   6 use Scalar::Util qw( refaddr );
  1         2  
  1         37  
11 1     1   384 use YAML;
  1         4957  
  1         818  
12              
13             # ABSTRACT: the brain behind App::Colorist
14              
15              
16             has configuration => (
17             is => 'ro',
18             isa => 'Str',
19             required => 1,
20             );
21              
22              
23             has ruleset => (
24             is => 'ro',
25             isa => 'Str',
26             required => 1,
27             default => 'rules',
28             );
29              
30              
31             has colorset => (
32             is => 'ro',
33             isa => 'Str',
34             required => 1,
35             default => 'colors',
36             );
37              
38              
39             has include => (
40             is => 'ro',
41             isa => 'ArrayRef',
42             traits => [ 'Array' ],
43             required => 1,
44             default => sub { [] },
45             handles => {
46             'include_paths' => 'elements',
47             },
48             );
49              
50              
51             has debug => (
52             is => 'ro',
53             isa => 'Bool',
54             required => 1,
55             default => 0,
56             );
57              
58              
59             has inputs => (
60             is => 'ro',
61             isa => 'ArrayRef',
62             lazy_build => 1,
63             traits => [ 'Array' ],
64             handles => {
65             all_inputs => 'elements',
66             },
67             );
68              
69 0     0   0 sub _build_inputs { [ \*ARGV ] }
70              
71              
72             has selected_inputs => (
73             is => 'ro',
74             isa => 'IO::Select',
75             lazy_build => 1,
76             );
77              
78             sub _build_selected_inputs {
79 1     1   3 my $self = shift;
80 1         11 my $s = IO::Select->new;
81 1         76 $s->add($self->all_inputs);
82 1         127 return $s;
83             }
84              
85              
86             has input_buffers => (
87             is => 'ro',
88             isa => 'HashRef',
89             lazy_build => 1,
90             traits => [ 'Hash' ],
91             handles => {
92             input_buffer_keys => 'keys',
93             get_input_buffer => 'get',
94             set_input_buffer => 'set',
95             },
96             );
97              
98 1     1   54 sub _build_input_buffers { +{} }
99              
100              
101             has output => (
102             is => 'ro',
103             lazy_build => 1,
104             );
105              
106 0     0   0 sub _build_output { \*STDOUT }
107              
108              
109             has search_path => (
110             is => 'ro',
111             isa => 'ArrayRef',
112             lazy_build => 1,
113             traits => [ 'Array' ],
114             handles => {
115             all_search_paths => 'elements',
116             first_path_that => 'first',
117             },
118             );
119              
120             sub _build_search_path {
121 1     1   3 my $self = shift;
122              
123             return [
124 0         0 $self->include_paths,
125 1   50     68 (grep { $_ } split /:/, ($ENV{COLORIST_CONFIG}||'')),
126             "$ENV{HOME}/.colorist",
127             '/etc/colorist',
128             ];
129             }
130              
131              
132             has ruleset_file => (
133             is => 'ro',
134             isa => 'Str',
135             lazy_build => 1,
136             );
137              
138             sub _build_ruleset_file {
139 1     1   4 my $self = shift;
140              
141 1         77 my $config = $self->configuration;
142 1         60 my $ruleset = $self->ruleset;
143              
144             my $path = $self->first_path_that(sub {
145 1 50   1   34 return 0 unless -d "$_/$config";
146 1 50       27 return 1 if -f "$_/$config/$ruleset.pl";
147 0         0 return 0;
148 1         64 });
149              
150 1 50       13 croak(qq[Unable to locate rules "$ruleset" in paths: ], join(' ', $self->all_search_paths))
151             unless defined $path;
152              
153 1         79 return "$path/$config/$ruleset.pl";
154             }
155              
156              
157             has colorset_file => (
158             is => 'ro',
159             isa => 'Str',
160             lazy_build => 1,
161             );
162              
163             sub _build_colorset_file {
164 1     1   2 my $self = shift;
165              
166 1         40 my $config = $self->configuration;
167 1         42 my $colorset = $self->colorset;
168              
169             my $path = $self->first_path_that(sub {
170 1 50   1   20 return 0 unless -d "$_/$config";
171 1 50       16 return 1 if -f "$_/$config/$colorset.yml";
172 0         0 return 0;
173 1         58 });
174              
175 1 50       9 croak(qq[Unable to locate colors "$colorset" in paths: ], join(' ', $self->all_search_paths))
176             unless defined $path;
177              
178 1         46 return "$path/$config/$colorset.yml";
179             }
180              
181              
182             has colors_mtime => (
183             is => 'rw',
184             isa => 'Int',
185             default => 0,
186             );
187              
188              
189             has colors => (
190             is => 'rw',
191             isa => 'HashRef',
192             trigger => sub {
193             my $self = shift;
194             $self->colors_mtime( (stat $self->colorset_file)[9] )
195             },
196             );
197              
198              
199             has rules_mtime => (
200             is => 'rw',
201             isa => 'Int',
202             default => 0,
203             );
204              
205              
206             has rules => (
207             is => 'rw',
208             isa => 'ArrayRef',
209             traits => [ 'Array' ],
210             trigger => sub {
211             my $self = shift;
212             $self->rules_mtime( (stat $self->ruleset_file)[9] )
213             },
214             handles => {
215             rule_pairs => [ 'natatime', 2 ],
216             },
217             );
218              
219              
220             sub load_colorset_file {
221 1     1 1 2 my $self = shift;
222 1         40 return YAML::LoadFile($self->colorset_file);
223             }
224              
225              
226             sub refresh_colorset_file {
227 15     15 1 26 my $self = shift;
228 15 100       478 if ( (stat $self->colorset_file)[9] > $self->colors_mtime ) {
229 1         5 $self->colors( $self->load_colorset_file );
230             }
231             }
232              
233              
234             sub load_ruleset_file {
235 1     1 1 4 my $self = shift;
236              
237 1         81 my $ruleset_file = $self->ruleset_file;
238 1         3 my $rules;
239              
240             {
241 1         2 package
242             ruleset;
243 1     1   388 use App::Colorist::Ruleset;
  1         2  
  1         4  
244 1 50       1171 $rules = do "$ruleset_file"
245             or Carp::croak(qq[Failed to read rule set "$ruleset_file": $@]);
246 1         21 push @$rules, qr{.*}, [ 'DEFAULT' ];
247             }
248              
249 1         52 return $rules;
250             }
251              
252              
253             sub refresh_ruleset_file {
254 15     15 1 22 my $self = shift;
255 15 100       581 if ( (stat $self->ruleset_file)[9] > $self->rules_mtime ) {
256 1         6 $self->rules( $self->load_ruleset_file );
257             }
258             }
259              
260             Readonly my %color_names => (
261             black => 0, gray => 8,
262             maroon => 1, red => 9,
263             green => 2, lime => 10,
264             olive => 3, yellow => 11,
265             navy => 4, blue => 12,
266             purple => 5, fuschia => 13,
267             teal => 6, aqua => 14,
268             silver => 7, white => 15,
269             map { ($_ => $_) } (0 .. 255),
270             );
271              
272              
273             sub print_reset_line {
274 15     15 1 20 my $self = shift;
275 15         565 my $fh = $self->output;
276              
277 15 50       503 if ($self->debug) {
278 15         37 $fh->print("{reset}");
279 15         222 return;
280             }
281              
282 0         0 $fh->print("\e[0m");
283             }
284              
285              
286             sub get_fg {
287 190     190 1 1023 my ($self, $fg) = @_;
288              
289 190 50       302 return '' unless defined $fg;
290 190 50       6042 if ($self->debug) {
291 190         572 return "{$fg}";
292             }
293             else {
294 0         0 return sprintf "\e[38;5;%03dm", $fg;
295             }
296             }
297              
298              
299             sub get_bg {
300 190     190 1 160 my ($self, $bg) = @_;
301              
302 190 50       605 return '' unless defined $bg;
303              
304 0 0       0 if ($self->debug) {
305 0         0 return "{$bg}";
306             }
307             else {
308 0         0 return sprintf "\e[48;5;%03dm", $bg;
309             }
310             }
311              
312              
313             sub gray {
314 0     0 1 0 my ($self, $offset) = @_;
315 0         0 return 232 + $offset;
316             }
317              
318              
319             sub rgb {
320 0     0 1 0 my ($self, $r, $g, $b) = @_;
321 0         0 return 16 + $r*36 + $g*6 + $b;
322             }
323              
324              
325             sub eval_color {
326 380     380 1 295 my ($self, $c) = @_;
327              
328 0         0 return !defined($c) ? undef
329             : !ref($c) ? $color_names{$c}
330 0         0 : @{$c} == 1 ? gray(@{$c})
  0         0  
331 380 0       1253 : @{$c} == 3 ? rgb(@{$c})
  0 0       0  
    50          
    100          
332             : croak("unknown color type");
333             }
334              
335              
336             sub fg {
337 190     190 1 184 my ($self, $c) = @_;
338 190         244 $self->get_fg($self->eval_color($c));
339             }
340              
341              
342             sub bg($) {
343 190     190 1 192 my ($self, $c) = @_;
344 190         238 $self->get_bg($self->eval_color($c));
345             }
346              
347              
348             sub c {
349 190     190 1 193 my ($self, $n) = @_;
350              
351 190         6063 my $c = $self->colors->{$n};
352 190 50       312 return unless defined $c;
353              
354 190         173 my ($fg, $bg);
355 190 50       301 if (ref $c eq 'HASH') {
356 0         0 $fg = $c->{fg};
357 0         0 $bg = $c->{bg};
358             }
359             else {
360 190         156 $fg = $c;
361             }
362              
363 190         310 return $self->fg($fg).$self->bg($bg);
364             }
365              
366              
367             sub run {
368 1     1 1 8 my $self = shift;
369              
370 1         5 $self->loop_and_colorize;
371             }
372              
373              
374             sub _split {
375 15     15   28 my ($line) = @_;
376 15         68 return split /^/, $line, 2;
377             }
378              
379             sub readline {
380 16     16 1 29 my ($self) = @_;
381              
382 16         568 my $s = $self->selected_inputs;
383              
384             # Empty pending buffers first
385 16         694 for my $key ($self->input_buffer_keys) {
386 15         591 my $buffer = $self->get_input_buffer($key);
387              
388 15 100 100     126 if (defined $buffer && $buffer =~ /\n/) {
389 13         34 my ($first_line, $rest) = _split($buffer);
390 13         618 $self->set_input_buffer($key, $rest);
391 13         48 return $first_line;
392             }
393             }
394              
395             # We will keep trying this until we get a full line
396 3         5 while (1) {
397              
398             # Quit if we've run out of handles
399 4 100       21 return unless $s->count > 0;
400              
401             # Otherwise, block until we have something to read
402 3         28 my @ready = $s->can_read;
403 3         138 for my $fh (@ready) {
404 3         31 $fh->blocking(0);
405              
406             # Start with the existing buffer
407 3         180 my $line = $self->get_input_buffer(refaddr($fh));
408 3 100       10 $line = '' unless defined $line;
409              
410             # Read it until we run out of input or until we hit at least one newline
411 3         7 my ($eof, $buffer);
412 3   66     15 READ: while (!defined $eof || ($eof != 0 && $line !~ /\n/)) {
      66        
413 3         50 $eof = sysread($fh, $buffer, 1024);
414 3 50       18 if (not defined $eof) {
415 0 0       0 if ($! == POSIX::EAGAIN) {
416 0         0 select undef, undef, undef, 0.1;
417 0         0 next READ;
418             }
419             else {
420 0         0 croak("Error while reading handle: $!");
421             }
422             }
423 3         39 $line .= $buffer;
424             }
425              
426 3 100       16 $s->remove($fh) if $eof == 0;
427              
428             # If we got a newline, return the first line and buffer the rest
429 3 100       56 if ($line =~ /\n/) {
430 2         7 my ($first_line, $rest) = _split($line);
431 2         124 $self->set_input_buffer(refaddr($fh), $rest);
432 2         13 return $first_line;
433             }
434              
435             # Otherwise, we got nothing, buffer all of it and keep going
436             else {
437 1         47 $self->set_input_buffer(refaddr($fh), $line);
438             }
439              
440             # Guess we will try the next ready file handle
441             }
442              
443             # Guess we'll go around again and wait for ready buffers again
444             }
445             }
446              
447              
448             sub loop_and_colorize {
449 1     1 1 2 my $self = shift;
450              
451 1         4 while (my $line = $self->readline) {
452 15         43 $self->refresh_ruleset_file;
453 15         45 $self->refresh_colorset_file;
454              
455 15         65 $self->colorize($line);
456             }
457             }
458              
459              
460             sub colorize {
461 15     15 1 151 my ($self, $line) = @_;
462 15         19 local $_ = $line;
463              
464 15         428 my $fh = $self->output;
465              
466 15         558 my $iter = $self->rule_pairs;
467 15         64 RULE: while (my ($rule, $names) = $iter->()) {
468              
469 66 100       1851 if (/^$rule$/) {
470              
471             # This sort is a little complex, so here's the explanation:
472             #
473             # We want to keep the parenthetical nesting in the correct order.
474             # This is easy when the parenthesis is separated by index. This is
475             # not easy otherwise. Here are some sample cases to explain:
476             #
477             # a(b)c - we can sort just by string position
478             # a(b(c - DITTO
479             # a)b)c - DITTO
480             # a)b(c - DITTO
481             #
482             # Hard cases:
483             #
484             # 11 <--- indexes in @- and @+
485             # a()b - sorting by group index order, ascending works
486             # XY <--- starting parenthesis = X, ending parenthesis = Y
487             #
488             # 12 <--- indexes in @- and @+
489             # a((b - we need to sort by group index order, ascending
490             # XX <--- starting parenthesis = X, ending parenthesis = Y
491             #
492             # * 21 <--- indexes in @- and @+
493             # * a))b - we need to sort by group index order, descending
494             # * YY <--- starting parenthesis = X, ending parenthesis = Y
495             #
496             # 12 <--- indexes in @- and @+
497             # a)(b - we need to sort by group index order, ascending
498             # YX <--- starting parenthesis = X, ending parenthesis = Y
499              
500 923 50 66     1498 my @pos = sort {
    100          
501 160   50     489 $a->[0] <=> $b->[0] # match index first
502             || ($a->[1] eq 'Y' and $b->[1] eq 'Y' ? $b->[2] <=> $a->[2] # X? name index (asc)
503             : $a->[2] <=> $b->[2]) # Y? XY? YX? index (desc)
504             } (
505 160   50     501 (map { [ ($-[$_] // 0), 'X', $_ ] } 0 .. $#- ),
506 15         55 (map { [ ($+[$_] // 0), 'Y', $_ ] } 0 .. $#+ ),
507             );
508 15         105 @pos = ([ 0, 'X', undef ], @pos, [ length, 'Y', undef ]);
509             #warn YAML::Dump(\@pos);
510              
511 15         21 my $offset = 0;
512 15         11 my @stack;
513 15         29 for my $pos (@pos) {
514 350         387 my ($i, $d, $n) = @$pos;
515              
516 350         263 my $color;
517 350 100       467 if ($d eq 'X') {
518 175 100       219 if (defined $n) {
519 160         298 $color = $self->c($names->[$n]);
520             }
521             else {
522 15         40 $color = $self->c('DEFAULT');
523             }
524              
525 175         374 push @stack, $color;
526             }
527             else {
528 175         143 pop @stack;
529 175 100       231 if (@stack) {
530 160         153 $color = $stack[-1];
531             }
532             else {
533 15         33 $color = $self->c('DEFAULT');
534             }
535             }
536              
537 350 50       600 if (defined $color) {
538 350         503 substr($_, $i + $offset, 0) = $color;
539 350         465 $offset += length $color;
540             }
541             }
542              
543 15         185 last RULE;
544             }
545             }
546              
547 15         91 $fh->print($_);
548 15         143 $self->print_reset_line;
549             }
550              
551             __PACKAGE__->meta->make_immutable;
552              
553             __END__
554              
555             =pod
556              
557             =encoding UTF-8
558              
559             =head1 NAME
560              
561             App::Colorist::Colorizer - the brain behind App::Colorist
562              
563             =head1 VERSION
564              
565             version 0.150460
566              
567             =head1 SYNOPSIS
568              
569             my $colorizer = App::Colorist::Colorizer->enw(
570             commandset => 'mycommand',
571             );
572             $colorizer->run;
573              
574             =head1 DESCRIPTION
575              
576             This is primarily engineered as a separate module to make testing easier. However, if you want to embed a colorizer in some other program for some reason or you want to extend colorizer, this provides the tools for that as well. This is why I decided to provide documentation for this module here.
577              
578             If you do provide extensions, I would love to see them. Patches are welcome.
579              
580             =head1 ATTRIBUTES
581              
582             =head2 configuration
583              
584             This is the name of the master configuration to use. This is usually the name of the command whose output you are colorizing. Each configuration must contain at least one ruleset and one colorset configuration. See L<App::Colorist/CONFIGURATION> for details on how this is used to locate the configuration files.
585              
586             =head2 ruleset
587              
588             This is the name of the rule set to use. See L<App::Colorist/CONFIGURATION> for how rule sets are defined and located.
589              
590             =head2 colorset
591              
592             This is the name of the color set to use. See L<App::Colorist/CONFIGURATION> for how color sets are defined and located.
593              
594             =head2 include
595              
596             This is an array of extra include paths to search when looking for colorist configuration files.
597              
598             =head2 debug
599              
600             This is mostly useful for testing the app itself. When set to a true value, the colors are not output but a numeric representation like "{12}" is output instead.
601              
602             =head2 inputs
603              
604             This is an array of file handles to use for input. A builder lazily sets this to an array containing only the C<ARGV> file handle by default. If more than one file handle is passed, this will capture output of all file handles and display from each as they come.
605              
606             =head2 selected_inputs
607              
608             This is an L<IO::Select> built from the list of input file handles in L</inputs>.
609              
610             =head2 input_buffers
611              
612             This is an array of strings used as input buffers. This is used with the non-blocking I/O code to store any partially read lines encountered.
613              
614             =head2 output
615              
616             This is the fil ehandle to use for output. A builder lazily sets this to C<STDOUT> by default.
617              
618             =head2 search_path
619              
620             This contains the full search path. You do not normally want to set this yourself, but use L</include> instead. It is lazily instantiated to includ the values set in L</include>, the value of the C<COLORIST_CONFIG> environment variable, followed by F<~/.colorist> and finally F</etc/colorist>.
621              
622             =head2 ruleset_file
623              
624             This is set to the name of the actual ruleset file found by searching L</search_paths> and L</ruleset>.
625              
626             =head2 colorset_file
627              
628             This is the actual colorset file found by searching L</search_paths> for C<colorset>.
629              
630             =head2 colors_mtime
631              
632             When the colorset file is loaded, this mtime is set to the current mtime of the file. Every time a line is colored it checks to see if the colorset file has changed and will reload it automatically if it has.
633              
634             =head2 colors
635              
636             This is the actual colorset configuration. It's a set of keys naming the various color names defined in the ruleset and the values are the color definitions. See L<App::Colorist/CONFIGURATION> for details.
637              
638             =head2 rules_mtime
639              
640             Whenever the rules are loaded, this mtime is recorded. If the file changes, the rules are reloaded.
641              
642             =head2 rules
643              
644             This contains the actual rules. This is an array where the even number indices point to a regular expression used to match lines and group submatches. The odd indices contain an array of names matching the overall match and the group matches, which are looked up in the L</colors> configuration. See L<App::Colorist/CONFIGURATION> for details.
645              
646             =head1 METHODS
647              
648             =head2 load_colorset_file
649              
650             Loads the colorset configuration using L<YAML>.
651              
652             =head2 refresh_colorset_file
653              
654             Checks to see if the L</colors> need to be reloaded and calls L</load_colorset_file> if they do.
655              
656             =head2 load_ruleset_file
657              
658             Reads in the ruleset configuration using a Perl C<do>.
659              
660             =head2 refresh_ruleset_file
661              
662             Checks to see if the ruleset file has changed since it's last load and calls L<load_ruleset_file> to reload the configuration if it has.
663              
664             =head2 print_reset_line
665              
666             Prints the escape code to reset everything to the terminal default.
667              
668             =head2 get_fg
669              
670             my $code = $c->get_fg(10);
671              
672             Returns the escape code required to change the foreground color to the given color number.
673              
674             =head2 get_bg
675              
676             my $code = $self->get_bg(10);
677              
678             Returns the escape code that will change the background color to the given color code.
679              
680             =head2 gray
681              
682             my $number = $c->gray(10);
683              
684             Given a number identifying the desired shade of gray, returns that color number. Only works on terminals supporting 256 colors.
685              
686             =head2 rgb
687              
688             my $number = $c->rgb(1, 3, 4);
689              
690             Given 3 numbers identifying the desired RGB color cube, returns that color number. Only works on terminals supporting 256 colors.
691              
692             =head2 eval_color
693              
694             my $number = $c->eval_color('blue');
695             my $number = $c->eval_color(10);
696             my $number = $c->eval_color([ 8 ]);
697             my $number = $c->eval_color([ 1, 2, 3 ]);
698              
699             Given one of the possible color configuration types from the color set configuration, returns a color number for it.
700              
701             =head2 fg
702              
703             my $code = $c->fg('blue');
704             my $code = $c->fg(10);
705             my $code = $c->fg([ 8 ]);
706             my $code = $c->fg([ 1, 2, 3 ]);
707              
708             Returns the escape code for changing the foreground color to the given color identifier.
709              
710             =head2 bg
711              
712             my $code = $c->bg('blue');
713             my $code = $c->bg(10);
714             my $code = $c->bg([ 8 ]);
715             my $code = $c->bg([ 1, 2, 3 ]);
716              
717             Returns the escape code for changing the background color to the given color identifier.
718              
719             =head2 c
720              
721             my $code = $c->c('rufus');
722              
723             Given the name of a color defined in the colorset, returns the escape codes defined for that color to change the background and foreground as configured.
724              
725             =head2 run
726              
727             Runs the colorization process to colorize input and send that to the output.
728              
729             =head2 readline
730              
731             Given an L<IO::Select> object, returns the first line it finds from the selected
732             file handles. This handles all buffering on the file handles and blocks until a
733             complete line is available. It returns only the first line that comes available.
734             It makes no guarantees about the order the file handles will be read or
735             processed. It does try to conserve memory and keep the buffers relatively small.
736              
737             =head2 loop_and_colorize
738              
739             Reads each line of input, reloads the ruleset and colorset configuration if they have changed, and calls L</colorize> to add color to the input and send it to the output.
740              
741             =head2 colorize
742              
743             $c->colorize('some input');
744              
745             Given a line of input, this method matches the ruleset rules agains the line until it finds a match. It then applies all the colors for the line and groups defined in the colorset and outputs that line to the output file handle.
746              
747             =head1 AUTHOR
748              
749             Andrew Sterling Hanenkamp <hanenkamp@cpan.org>
750              
751             =head1 COPYRIGHT AND LICENSE
752              
753             This software is copyright (c) 2015 by Qubling Software LLC.
754              
755             This is free software; you can redistribute it and/or modify it under
756             the same terms as the Perl 5 programming language system itself.
757              
758             =cut