File Coverage

blib/lib/App/ansicolumn.pm
Criterion Covered Total %
statement 59 277 21.3
branch 0 84 0.0
condition 0 69 0.0
subroutine 20 37 54.0
pod 0 15 0.0
total 79 482 16.3


line stmt bran cond sub pod time code
1             package App::ansicolumn;
2              
3             our $VERSION = "1.35";
4              
5 1     1   825 use v5.14;
  1         4  
6 1     1   5 use warnings;
  1         2  
  1         28  
7 1     1   623 use utf8;
  1         15  
  1         5  
8 1     1   611 use Encode;
  1         10287  
  1         72  
9 1     1   603 use open IO => 'utf8', ':std';
  1         1238  
  1         6  
10 1     1   666 use Pod::Usage;
  1         39207  
  1         159  
11 1     1   642 use Getopt::EX::Long qw(:DEFAULT Configure ExConfigure);
  1         46280  
  1         224  
12             ExConfigure BASECLASS => [ __PACKAGE__, "Getopt::EX" ];
13             Configure qw(bundling);
14              
15 1     1   25 use Data::Dumper;
  1         3  
  1         45  
16 1     1   5 use List::Util qw(max sum);
  1         2  
  1         52  
17 1     1   572 use Text::ANSI::Fold qw(ansi_fold);
  1         67413  
  1         94  
18 1     1   455 use Text::ANSI::Fold::Util qw(ansi_width);
  1         656  
  1         63  
19 1     1   469 use Text::ANSI::Printf qw(ansi_printf ansi_sprintf);
  1         2953  
  1         70  
20 1     1   518 use App::ansicolumn::Util;
  1         3  
  1         30  
21 1     1   443 use App::ansicolumn::Border;
  1         6  
  1         44  
22 1     1   497 use Getopt::EX::RPN qw(rpn_calc);
  1         6390  
  1         104  
23              
24             my %DEFAULT_COLORMAP = (
25             BORDER => '',
26             TEXT => '',
27             );
28              
29 1     1   548 use Getopt::EX::Hashed 1.05; {
  1         7503  
  1         16  
30              
31             Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
32              
33             has debug => ' ' ;
34             has help => ' h ' ;
35             has version => ' v ' ;
36             has width => ' =s w c ' ;
37             has fillrows => ' x ' ;
38             has table => ' t ' ;
39             has table_columns_limit => ' =i l ' , default => 0 ;
40             has table_right => ' =s R ' , default => '' ;
41             has separator => ' =s s ' , default => ' ' ;
42             has regex_sep => ' r ' ;
43             has output_separator => ' =s o ' , default => ' ' ;
44             has document => ' D ' ;
45             has parallel => ' ! V ' ;
46             has filename => ' ! H ' ;
47             has filename_format => ' =s ' , default => ': %s';
48             has pages => ' ! ' ;
49             has up => ' :i U ' ;
50             has page => ' :i P ' , min => 0;
51             has pane => ' =s C ' , default => 0 ;
52             has pane_width => ' =s S pw ' ;
53             has widen => ' ! W ' ;
54             has paragraph => ' ! p ' ;
55             has height => ' =s ' , default => 0 ;
56             has column_unit => ' =i cu ' , min => 1, default => 8 ;
57             has margin => ' =i ' , min => 0, default => 1 ;
58             has tabstop => ' =i ' , min => 1, default => 8 ;
59             has tabhead => ' =s ' ;
60             has tabspace => ' =s ' ;
61             has tabstyle => ' :s ts ' ;
62             has ignore_space => ' ! is ' , default => 1 ;
63             has linestyle => ' =s ls ' , default => '' ;
64             has boundary => ' =s ' , default => '' ;
65             has linebreak => ' =s lb ' , default => '' ;
66             has runin => ' =i ' , min => 0, default => 2 ;
67             has runout => ' =i ' , min => 0, default => 2 ;
68             has run => ' =i ' ;
69             has pagebreak => ' ! ' , default => 1 ;
70             has border => ' :s ' ; has B => '' , action => sub { $_->border = '' } ;
71             has border_style => ' =s bs ' , default => 'box' ;
72             has white_space => ' ! ' , default => 2 ;
73             has isolation => ' ! ' , default => 2 ;
74             has fillup => ' :s ' ; has F => '' , action => sub { $_->fillup = '' } ;
75             has fillup_str => ' :s ' , default => '' ;
76             has ambiguous => ' =s ' , default => 'narrow' ;
77             has discard_el => ' ! ' , default => 1 ;
78             has padchar => ' =s ' , default => ' ' ;
79             has colormap => ' =s@ cm ' , default => [] ;
80              
81             has '+boundary' => any => [ qw(none word space) ] ;
82             has '+linestyle' => any => [ qw(none wordwrap wrap truncate) ] ;
83             has '+fillup' => any => [ qw(pane page none), '' ] ;
84             has '+ambiguous' => any => [ qw(wide narrow) ] ;
85              
86             # --2up .. --9up
87             my $nup = sub { $_[0] =~ /^(\d+)/ and $_->up = $1 } ;
88             for my $n (2..9) {
89             has "${n}up" => '', action => $nup;
90             }
91              
92             # for run-time use
93             has span => ;
94             has panes => ;
95             has border_height => ;
96             has current_page => ;
97              
98             Getopt::EX::Hashed->configure( DEFAULT => [] );
99              
100             has '+help' => sub {
101             pod2usage
102             -verbose => 99,
103             -sections => [ qw(SYNOPSIS VERSION) ];
104             };
105              
106             has '+version' => sub {
107             say "Version: $VERSION";
108             exit;
109             };
110              
111             ### RPN calc for --height, --width, --pane, --pane-width
112             has [ qw(+height +width +pane +pane_width) ] => sub {
113             my $obj = $_;
114             my($name, $val) = @_;
115             $obj->$name = $val !~ /\D/ ? $val : do {
116             my $init = $name =~ /height/ ? $obj->term_height : $obj->term_width;
117             rpn_calc($init, $val) // die "$val: invalid $name.\n";
118             };
119             };
120              
121             ### --ambiguous=wide
122             has '+ambiguous' => sub {
123             if ($_[1] eq 'wide') {
124             $Text::VisualWidth::PP::EastAsian = 1;
125             Text::ANSI::Fold->configure(ambiguous => 'wide');
126             }
127             };
128              
129             ### --run
130             has '+run' => sub {
131             $_->runin = $_->runout = $_[1];
132             };
133              
134             ### --tabstop, --tabstyle
135             has [ qw(+tabstop +tabstyle) ] => sub {
136             my($name, $val) = map "$_", @_;
137             if ($val eq '') {
138             list_tabstyle();
139             exit;
140             }
141             Text::ANSI::Fold->configure($name => $val);
142             };
143              
144             ### --tabhead, --tabspace
145 1     1   1972 use charnames ':loose';
  1         740  
  1         8  
146             has [ qw(+tabhead +tabspace) ] => sub {
147             my($name, $c) = map "$_", @_;
148             $c = charnames::string_vianame($c) || die "$c: invalid name\n"
149             if length($c) > 1;
150             Text::ANSI::Fold->configure($name => $c);
151             };
152              
153             has TERM_SIZE => ;
154             has COLORHASH => default => { %DEFAULT_COLORMAP };
155             has COLORLIST => default => [];
156             has COLOR => ;
157             has BORDER => ;
158              
159 1     1   448 } no Getopt::EX::Hashed;
  1         3  
  1         9  
160              
161             sub list_tabstyle {
162 0     0 0   my %style = %Text::ANSI::Fold::TABSTYLE;
163 0           my $max = max map length, keys %style;
164 0           for my $name (sort keys %style) {
165 0           my($head, $space) = @{$style{$name}};
  0            
166 0           printf "%*s %s%s\n", $max, $name, $head, $space x 7;
167             }
168             }
169              
170             sub perform {
171 0     0 0   my $obj = shift;
172 0           local @ARGV = decode_argv(@_);
173 0 0         $obj->getopt || pod2usage(2);
174              
175 0           $obj->setup_options;
176              
177 0 0         warn Dumper $obj if $obj->debug;
178              
179 0 0         my @files = $obj->read_files(@ARGV ? @ARGV : '-') or return 1;
    0          
180              
181 0 0         if ($obj->table) {
    0          
182 0           my @lines = map { @{$_->{data}} } @files;
  0            
  0            
183 0           $obj->table_out(@lines);
184             }
185             elsif ($obj->parallel) {
186 0           $obj->parallel_out(@files);
187             }
188             else {
189 0           $obj->nup_out(@files);
190             }
191              
192 0           return 0
193             }
194              
195             sub setup_options {
196 0     0 0   my $obj = shift;
197              
198             ## --parallel or @ARGV > 1
199 0 0 0       if ($obj->parallel //= @ARGV > 1) {
200 0   0       $obj->linestyle ||= 'wrap';
201 0   0       $obj->widen //= 1;
202 0   0       $obj->border //= '';
203             }
204              
205             ## --border takes optional border-style value
206 0 0         if (defined(my $border = $obj->border)) {
207 0 0         if ($border ne '') {
208 0           $obj->border_style = $border;
209             }
210 0           $obj->border = 1;
211 0   0       $obj->fillup //= 'pane';
212             }
213              
214             ## --linestyle
215 0 0         if ($obj->linestyle eq 'wordwrap') {
216 0           $obj->linestyle = 'wrap';
217 0           $obj->boundary = 'word';
218             }
219              
220             ## -P
221 0 0         if (defined $obj->page) {
222 0 0 0       $obj->widen = 1 if $obj->pane and not $obj->pane_width;
223 0   0       $obj->height ||= $obj->page || $obj->term_height - 1;
      0        
224 0   0       $obj->linestyle ||= 'wrap';
225 0   0       $obj->border //= 1;
226 0   0       $obj->fillup //= 'pane';
227             }
228              
229             ## -U
230 0 0         if ($obj->up) {
231 0           $obj->pane = $obj->up;
232 0           $obj->widen = 1;
233 0   0       $obj->linestyle ||= 'wrap';
234 0   0       $obj->border //= 1;
235 0   0       $obj->fillup //= 'pane';
236             }
237              
238             ## -D
239 0 0         if ($obj->document) {
240 0           $obj->widen = 1;
241 0   0       $obj->linebreak ||= 'all';
242 0   0       $obj->linestyle ||= 'wrap';
243 0   0       $obj->boundary ||= 'word';
244 0 0         $obj->white_space = 0 if $obj->white_space > 1;
245 0 0         $obj->isolation = 0 if $obj->isolation > 1;
246             }
247              
248             ## --colormap
249 1     1   722 use Getopt::EX::Colormap;
  1         3  
  1         1242  
250             my $cm = Getopt::EX::Colormap
251             ->new(HASH => $obj->{COLORHASH}, LIST => $obj->{COLORLIST})
252 0           ->load_params(@{$obj->colormap});
  0            
253 0     0     $obj->{COLOR} = sub { $cm->color(@_) };
  0            
254              
255             ## --border
256 0 0         if ($obj->border) {
257 0           my $style = $obj->border_style;
258 0   0       ($obj->{BORDER} = App::ansicolumn::Border->new)
259             ->style($style) // die "$style: Unknown style.\n";
260             }
261              
262 0           $obj;
263             }
264              
265             sub color {
266 0     0 0   my $obj = shift;
267 0           $obj->{COLOR}->(@_);
268             }
269              
270             sub parallel_out {
271 0     0 0   my $obj = shift;
272 0           my @files = @_;
273              
274 0           my $max_line_length = max map { $_->{length} } @files;
  0            
275 0   0       $obj->pane ||= @files;
276 0           $obj->set_horizontal($max_line_length);
277 0           $obj->set_contents($_->{data}) for @files;
278              
279 0           while (@files) {
280 0           my @rows = splice @files, 0, $obj->pane;
281 0           my $max_length = max map { int @{$_->{data}} } @rows;
  0            
  0            
282 0 0         if ($obj->filename) {
283 0           my $w = $obj->span + $obj->border_width('center');
284 0           my $format = "%-${w}.${w}s" x (@rows - 1) . "%s\n";
285             ansi_printf $format, map {
286 0           ansi_sprintf $obj->filename_format, $_->{name};
  0            
287             } @rows;
288             }
289             $obj->column_out(map {
290 0           my $data = $_->{data};
  0            
291 0           my $length = @$data;
292 0           push @$data, (($obj->fillup_str) x ($max_length - $length));
293 0           $data;
294             } @rows);
295             }
296 0           return $obj;
297             }
298              
299             sub nup_out {
300 0     0 0   my $obj = shift;
301 0           my @files = @_;
302 0           my $max_length = max map { $_->{length} } @files;
  0            
303 0           $obj->set_horizontal($max_length);
304 0     0     my $reset = do { my @o = %$obj; sub { %$obj = @o } };
  0            
  0            
  0            
305 0           for my $file (@files) {
306 0           my $data = $file->{data};
307 0 0         next if @$data == 0;
308 0           $obj->set_contents($data)
309             ->set_vertical($data)
310             ->set_layout($data)
311             ->page_out(@$data);
312 0           $reset->();
313             }
314 0           return $obj;
315             }
316              
317             sub read_files {
318 0     0 0   my $obj = shift;
319 0           my @files;
320 0           for my $file (@_) {
321 0 0         open my $fh, $file or die "$file: $!";
322 0   0       my $content = do { local $/; <$fh> } // do {
  0            
  0            
323 0 0         warn "$file: $!\n" if $!;
324 0           next;
325             };
326 0 0         my @data = $obj->pages ? split(/\f/, $content) : $content;
327 0           for my $data (@data) {
328 0           my @line = split /\n/, $data;
329 0 0         @line = insert_space @line if $obj->paragraph;
330 0           my $length = do {
331 0 0         if ($obj->table) {
332 0           max map length, @line;
333             } else {
334 0           $obj->expand_tab(\@line, \my @length);
335 0           max @length;
336             }
337             };
338 0   0       push @files, {
339             name => $file,
340             length => $length // 0,
341             data => \@line,
342             };
343             }
344             }
345 0           @files;
346             }
347              
348             sub expand_tab {
349 0     0 0   my $obj = shift;
350 0           my($dp, $lp) = @_;
351 0           for (@$dp) {
352 0           ($_, my($dmy, $length)) = ansi_fold $_, -1, expand => 1;
353 0           push @$lp, $length;
354             }
355             }
356              
357             sub set_horizontal {
358 0     0 0   my $obj = shift;
359 0           my $max_data_length = shift;
360              
361 1     1   8 use integer;
  1         2  
  1         17  
362 0           my $width = $obj->get_width - $obj->border_width(qw(left right));
363 0   0       my $unit = $obj->column_unit // 1;
364              
365 0           my $span;
366             my $panes;
367 0   0       my $claim = sum($max_data_length,
368             $obj->runin_margin,
369             $obj->border_width('center') || $obj->margin);
370 0 0 0       if ($obj->widen and not $obj->pane_width) {
371 0   0       $panes = $obj->pane || $width / $claim || 1;
372 0           $span = ($width + $obj->border_width('center')) / $panes;
373             } else {
374 0   0       $span = $obj->pane_width || roundup($claim, $unit);
375 0   0       $panes = $obj->pane || $width / $span || 1;
376             }
377 0           $span -= $obj->border_width('center');
378 0 0         $span < 1 and die "Not enough space.\n";
379              
380 0           ($obj->span, $obj->panes) = ($span, $panes);
381              
382 0           return $obj;
383             }
384              
385             sub set_contents {
386 0     0 0   my $obj = shift;
387 0           my $dp = shift;
388 0 0         (my $cell_width = $obj->span - $obj->runin_margin) < 1
389             and die "Not enough space.\n";
390             # Fold long lines
391 0 0 0       if ($obj->linestyle and $obj->linestyle ne 'none') {
392 0 0         my $fold = $obj->foldsub($cell_width) or die;
393 0           @$dp = map { $fold->($_) } @$dp;
  0            
394             }
395 0           return $obj;
396             }
397              
398             sub set_vertical {
399 0     0 0   my $obj = shift;
400 0           my $dp = shift;
401 0           $obj->border_height = do {
402 0           sum map { length > 0 }
403 0           map { $obj->get_border($_) }
  0            
404             qw(top bottom);
405             };
406 0   0       $obj->height ||= div(int @$dp, $obj->panes) + $obj->border_height;
407 0 0         die "Not enough height.\n" if $obj->effective_height <= 0;
408 0           return $obj;
409             }
410              
411             sub page_out {
412 0     0 0   my $obj = shift;
413 0           for ($obj->current_page = 0; @_; $obj->current_page++) {
414 0           my @columns = grep { @$_ } do {
  0            
415 0 0         if ($obj->fillrows) {
416 0           xpose map { [ splice @_, 0, $obj->panes ] } 1 .. $obj->effective_height;
  0            
417             } else {
418 0           map { [ splice @_, 0, $obj->effective_height ] } 1 .. $obj->panes;
  0            
419             }
420             };
421 0           $obj->column_out(@columns);
422             }
423 0           return $obj;
424             }
425              
426             sub color_border {
427 0     0 0   my $obj = shift;
428 0           $obj->color('BORDER', $obj->get_border(@_));
429             }
430              
431             sub column_out {
432 0     0 0   my $obj = shift;
433 0           my($bdr_top, $bdr_btm) = do {
434 0           map { $obj->color('BORDER', $_) }
435 0           map { $obj->get_border($_) x $obj->span }
  0            
436             qw(top bottom);
437             };
438 0 0         map { unshift @$_, $bdr_top } @_ if $bdr_top;
  0            
439 0 0         map { push @$_, $bdr_btm } @_ if $bdr_btm;
  0            
440 0           my $max = max(map { int @$_ } @_) - 1;
  0            
441 0           for my $i (0 .. $max) {
442 0 0         my $pos = $i == 0 ? 0 : $i == $max ? 2 : 1;
    0          
443             my @panes = map {
444 0 0         @$_ ? ansi_sprintf("%-$obj->{span}s", shift @$_) : ();
  0            
445             } @_;
446 0           print $obj->color_border('left', $pos, $obj->current_page);
447             print join $obj->color_border('center', $pos, $obj->current_page),
448 0           map { $obj->color('TEXT', $_) } @panes;
  0            
449 0           print $obj->color_border('right', $pos, $obj->current_page);
450 0           print "\n";
451             }
452 0           return $obj;
453             }
454              
455             sub table_out {
456 0     0 0   my $obj = shift;
457 0 0         return unless @_;
458 0           my $split = do {
459 0 0         if ($obj->separator eq ' ') {
    0          
460 0 0         $obj->ignore_space ? ' ' : qr/\s+/;
461             } elsif ($obj->regex_sep) {
462 0           qr($obj->{separator});
463             } else {
464 0           qr/[\Q$obj->{separator}\E]/;
465             }
466             };
467 0           my @lines = map { [ split $split, $_, $obj->table_columns_limit ] } @_;
  0            
468 0           my @length = map { [ map { ansi_width $_ } @$_ ] } @lines;
  0            
  0            
469 0           my @max = map { max @$_ } xpose @length;
  0            
470 0           my @align = newlist(count => int @max, default => '-',
471             [ map --$_, split /,/, $obj->table_right ] => '');
472 0           my @format = map "%$align[$_]$max[$_]s", 0 .. $#max;
473 0           for my $line (@lines) {
474 0 0         next unless @$line;
475 0           my @fmt = @format[0 .. $#{$line}];
  0            
476 0 0         $fmt[$#{$line}] = '%s' if $align[$#{$line}] eq '-';
  0            
  0            
477 0           my $format = join $obj->output_separator, @fmt;
478 0           ansi_printf $format, @$line;
479             } continue {
480 0           print "\n";
481             }
482 0           return $obj;
483             }
484              
485             1;
486              
487             __END__