File Coverage

blib/lib/Data/Format/Pretty/Console.pm
Criterion Covered Total %
statement 255 288 88.5
branch 104 146 71.2
condition 20 43 46.5
subroutine 25 26 96.1
pod 1 3 33.3
total 405 506 80.0


line stmt bran cond sub pod time code
1             package Data::Format::Pretty::Console;
2              
3             our $DATE = '2021-08-08'; # DATE
4             our $VERSION = '0.391'; # VERSION
5              
6 1     1   119598 use 5.010001;
  1         14  
7 1     1   5 use strict;
  1         2  
  1         21  
8 1     1   4 use warnings;
  1         2  
  1         27  
9 1     1   5 use experimental 'smartmatch';
  1         1  
  1         5  
10 1     1   2638 use Log::ger;
  1         54  
  1         6  
11              
12 1     1   316 use Scalar::Util qw(blessed);
  1         2  
  1         50  
13 1     1   865 use Text::ANSITable;
  1         79819  
  1         36  
14 1     1   469 use YAML::Any;
  1         1209  
  1         5  
15 1     1   4000 use JSON::MaybeXS;
  1         5798  
  1         3188  
16              
17             my $json = JSON::MaybeXS->new->allow_nonref;
18              
19             require Exporter;
20             our @ISA = qw(Exporter);
21             our @EXPORT_OK = qw(format_pretty);
22              
23 0     0 0 0 sub content_type { "text/plain" }
24              
25             sub format_pretty {
26 23     23 1 47831 my ($data, $opts) = @_;
27 23   50     82 $opts //= {};
28 23         92 __PACKAGE__->new($opts)->_format($data);
29             }
30              
31             # OO interface is nto documented, we use it just to subclass
32             # Data::Format::Pretty::HTML
33             sub new {
34 44     44 0 107890 my ($class, $opts) = @_;
35 44   100     189 $opts //= {};
36 44   33     386 $opts->{interactive} //= $ENV{INTERACTIVE} // (-t STDOUT);
      66        
37             $opts->{table_column_orders} //= $json->decode(
38             $ENV{FORMAT_PRETTY_TABLE_COLUMN_ORDERS})
39 44 50 0     129 if defined($ENV{FORMAT_PRETTY_TABLE_COLUMN_ORDERS});
40             $opts->{table_column_formats} //= $json->decode(
41             $ENV{FORMAT_PRETTY_TABLE_COLUMN_FORMATS})
42 44 50 0     109 if defined($ENV{FORMAT_PRETTY_TABLE_COLUMN_FORMATS});
43             $opts->{table_column_types} //= $json->decode(
44             $ENV{FORMAT_PRETTY_TABLE_COLUMN_TYPES})
45 44 50 0     103 if defined($ENV{FORMAT_PRETTY_TABLE_COLUMN_TYPES});
46 44   33     221 $opts->{list_max_columns} //= $ENV{FORMAT_PRETTY_LIST_MAX_COLUMNS};
47 44         190 bless {opts=>$opts}, $class;
48             }
49              
50             sub _is_cell_or_format_cell {
51 147     147   270 my ($self, $data, $is_format) = @_;
52              
53             # XXX currently hardcoded limits
54 147         217 my $maxlen = 1000;
55              
56 147 100 66     400 if (!ref($data) || blessed($data)) {
    100          
57 135 100       233 if (!defined($data)) {
58 8 50       44 return "" if $is_format;
59 0         0 return 1;
60             }
61 127 50       256 if (length($data) > $maxlen) {
62 0         0 return;
63             }
64 127 100       311 return "$data" if $is_format;
65 85         227 return 1;
66             } elsif (ref($data) eq 'ARRAY') {
67 9 50       22 if (grep {ref($_) && !blessed($_)} @$data) {
  21 50       70  
68 0         0 return;
69             }
70 9 50       20 my $s = join(", ", map {defined($_) ? "$_":""} @$data);
  21         86  
71 9 50       29 if (length($s) > $maxlen) {
72 0         0 return;
73             }
74 9 100       27 return $s if $is_format;
75 6         23 return 1;
76             } else {
77 3         10 return;
78             }
79             }
80              
81             # return a string when data can be represented as a cell, otherwise undef. what
82             # can be put in a table cell? a string (or stringified object) or array of
83             # strings (stringified objects) that is quite "short".
84 53     53   97 sub _format_cell { _is_cell_or_format_cell(@_, 1) }
85              
86 94     94   187 sub _is_cell { _is_cell_or_format_cell(@_, 0) }
87              
88             sub _detect_struct {
89 72     72   190 my ($self, $data) = @_;
90 72         112 my $struct;
91 72         110 my $struct_meta = {};
92              
93             # XXX perhaps, use Data::Schema later?
94             CHECK_FORMAT:
95             {
96 72         120 CHECK_SCALAR:
97             {
98 72 100 100     141 if (!ref($data) || blessed($data)) {
  72         302  
99 27         40 $struct = "scalar";
100 27         58 last CHECK_FORMAT;
101             }
102             }
103              
104             CHECK_AOA:
105             {
106 45 100       80 if (ref($data) eq 'ARRAY') {
  45         116  
107 24         37 my $numcols;
108 24         59 for my $row (@$data) {
109 27 100       84 last CHECK_AOA unless ref($row) eq 'ARRAY';
110 10 100 100     42 last CHECK_AOA if defined($numcols) && $numcols != @$row;
111 8 50       24 last CHECK_AOA if grep { !$self->_is_cell($_) } @$row;
  18         44  
112 8         22 $numcols = @$row;
113             }
114 5         10 $struct = "aoa";
115 5         17 last CHECK_FORMAT;
116             }
117             }
118              
119             CHECK_AOH:
120             {
121 40 100       69 if (ref($data) eq 'ARRAY') {
  40         89  
122 19         49 $struct_meta->{columns} = {};
123 19         38 for my $row (@$data) {
124 27 100       67 last CHECK_AOH unless ref($row) eq 'HASH';
125 20         74 for my $k (keys %$row) {
126 47 50       95 last CHECK_AOH if !$self->_is_cell($row->{$k});
127 47         113 $struct_meta->{columns}{$k} = 1;
128             }
129             }
130 12         21 $struct = "aoh";
131 12         28 last CHECK_FORMAT;
132             }
133             }
134              
135             # list of scalars/cells
136             CHECK_LIST:
137             {
138 28 100       45 if (ref($data) eq 'ARRAY') {
  28         57  
139 7         38 for (@$data) {
140 16 50       39 last CHECK_LIST unless $self->_is_cell($_);
141             }
142 7         12 $struct = "list";
143 7         15 last CHECK_FORMAT;
144             }
145             }
146              
147             # hash which contains at least one "table" (list/aoa/aoh)
148             CHECK_HOT:
149             {
150 21 50       35 last CHECK_HOT if $self->{opts}{skip_hot};
  21         45  
151 21 50       41 last CHECK_HOT unless ref($data) eq 'HASH';
152 21         33 my $has_t;
153 21         64 while (my ($k, $v) = each %$data) {
154 24         74 my ($s2, $sm2) = $self->_detect_struct($v, {skip_hot=>1});
155 24 50       62 last CHECK_HOT unless $s2;
156 24 100       136 $has_t = 1 if $s2 =~ /^(?:list|aoa|aoh|hash)$/;
157             }
158 21 100       55 last CHECK_HOT unless $has_t;
159 7         13 $struct = "hot";
160 7         11 last CHECK_FORMAT;
161             }
162              
163             # hash of scalars/cells
164             CHECK_HASH:
165             {
166 14 50       19 if (ref($data) eq 'HASH') {
  14         32  
167 14         33 for (values %$data) {
168 13 100       31 last CHECK_HASH unless $self->_is_cell($_);
169             }
170 11         17 $struct = "hash";
171 11         20 last CHECK_FORMAT;
172             }
173             }
174              
175             }
176              
177 72         223 ($struct, $struct_meta);
178             }
179              
180             # t (table) is a structure like this: {cols=>["colName1", "colName2", ...]},
181             # rows=>[ [row1.1, row1.2, ...], [row2.1, row2.2, ...], ... ], at_opts=>{...},
182             # col_widths=>{colName1=>5, ...}}. the job of this routine is to render it
183             # (currently uses Text::ANSITable).
184             sub _render_table {
185 12     12   29 my ($self, $t) = @_;
186              
187 12         19 my $colfmts;
188 12         29 my $tcff = $self->{opts}{table_column_formats};
189 12 100       29 if ($tcff) {
190 2         5 for my $tcf (@$tcff) {
191 2         5 my $match = 1;
192 2         5 my @tcols = @{ $t->{cols} };
  2         6  
193 2         9 for my $scol (keys %$tcf) {
194 3 50       12 do { $match = 0; last } unless $scol ~~ @tcols;
  0         0  
  0         0  
195             }
196 2 50       5 if ($match) {
197 2         4 $colfmts = $tcf;
198 2         5 last;
199             }
200             }
201             }
202              
203 12         21 my $coltypes;
204 12         24 my $tctt = $self->{opts}{table_column_types};
205 12 50       30 if ($tctt) {
206 0         0 for my $tct (@$tctt) {
207 0         0 my $match = 1;
208 0         0 my @tcols = @{ $t->{cols} };
  0         0  
209 0         0 for my $scol (keys %$tct) {
210 0 0       0 do { $match = 0; last } unless $scol ~~ @tcols;
  0         0  
  0         0  
211             }
212 0 0       0 if ($match) {
213 0         0 $coltypes = $tct;
214 0         0 last;
215             }
216             }
217             }
218              
219             # render using Text::ANSITable
220 12         321 my $at = Text::ANSITable->new;
221 12         140219 $at->columns($t->{cols});
222 12         364 $at->rows($t->{rows});
223 12 100       217 if ($t->{at_opts}) {
224 7         15 $at->{$_} = $t->{at_opts}{$_} for keys %{ $t->{at_opts} };
  7         33  
225             }
226 12 100       34 if ($colfmts) {
227             $at->set_column_style($_ => formats => $colfmts->{$_})
228 2         13 for keys %$colfmts;
229             }
230 12 50       118 if ($coltypes) {
231             $at->set_column_style($_ => type => $coltypes->{$_})
232 0         0 for keys %$coltypes;
233             }
234 12 50       39 if ($t->{col_widths}) {
235             $at->set_column_style($_ => width => $t->{col_widths}{$_})
236 0         0 for keys %{ $t->{col_widths} };
  0         0  
237             }
238 12         43 $at->draw;
239             }
240              
241             # format unknown structure, the default is to dump YAML structure
242             sub _format_unknown {
243 2     2   6 my ($self, $data) = @_;
244 2         10 Dump($data);
245             }
246              
247             sub _format_scalar {
248 8     8   19 my ($self, $data) = @_;
249              
250 8 100       22 my $sdata = defined($data) ? "$data" : "";
251 8 100       35 return "" if !length($sdata);
252 6 100       51 return $sdata =~ /\n\z/s ? $sdata : "$sdata\n";
253             }
254              
255             sub _format_list {
256 3     3   10 my ($self, $data) = @_;
257 3 50       12 if ($self->{opts}{interactive}) {
258              
259 3         28 require List::Util;
260 3         15 require POSIX;
261              
262             # format list as as columns (a la 'ls' output)
263              
264 3         9 my @rows = map { $self->_format_cell($_) } @$data;
  7         17  
265              
266 3   50     9 my $maxwidth = List::Util::max(map { length } @rows) // 0;
  7         20  
267 3         6 my ($termcols, $termrows);
268 3 50       11 if ($ENV{COLUMNS}) {
    50          
269 0         0 $termcols = $ENV{COLUMNS};
270 3         614 } elsif (eval { require Term::Size; 1 }) {
  3         1893  
271 3         37 ($termcols, $termrows) = Term::Size::chars(*STDOUT{IO});
272             } else {
273             # sane default, on windows we need to offset by 1 because printing
274             # at the rightmost column will cause cursor to move down one line.
275 0 0       0 $termcols = $^O =~ /Win/ ? 79 : 80;
276             }
277 3         11 my $numcols = 1;
278 3 50       11 if ($maxwidth) {
279             # | some-text-some | some-text-some... |
280             # 2/\__maxwidth__/\3/\__maxwidth__/...\2
281             #
282             # table width = (2+maxwidth) + (3+maxwidth)*(numcols-1) + 2
283             #
284             # so with a bit of algrebra, solve for numcols:
285 3         203 $numcols = int( (($termcols-1)-$maxwidth-6)/(3+$maxwidth) + 1 );
286 3 50       22 $numcols = @rows if $numcols > @rows;
287 3 50       11 $numcols = 1 if $numcols < 1;
288             }
289             $numcols = $self->{opts}{list_max_columns}
290             if defined($self->{opts}{list_max_columns}) &&
291 3 50 33     15 $numcols > $self->{opts}{list_max_columns};
292 3         18 my $numrows = POSIX::ceil(@rows/$numcols);
293 3 50       11 if ($numrows) {
294             # reduce number of columns to avoid empty columns
295 3         11 $numcols = POSIX::ceil(@rows/$numrows);
296             }
297             #say "D: $numcols x $numrows";
298              
299 3         16 my $t = {rows=>[], at_opts=>{show_header=>0}};
300 3         12 $t->{cols} = [map { "c$_" } 1..$numcols];
  3         15  
301 3 50       12 if ($numcols > 1) {
302 0         0 $t->{col_widths}{"c$_"} = $maxwidth for 1..$numcols;
303             }
304 3         19 for my $r (1..$numrows) {
305 7         15 my @trow;
306 7         14 for my $c (1..$numcols) {
307 7         16 my $idx = ($c-1)*$numrows + ($r-1);
308 7 50       26 push @trow, $idx < @rows ? $rows[$idx] : '';
309             }
310 7         13 push @{$t->{rows}}, \@trow;
  7         20  
311             }
312              
313 3         18 return $self->_render_table($t);
314              
315             } else {
316 0         0 my @rows;
317 0         0 for my $row (@$data) {
318 0   0     0 push @rows, ($row // "") . "\n";
319             }
320 0         0 return join("", @rows);
321             }
322             }
323              
324             sub _format_hash {
325 3     3   8 my ($self, $data) = @_;
326             # format hash as two-column table
327 3 50       21 if ($self->{opts}{interactive}) {
328 3         23 my $t = {cols=>[qw/key value/], rows=>[],
329             at_opts=>{}};
330 3         15 for my $k (sort keys %$data) {
331 4         7 push @{ $t->{rows} }, [$k, $self->_format_cell($data->{$k})];
  4         14  
332             }
333 3         10 return $self->_render_table($t);
334             } else {
335 0         0 my @t;
336 0         0 for my $k (sort keys %$data) {
337 0   0     0 push @t, $k, "\t", ($data->{$k} // ""), "\n";
338             }
339 0         0 return join("", @t);
340             }
341             }
342              
343             sub _format_aoa {
344 3     3   9 my ($self, $data) = @_;
345             # show aoa as table
346 3 100       14 if ($self->{opts}{interactive}) {
347 2 100       6 if (@$data) {
348 1         5 my $t = {rows=>[], at_opts=>{}};
349 1         4 $t->{cols} = [map { "column$_" } 0..@{ $data->[0] }-1];
  2         8  
  1         3  
350 1         5 for my $i (0..@$data-1) {
351 2         6 push @{ $t->{rows} },
352 2         4 [map {$self->_format_cell($_)} @{ $data->[$i] }];
  4         8  
  2         5  
353             }
354 1         3 return $self->_render_table($t);
355             } else {
356 1         4 return "";
357             }
358             } else {
359             # tab-separated
360 1         3 my @t;
361 1         3 for my $row (@$data) {
362 2         7 push @t, join("\t", map { $self->_format_cell($_) } @$row) .
  4         12  
363             "\n";
364             }
365 1         7 return join("", @t);
366             }
367             }
368              
369             sub _format_aoh {
370 6     6   14 my ($self, $data, $struct_meta) = @_;
371             # show aoh as table
372 6         10 my @cols = @{ $self->_order_table_columns(
373 6         10 [keys %{$struct_meta->{columns}}]) };
  6         29  
374 6 100       21 if ($self->{opts}{interactive}) {
375 5         20 my $t = {cols=>\@cols, rows=>[]};
376 5         18 for my $i (0..@$data-1) {
377 8         18 my $row = $data->[$i];
378 8         11 push @{ $t->{rows} }, [map {$self->_format_cell($row->{$_})} @cols];
  8         20  
  25         56  
379             }
380 5         16 return $self->_render_table($t);
381             } else {
382             # tab-separated
383 1         2 my @t;
384 1         2 for my $row (@$data) {
385 3         7 my @row = map {$self->_format_cell($row->{$_})} @cols;
  9         18  
386 3         13 push @t, join("\t", @row) . "\n";
387             }
388 1         7 return join("", @t);
389             }
390             }
391              
392             sub _format_hot {
393 2     2   6 my ($self, $data) = @_;
394             # show hot as paragraphs:
395             #
396             # key:
397             # value (table)
398             #
399             # key2:
400             # value ...
401 2         4 my @t;
402 2         8 for my $k (sort keys %$data) {
403 4         3973 push @t, "$k:\n", $self->_format($data->{$k}), "\n";
404             }
405 2         6530 return join("", @t);
406             }
407              
408             sub _format {
409 27     27   66 my ($self, $data) = @_;
410              
411 27         74 my ($struct, $struct_meta) = $self->_detect_struct($data);
412              
413 27 100       131 if (!$struct) {
    100          
    100          
    100          
    100          
    100          
    50          
414 2         7 return $self->_format_unknown($data, $struct_meta);
415             } elsif ($struct eq 'scalar') {
416 8         20 return $self->_format_scalar($data, $struct_meta);
417             } elsif ($struct eq 'list') {
418 3         20 return $self->_format_list($data, $struct_meta);
419             } elsif ($struct eq 'hash') {
420 3         11 return $self->_format_hash($data, $struct_meta);
421             } elsif ($struct eq 'aoa') {
422 3         10 return $self->_format_aoa($data, $struct_meta);
423             } elsif ($struct eq 'aoh') {
424 6         21 return $self->_format_aoh($data, $struct_meta);
425             } elsif ($struct eq 'hot') {
426 2         7 return $self->_format_hot($data, $struct_meta);
427             } else {
428 0         0 die "BUG: Unknown format `$struct`";
429             }
430             }
431              
432             sub _order_table_columns {
433             #$log->tracef('=> _order_table_columns(%s)', \@_);
434 6     6   14 my ($self, $cols) = @_;
435              
436 6         11 my $found; # whether we found an ordering in table_column_orders
437 6         17 my $tco = $self->{opts}{table_column_orders};
438 6         9 my %orders; # colname => idx
439 6 100       18 if ($tco) {
440 2 50       7 die "table_column_orders should be an arrayref"
441             unless ref($tco) eq 'ARRAY';
442             CO:
443 2         6 for my $co (@$tco) {
444 2 50       12 die "table_column_orders elements must all be arrayrefs"
445             unless ref($co) eq 'ARRAY';
446 2         5 for (@$co) {
447 7 100       25 next CO unless $_ ~~ @$cols;
448             }
449              
450 1         2 $found++;
451 1         5 for (my $i=0; $i<@$co; $i++) {
452 3         8 $orders{$co->[$i]} = $i;
453             }
454 1         3 $found++;
455 1         2 last CO;
456             }
457             }
458              
459 6         11 my @ocols;
460 6 100       15 if ($found) {
461             @ocols = sort {
462 1         15 (defined($orders{$a}) && defined($orders{$b}) ?
463 10 100 100     46 $orders{$a} <=> $orders{$b} : 0)
    50          
464             || $a cmp $b
465             } (sort @$cols);
466             } else {
467 5         25 @ocols = sort @$cols;
468             }
469              
470 6         28 \@ocols;
471             }
472              
473             1;
474             # ABSTRACT: Pretty-print data structure for console output
475              
476             __END__
477              
478             =pod
479              
480             =encoding UTF-8
481              
482             =head1 NAME
483              
484             Data::Format::Pretty::Console - Pretty-print data structure for console output
485              
486             =head1 VERSION
487              
488             This document describes version 0.391 of Data::Format::Pretty::Console (from Perl distribution Data-Format-Pretty-Console), released on 2021-08-08.
489              
490             =head1 SYNOPSIS
491              
492             In your program:
493              
494             use Data::Format::Pretty::Console qw(format_pretty);
495             ...
496             print format_pretty($result);
497              
498             Some example output:
499              
500             Scalar, format_pretty("foo"):
501              
502             foo
503              
504             List, format_pretty([1..21]):
505              
506             .------------------------------------------------------.
507             | 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 |
508             | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | |
509             '----+----+----+----+----+----+----+----+----+----+----'
510              
511             The same list, when program output is being piped (that is, (-t STDOUT) is
512             false):
513              
514             1
515             2
516             3
517             4
518             5
519             6
520             7
521             8
522             9
523             10
524             11
525             12
526             14
527             15
528             16
529             17
530             18
531             19
532             20
533             21
534              
535             Hash, format_pretty({foo=>"data",bar=>"format",baz=>"pretty",qux=>"console"}):
536              
537             +-----+---------+
538             | bar | format |
539             | baz | pretty |
540             | foo | data |
541             | qux | console |
542             '-----+---------'
543              
544             2-dimensional array, format_pretty([ [1, 2, ""], [28, "bar", 3], ["foo", 3,
545             undef] ]):
546              
547             +---------+---------+---------+
548             | 1 | 2 | |
549             | 28 | bar | 3 |
550             | foo | 3 | |
551             '---------+---------+---------'
552              
553             An array of hashrefs, such as commonly found if you use DBI's fetchrow_hashref()
554             and friends, format_pretty([ {a=>1, b=>2}, {b=>2, c=>3}, {c=>4} ]):
555              
556             .-----------.
557             | a | b | c |
558             +---+---+---+
559             | 1 | 2 | |
560             | | 2 | 3 |
561             | | | 4 |
562             '---+---+---'
563              
564             Some more complex data, format_pretty({summary => "Blah...", users =>
565             [{name=>"budi", domains=>["foo.com", "bar.com"], quota=>"1000"}, {name=>"arif",
566             domains=>["baz.com"], quota=>"2000"}], verified => 0}):
567              
568             summary:
569             Blah...
570              
571             users:
572             .---------------------------------.
573             | domains | name | quota |
574             +------------------+------+-------+
575             | foo.com, bar.com | budi | 1000 |
576             | baz.com | arif | 2000 |
577             '------------------+------+-------'
578              
579             verified:
580             0
581              
582             Structures which can't be handled yet will simply be output as YAML,
583             format_pretty({a {b=>1}}):
584              
585             ---
586             a:
587             b: 1
588              
589             =head1 DESCRIPTION
590              
591             This module is meant to output data structure in a "pretty" or "nice" format,
592             suitable for console programs. The idea of this module is that for you to just
593             merrily dump data structure to the console, and this module will figure out how
594             to best display your data to the end-user.
595              
596             Currently this module tries to display the data mostly as a nice text table (or
597             a series of text tables), and failing that, display it as YAML.
598              
599             This module takes piping into consideration, and will output a simpler, more
600             suitable format when your user pipes your program's output into some other
601             program.
602              
603             Most of the time, you don't have to configure anything, but some options are
604             provided to tweak the output.
605              
606             =for Pod::Coverage ^(content_type)$
607              
608             =head1 FUNCTIONS
609              
610             =for Pod::Coverage new
611              
612             =head2 format_pretty($data, \%opts)
613              
614             Return formatted data structure. Options:
615              
616             =over
617              
618             =item * interactive => BOOL (optional, default undef)
619              
620             If set, will override interactive terminal detection (-t STDOUT). Simpler
621             formatting will be done if terminal is non-interactive (e.g. when output is
622             piped). Using this option will force simpler/full formatting.
623              
624             =item * list_max_columns => INT
625              
626             When displaying list as columns, specify maximum number of columns. This can be
627             used to force fewer columns (for example, single column) instead of using the
628             whole available terminal width.
629              
630             =item * table_column_orders => [[COLNAME1, COLNAME2], ...]
631              
632             Specify column orders when drawing a table. If a table has all the columns, then
633             the column names will be ordered according to the specification. For example,
634             when table_column_orders is [[qw/foo bar baz/]], this table's columns will not
635             be reordered because it doesn't have all the mentioned columns:
636              
637             |foo|quux|
638              
639             But this table will:
640              
641             |apple|bar|baz|foo|quux|
642              
643             into:
644              
645             |apple|foo|bar|baz|quux|
646              
647             =item * table_column_formats => [{COLNAME=>FMT, ...}, ...]
648              
649             Specify formats for columns. Each table format specification is a hashref
650             {COLNAME=>FMT, COLNAME2=>FMT2, ...}. It will be applied to a table if the table
651             has all the columns. FMT is a format specification according to
652             L<Data::Unixish::Apply>, it's basically either a name of a dux function (e.g.
653             C<"date">) or an array of function name + arguments (e.g. C<< [['date', [align
654             => {align=>'middle'}]] >>). This will be fed to L<Text::ANSITable>'s C<formats>
655             column style.
656              
657             =item * table_column_types => [{COLNAME=>TYPE, ...}, ...]
658              
659             Specify types for columns. Each table format specification is a hashref
660             {COLNAME=>TYPE, COLNAME2=>TYPE2, ...}. It will be applied to a table if the
661             table has all the columns. TYPE is type name according to L<Sah> schema. This
662             will be fed to L<Text::ANSITable>'s C<type> column style to give hints on how to
663             format the column. Sometimes this is the simpler alternative to
664             C<table_column_formats>.
665              
666             =back
667              
668             =head1 ENVIRONMENT
669              
670             =over
671              
672             =item * INTERACTIVE => BOOL
673              
674             To set default for C<interactive> option (overrides automatic detection).
675              
676             =item * FORMAT_PRETTY_LIST_MAX_COLUMNS => INT
677              
678             To set C<list_max_columns> option.
679              
680             =item * FORMAT_PRETTY_TABLE_COLUMN_FORMATS => ARRAY (JSON)
681              
682             To set C<table_column_formats> option, interpreted as JSON.
683              
684             =item * FORMAT_PRETTY_TABLE_COLUMN_TYPES => ARRAY (JSON)
685              
686             To set C<table_column_types> option, interpreted as JSON.
687              
688             =item * FORMAT_PRETTY_TABLE_COLUMN_ORDERS => ARRAY (JSON)
689              
690             To set C<table_column_orders> option, interpreted as JSON.
691              
692             =item * COLUMNS => INT
693              
694             To override terminal width detection.
695              
696             =back
697              
698             =head1 HOMEPAGE
699              
700             Please visit the project's homepage at L<https://metacpan.org/release/Data-Format-Pretty-Console>.
701              
702             =head1 SOURCE
703              
704             Source repository is at L<https://github.com/perlancar/perl-Data-Format-Pretty-Console>.
705              
706             =head1 BUGS
707              
708             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Data-Format-Pretty-Console>
709              
710             When submitting a bug or request, please include a test-file or a
711             patch to an existing test-file that illustrates the bug or desired
712             feature.
713              
714             =head1 SEE ALSO
715              
716             Modules used for formatting: L<Text::ANSITable>, L<YAML>.
717              
718             L<Data::Format::Pretty>
719              
720             =head1 AUTHOR
721              
722             perlancar <perlancar@cpan.org>
723              
724             =head1 CONTRIBUTOR
725              
726             =for stopwords Steven Haryanto
727              
728             Steven Haryanto <sharyanto@cpan.org>
729              
730             =head1 COPYRIGHT AND LICENSE
731              
732             This software is copyright (c) 2021, 2017, 2016, 2015, 2014, 2013, 2012, 2011, 2010 by perlancar@cpan.org.
733              
734             This is free software; you can redistribute it and/or modify it under
735             the same terms as the Perl 5 programming language system itself.
736              
737             =cut