File Coverage

blib/lib/App/ansicolumn.pm
Criterion Covered Total %
statement 59 276 21.3
branch 0 82 0.0
condition 0 69 0.0
subroutine 20 37 54.0
pod 0 15 0.0
total 79 479 16.4


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