File Coverage

blib/lib/App/Colorist/Colorizer.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package App::Colorist::Colorizer;
2             $App::Colorist::Colorizer::VERSION = '0.142540';
3 1     1   6 use Moose;
  1         3  
  1         17  
4              
5 1     1   6345 use Carp;
  1         3  
  1         83  
6 1     1   6 use IO::Handle;
  1         1  
  1         35  
7 1     1   1901 use IO::Select;
  1         1958  
  1         66  
8 1     1   1106 use POSIX;
  1         8447  
  1         9  
9 1     1   4016 use Readonly;
  0            
  0            
10             use Scalar::Util qw( refaddr );
11             use YAML;
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             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             my $self = shift;
80             my $s = IO::Select->new;
81             $s->add($self->all_inputs);
82             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             sub _build_input_buffers { +{} }
99              
100              
101             has output => (
102             is => 'ro',
103             lazy_build => 1,
104             );
105              
106             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             my $self = shift;
122              
123             return [
124             $self->include_paths,
125             (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             my $self = shift;
140              
141             my $config = $self->configuration;
142             my $ruleset = $self->ruleset;
143              
144             my $path = $self->first_path_that(sub {
145             return 0 unless -d "$_/$config";
146             return 1 if -f "$_/$config/$ruleset.pl";
147             return 0;
148             });
149              
150             croak(qq[Unable to locate rules "$ruleset" in paths: ], join(' ', $self->all_search_paths))
151             unless defined $path;
152              
153             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             my $self = shift;
165              
166             my $config = $self->configuration;
167             my $colorset = $self->colorset;
168              
169             my $path = $self->first_path_that(sub {
170             return 0 unless -d "$_/$config";
171             return 1 if -f "$_/$config/$colorset.yml";
172             return 0;
173             });
174              
175             croak(qq[Unable to locate colors "$colorset" in paths: ], join(' ', $self->all_search_paths))
176             unless defined $path;
177              
178             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             my $self = shift;
222             return YAML::LoadFile($self->colorset_file);
223             }
224              
225              
226             sub refresh_colorset_file {
227             my $self = shift;
228             if ( (stat $self->colorset_file)[9] > $self->colors_mtime ) {
229             $self->colors( $self->load_colorset_file );
230             }
231             }
232              
233              
234             sub load_ruleset_file {
235             my $self = shift;
236              
237             my $ruleset_file = $self->ruleset_file;
238             my $rules;
239              
240             {
241             package
242             ruleset;
243             use App::Colorist::Ruleset;
244             $rules = do "$ruleset_file"
245             or Carp::croak(qq[Failed to read rule set "$ruleset_file": $@]);
246             push @$rules, qr{.*}, [ 'DEFAULT' ];
247             }
248              
249             return $rules;
250             }
251              
252              
253             sub refresh_ruleset_file {
254             my $self = shift;
255             if ( (stat $self->ruleset_file)[9] > $self->rules_mtime ) {
256             $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             my $self = shift;
275             my $fh = $self->output;
276              
277             if ($self->debug) {
278             $fh->print("{reset}");
279             return;
280             }
281              
282             $fh->print("\e[0m");
283             }
284              
285              
286             sub get_fg {
287             my ($self, $fg) = @_;
288              
289             return '' unless defined $fg;
290             if ($self->debug) {
291             return "{$fg}";
292             }
293             else {
294             return sprintf "\e[38;5;%03dm", $fg;
295             }
296             }
297              
298              
299             sub get_bg {
300             my ($self, $bg) = @_;
301              
302             return '' unless defined $bg;
303              
304             if ($self->debug) {
305             return "{$bg}";
306             }
307             else {
308             return sprintf "\e[48;5;%03dm", $bg;
309             }
310             }
311              
312              
313             sub gray {
314             my ($self, $offset) = @_;
315             return 232 + $offset;
316             }
317              
318              
319             sub rgb {
320             my ($self, $r, $g, $b) = @_;
321             return 16 + $r*36 + $g*6 + $b;
322             }
323              
324              
325             sub eval_color {
326             my ($self, $c) = @_;
327              
328             return !defined($c) ? undef
329             : !ref($c) ? $color_names{$c}
330             : @{$c} == 1 ? gray(@{$c})
331             : @{$c} == 3 ? rgb(@{$c})
332             : croak("unknown color type");
333             }
334              
335              
336             sub fg {
337             my ($self, $c) = @_;
338             $self->get_fg($self->eval_color($c));
339             }
340              
341              
342             sub bg($) {
343             my ($self, $c) = @_;
344             $self->get_bg($self->eval_color($c));
345             }
346              
347              
348             sub c {
349             my ($self, $n) = @_;
350              
351             my $c = $self->colors->{$n};
352             return unless defined $c;
353              
354             my ($fg, $bg);
355             if (ref $c eq 'HASH') {
356             $fg = $c->{fg};
357             $bg = $c->{bg};
358             }
359             else {
360             $fg = $c;
361             }
362              
363             return $self->fg($fg).$self->bg($bg);
364             }
365              
366              
367             sub run {
368             my $self = shift;
369              
370             $self->loop_and_colorize;
371             }
372              
373              
374             sub _split {
375             my ($line) = @_;
376             return split /^/, $line, 2;
377             }
378              
379             sub readline {
380             my ($self) = @_;
381              
382             my $s = $self->selected_inputs;
383              
384             # Empty pending buffers first
385             for my $key ($self->input_buffer_keys) {
386             my $buffer = $self->get_input_buffer($key);
387              
388             if (defined $buffer && $buffer =~ /\n/) {
389             my ($first_line, $rest) = _split($buffer);
390             $self->set_input_buffer($key, $rest);
391             return $first_line;
392             }
393             }
394              
395             # We will keep trying this until we get a full line
396             while (1) {
397              
398             # Quit if we've run out of handles
399             return unless $s->count > 0;
400              
401             # Otherwise, block until we have something to read
402             my @ready = $s->can_read;
403             for my $fh (@ready) {
404             $fh->blocking(0);
405              
406             # Start with the existing buffer
407             my $line = $self->get_input_buffer(refaddr($fh));
408             $line = '' unless defined $line;
409              
410             # Read it until we run out of input or until we hit at least one newline
411             my ($eof, $buffer);
412             do {
413             $eof = sysread($fh, $buffer, 1024);
414             if (not defined $eof) {
415             if ($! == POSIX::EAGAIN) {
416             select undef, undef, undef, 0.1;
417             next;
418             }
419             else {
420             croak("Error while reading handle: $!");
421             }
422             }
423             $line .= $buffer;
424             } while ($eof != 0 && $line !~ /\n/);
425              
426             $s->remove($fh) if $eof == 0;
427              
428             # If we got a newline, return the first line and buffer the rest
429             if ($line =~ /\n/) {
430             my ($first_line, $rest) = _split($line);
431             $self->set_input_buffer(refaddr($fh), $rest);
432             return $first_line;
433             }
434              
435             # Otherwise, we got nothing, buffer all of it and keep going
436             else {
437             $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             my $self = shift;
450              
451             while (my $line = $self->readline) {
452             $self->refresh_ruleset_file;
453             $self->refresh_colorset_file;
454              
455             $self->colorize($line);
456             }
457             }
458              
459              
460             sub colorize {
461             my ($self, $line) = @_;
462             local $_ = $line;
463              
464             my $fh = $self->output;
465              
466             my $iter = $self->rule_pairs;
467             RULE: while (my ($rule, $names) = $iter->()) {
468              
469             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             my @pos = sort {
501             $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             (map { [ ($-[$_] // 0), 'X', $_ ] } 0 .. $#- ),
506             (map { [ ($+[$_] // 0), 'Y', $_ ] } 0 .. $#+ ),
507             );
508             @pos = ([ 0, 'X', undef ], @pos, [ length, 'Y', undef ]);
509             #warn YAML::Dump(\@pos);
510              
511             my $offset = 0;
512             my @stack;
513             for my $pos (@pos) {
514             my ($i, $d, $n) = @$pos;
515              
516             my $color;
517             if ($d eq 'X') {
518             if (defined $n) {
519             $color = $self->c($names->[$n]);
520             }
521             else {
522             $color = $self->c('DEFAULT');
523             }
524              
525             push @stack, $color;
526             }
527             else {
528             pop @stack;
529             if (@stack) {
530             $color = $stack[-1];
531             }
532             else {
533             $color = $self->c('DEFAULT');
534             }
535             }
536              
537             if (defined $color) {
538             substr($_, $i + $offset, 0) = $color;
539             $offset += length $color;
540             }
541             }
542              
543             last RULE;
544             }
545             }
546              
547             $fh->print($_);
548             $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.142540
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) 2014 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