File Coverage

blib/lib/Term/TablePrint.pm
Criterion Covered Total %
statement 45 586 7.6
branch 1 306 0.3
condition 0 69 0.0
subroutine 16 35 45.7
pod 2 2 100.0
total 64 998 6.4


line stmt bran cond sub pod time code
1             package Term::TablePrint;
2              
3 1     1   152747 use warnings;
  1         3  
  1         118  
4 1     1   19 use strict;
  1         3  
  1         33  
5 1     1   16 use 5.16.0;
  1         5  
6              
7             our $VERSION = '0.181';
8 1     1   8 use Exporter 'import';
  1         4  
  1         105  
9             our @EXPORT_OK = qw( print_table );
10              
11 1     1   8 use Carp qw( croak );
  1         1  
  1         93  
12              
13 1     1   8 use List::Util qw( sum max );
  1         3  
  1         122  
14 1     1   8 use Scalar::Util qw( looks_like_number );
  1         2  
  1         63  
15              
16 1     1   995 use Term::Choose qw( choose );
  1         117695  
  1         179  
17 1     1   14 use Term::Choose::Constants qw( EXTRA_W PH SGR_ES );
  1         2  
  1         80  
18 1     1   7 use Term::Choose::LineFold qw( print_columns cut_to_printwidth adjust_to_printwidth line_fold );
  1         2  
  1         78  
19 1     1   9 use Term::Choose::Screen qw( hide_cursor show_cursor );
  1         2  
  1         65  
20 1     1   8 use Term::Choose::ValidateOptions qw( validate_options );
  1         3  
  1         60  
21 1     1   946 use Term::Choose::Util qw( get_term_width insert_sep );
  1         59323  
  1         116  
22 1     1   770 use Term::TablePrint::ProgressBar qw();
  1         4  
  1         76  
23              
24              
25             BEGIN {
26 1 50   1   9599 if ( $^O eq 'MSWin32' ) {
27 0         0 require Win32::Console::ANSI;
28             }
29             }
30              
31              
32             sub new {
33 0     0 1   my $class = shift;
34 0 0         croak "new: called with " . @_ . " arguments - 0 or 1 arguments expected." if @_ > 1;
35 0           my ( $opt ) = @_;
36              
37             #####
38             #choose( [ 'Continue with ENTER' ], { prompt => "The 'min_col_width' option has been renamed to 'col_trim_threshold'." } ) if exists $opt->{min_col_width}; # 21.02.2026
39             #choose( [ 'Continue with ENTER' ], { prompt => "The 'max_width_exp' option has been renamed to 'expanded_max_width'." } ) if exists $opt->{max_width_exp}; # 21.02.2026
40             #####
41              
42 0           my $instance_defaults = _defaults();
43 0 0         if ( defined $opt ) {
44 0 0         croak "new: The (optional) argument is not a HASH reference." if ref $opt ne 'HASH';
45 0           validate_options( _valid_options(), $opt, 'new' );
46 0           for my $key ( keys %$opt ) {
47 0 0         $instance_defaults->{$key} = $opt->{$key} if defined $opt->{$key};
48             }
49             }
50 0           my $self = bless $instance_defaults, $class;
51 0           $self->{backup_instance_defaults} = { %$instance_defaults };
52 0           return $self;
53             }
54              
55              
56             sub _valid_options {
57             return {
58 0     0     codepage_mapping => '[ 0 1 ]',
59             expanded_line_spacing => '[ 0 1 ]',
60             hide_cursor => '[ 0 1 ]', # documentation
61             mouse => '[ 0 1 ]',
62             squash_spaces => '[ 0 1 ]',
63             table_expand => '[ 0 1 ]',
64             trunc_fract_first => '[ 0 1 ]',
65             pad_row_edges => '[ 0 1 ]',
66             binary_filter => '[ 0 1 2 ]',
67             choose_columns => '[ 0 1 2 ]',
68             color => '[ 0 1 2 ]',
69             page => '[ 0 1 2 ]', # undocumented
70             search => '[ 0 1 2 ]', #
71             keep => '[ 1-9 ][ 0-9 ]*', # undocumented
72             col_trim_threshold => '[ 0-9 ]+',
73             max_width_exp => '[ 0-9 ]+', # now expanded_max_width # 21.02.2026
74             expanded_max_width => '[ 0-9 ]+',
75             max_rows => '[ 0-9 ]+',
76             min_col_width => '[ 0-9 ]+', # now col_trim_threshold # 21.02.2026
77             progress_bar => '[ 0-9 ]+',
78             tab_width => '[ 0-9 ]+',
79             binary_string => 'Str', ##
80             decimal_separator => 'Str',
81             footer => 'Str',
82             info => 'Str',
83             prompt => 'Str',
84             undef => 'Str',
85             #thsd_sep => 'Str',
86             };
87             }
88              
89              
90             sub _defaults {
91             return {
92 0     0     binary_filter => 0,
93             binary_string => 'BNRY',
94             choose_columns => 0,
95             codepage_mapping => 0,
96             col_trim_threshold => 30,
97             color => 0,
98             decimal_separator => '.',
99             expanded_line_spacing => 1,
100             #expanded_max_width => undef,
101             #footer => undef,
102             hide_cursor => 1,
103             #info => undef,
104             #keep => undef,
105             max_rows => 0,
106             mouse => 0,
107             pad_row_edges => 0,
108             page => 2, ##
109             progress_bar => 40000,
110             prompt => '',
111             search => 1,
112             squash_spaces => 0,
113             tab_width => 2,
114             table_expand => 1,
115             trunc_fract_first => 1,
116             undef => '',
117             thsd_sep => ',', #
118             }
119             }
120              
121              
122             sub __reset {
123 0     0     my ( $self ) = @_;
124 0 0         if ( $self->{hide_cursor} ) {
125 0           print show_cursor();
126             }
127 0 0         if ( exists $self->{backup_instance_defaults} ) {
128 0           my $instance_defaults = $self->{backup_instance_defaults};
129 0           for my $key ( keys %$self ) {
130 0 0 0       if ( $key eq 'plugin' || $key eq 'backup_instance_defaults' ) {
    0          
131 0           next;
132             }
133             elsif ( exists $instance_defaults->{$key} ) {
134 0           $self->{$key} = $instance_defaults->{$key};
135             }
136             else {
137 0           delete $self->{$key};
138             }
139             }
140             }
141             }
142              
143              
144             my $last_write_table = 0;
145             my $window_width_changed = 1;
146             my $enter_search_string = 2;
147             my $from_filtered_table = 3;
148             my $choose_and_print = 4;
149              
150              
151             sub print_table {
152 0 0   0 1   if ( ref $_[0] ne __PACKAGE__ ) {
153 0           my $ob = __PACKAGE__->new();
154 0           delete $ob->{backup_instance_defaults};
155 0           return $ob->print_table( @_ );
156             }
157 0           my $self = shift;
158 0           my ( $tbl_orig, $opt ) = @_;
159 0 0 0       croak "print_table: called with " . @_ . " arguments - 1 or 2 arguments expected." if @_ < 1 || @_ > 2;
160 0 0         croak "print_table: requires an ARRAY reference as its first argument." if ref $tbl_orig ne 'ARRAY';
161 0 0         if ( defined $opt ) {
162 0 0         croak "print_table: the (optional) second argument is not a HASH reference." if ref $opt ne 'HASH';
163              
164             #####
165             #choose( [ 'Continue with ENTER' ], { prompt => "The 'min_col_width' option has been renamed to 'col_trim_threshold'." } ) if exists $opt->{min_col_width}; # 21.02.2026
166             #choose( [ 'Continue with ENTER' ], { prompt => "The 'max_width_exp' option has been renamed to 'expanded_max_width'." } ) if exists $opt->{max_width_exp}; # 21.02.2026
167             #####
168              
169 0           validate_options( _valid_options(), $opt, 'print_table' );
170 0           for my $key ( keys %$opt ) {
171 0 0         $self->{$key} = $opt->{$key} if defined $opt->{$key};
172             }
173             }
174 0           local $| = 1;
175             local $SIG{INT} = sub {
176 0     0     $self->__reset();
177 0           print "\n";
178 0           exit;
179 0           };
180 0 0         if ( print_columns( $self->{decimal_separator} ) != 1 ) {
181 0           $self->{decimal_separator} = '.';
182             }
183 0 0         if ( $self->{decimal_separator} ne '.' ) {
184 0           $self->{thsd_sep} = '_';
185             }
186 0 0         if ( $self->{hide_cursor} ) {
187 0           print hide_cursor();
188             # 'choose' functions: Deactivate 'hide_cursor', because if 'hide_cursor' is
189             # activated (default), 'choose' activates the cursor before returning.
190             }
191 0 0 0       if ( ! @$tbl_orig || ! @{$tbl_orig->[0]} ) {
  0            
192 0           my $message;
193 0 0         if ( ! @$tbl_orig ) {
194 0           $message = "'print_table': Empty table without header row!";
195             }
196             else {
197 0           $message = "'print_table': No columns!";
198             }
199             # Choose
200 0           choose(
201             [ 'Close with ENTER' ],
202             { prompt => $message, hide_cursor => 0 }
203             );
204 0           $self->__reset();
205 0           return;
206             }
207 0           $self->{_last_index} = $#$tbl_orig; ##
208 0 0 0       if ( $self->{max_rows} && $self->{_last_index} > $self->{max_rows} ) {
209 0           $self->{_info_row} = sprintf( 'Limited to %s rows', insert_sep( $self->{max_rows}, $self->{thsd_sep} ) );
210 0           $self->{_info_row} .= sprintf( ' (total %s)', insert_sep( $self->{_last_index}, $self->{thsd_sep} ) );
211 0 0         $self->{_info_row} = ' ' . $self->{_info_row} if $self->{pad_row_edges};
212 0           $self->{_last_index} = $self->{max_rows};
213             }
214 0           $self->{_search_regex} = '';
215 0           $self->{_idx_search_matches} = [];
216 0           my $width = {};
217 0 0         $width->{edge} = $self->{pad_row_edges} ? 1 : 0;
218 0           my ( $tbl_print, $header_rows ) = $self->__get_data( $tbl_orig, $width );
219 0 0         if ( ! defined $tbl_print ) {
220 0           $self->__reset();
221 0           return;
222             }
223              
224 0           WRITE_TABLE: while ( 1 ) {
225 0           my $next = $self->__write_table( $tbl_orig, $tbl_print, $header_rows, $width );
226 0 0 0       if ( ! defined $next ) {
    0          
    0          
    0          
    0          
227 0           die;
228             }
229             elsif ( $next == $last_write_table ) {
230 0           last WRITE_TABLE;
231             }
232             elsif ( $next == $window_width_changed || $next == $choose_and_print ) {
233 0           ( $tbl_print, $header_rows ) = $self->__get_data( $tbl_orig, $width, $next );
234 0 0         if ( ! defined $tbl_print ) {
235 0           $self->__reset();
236 0           return;
237             }
238 0           next WRITE_TABLE;
239             }
240             elsif ( $next == $enter_search_string ) {
241 0           $self->__search( $tbl_orig );
242 0           next WRITE_TABLE;
243             }
244             elsif ( $next == $from_filtered_table ) {
245 0           $self->__reset_search();
246 0           next WRITE_TABLE;
247             }
248             }
249 0           $self->__reset();
250 0           return 1;
251             }
252              
253              
254             sub __used_columns {
255 0     0     my ( $self, $tbl_orig, $width, $next ) = @_;
256 0           my $tcu = Term::Choose::Util->new(
257             { index => 1, all_by_default => 1, keep_chosen => 1, cs_begin => "\n", confirm => '-OK-', back => ' << ' } # pad_row_edges
258             );
259 0           my $cols;
260 0 0 0       if ( defined $next && $next == $window_width_changed ) {
    0          
261 0           $cols = [ @{$self->{_desired_cols_tbl_orig}} ];
  0            
262             }
263             elsif ( $self->{choose_columns} == 1 ) {
264             # Choose
265 0           $cols = $tcu->choose_a_subset( $tbl_orig->[0], { cs_label => 'Chosen columns:' } );
266 0 0         if ( ! defined $cols ) {
267 0           return;
268             }
269             }
270             else {
271 0           $cols = [ 0 .. $#{$tbl_orig->[0]} ];
  0            
272             }
273 0 0         my $min_col_w_th = $self->{choose_columns} ? 3 : 2; # min_col_width_treshold
274 0           my $desired_cols;
275              
276 0           while ( ( $width->{tab} + $min_col_w_th ) * $#$cols + $min_col_w_th + $width->{edge} * 2 > $width->{term} ) {
277 0           $width->{tab} -= 2;
278 0 0         if ( $width->{tab} < 1 ) {
279 0 0         if ( $self->{choose_columns} ) {
280 0 0         if ( ! $desired_cols ) {
281 0           $desired_cols = [ @$cols ];
282             }
283 0           $width->{tab} = $self->{tab_width};
284 0 0         if ( ! ( $self->{tab_width} % 2 ) ) {
285 0           ++$width->{tab};
286             }
287 0           my $avail_w = $width->{term} - ( $width->{edge} * 2 + $min_col_w_th );
288 0           my $max_cols = 1 + int( $avail_w / ( 1 + $min_col_w_th ) );
289 0           my $cs_label = "Current window width only supports $max_cols columns.";
290 0           $cs_label .= "\nPlease reduce your selection or widen the terminal:";
291             # Choose
292 0           my $new_cols = $tcu->choose_a_subset( [ @{$tbl_orig->[0]}[@$cols] ], { cs_label => $cs_label } );
  0            
293 0 0         if ( ! defined $new_cols ) {
294 0           return;
295             }
296 0           $width->{term} = get_term_width() + EXTRA_W;
297 0           $cols = [ @{$cols}[@$new_cols] ];
  0            
298             }
299             else {
300 0           my $info = 'Too many columns; the terminal window is not wide enough.';
301 0           my $prompt = 'Close with ENTER.';
302             # Choose
303             choose(
304             $tbl_orig->[0],
305             # [ @{$tbl_orig->[0]}[@$cols] ],
306             { info => $info, prompt => $prompt, clear_screen => 1, mouse => $self->{mouse}, hide_cursor => 0,
307             search => $self->{search} }
308 0           );
309 0           return;
310             }
311             }
312             }
313 0 0         if ( ! $desired_cols ) {
314 0           $desired_cols = $cols;
315             }
316 0           return $desired_cols, $cols;
317             }
318              
319              
320              
321             sub __get_data {
322 0     0     my ( $self, $tbl_orig, $width, $next ) = @_;
323 0           $width->{term}= get_term_width() + EXTRA_W;
324 0           $width->{tab} = $self->{tab_width};
325 0 0         if ( ! ( $self->{tab_width} % 2 ) ) {
326 0           ++$width->{tab}; # include the `|`
327             }
328 0           my ( $desired_cols, $possible_cols ) = $self->__used_columns( $tbl_orig, $width, $next );
329 0 0         if ( ! defined $desired_cols ) {
330 0           return;
331             }
332 0           $self->{_used_cols_tbl_orig} = $possible_cols;
333 0           $self->{_desired_cols_tbl_orig} = $desired_cols;
334 0           my $items_count = $self->{_last_index} * @{$tbl_orig->[0]}; ##
  0            
335             my $progress = Term::TablePrint::ProgressBar->new( {
336             total => $self->{_last_index} * 3 + 2, # + 2: 2 out of 3 loops include the header.
337 0           show_progress_bar => $self->{progress_bar} < $items_count,
338             } );
339 0           my $tbl_copy = $self->__copy_table( $tbl_orig, $progress );
340 0           $self->__calc_col_width( $tbl_copy, $width, $progress );
341 0           $self->__calc_avail_col_width( $tbl_copy, $width );
342 0           $width->{table} = sum( @{$width->{cols_calc}}, $width->{tab} * $#{$width->{cols_calc}}, 2 * $width->{edge} );
  0            
  0            
343 0           my $tbl_print = $self->__cols_to_string( $tbl_orig, $tbl_copy, $width, $progress );
344 0           my @tmp_header_rows;
345 0 0         if ( length $self->{prompt} ) {
346 0           push @tmp_header_rows, $self->{prompt};
347             }
348 0 0 0       if ( length $self->{info} || length $self->{prompt} ) {
349 0           push @tmp_header_rows, $self->__header_sep( $width );
350             }
351 0           my $col_names = shift @{$tbl_print};
  0            
352 0           push @tmp_header_rows, $col_names, $self->__header_sep( $width );
353 0           my $header_rows = join "\n", @tmp_header_rows;
354 0 0         if ( $self->{_info_row} ) {
355 0 0         if ( print_columns( $self->{_info_row} ) > $width->{table} ) {
356 0           push @{$tbl_print}, cut_to_printwidth( $self->{_info_row}, $width->{table} - 3 ) . '...';
  0            
357             }
358             else {
359 0           push @{$tbl_print}, $self->{_info_row};
  0            
360             }
361             }
362 0           return $tbl_print, $header_rows;
363             }
364              
365              
366             sub __write_table {
367 0     0     my ( $self, $tbl_orig, $tbl_print, $header_rows, $width ) = @_;
368 0           my @idxs_tbl_print;
369             my $return;
370 0 0         if ( $self->{_search_regex} ) {
    0          
371 0           @idxs_tbl_print = map { $_ - 1 } @{$self->{_idx_search_matches}}; # because of the removed tbl_print header
  0            
  0            
372 0           $return = $from_filtered_table;
373             }
374             elsif ( $self->{choose_columns} == 1 ) {
375 0           $return = $choose_and_print;
376             }
377             else {
378 0           $return = $last_write_table;
379             }
380 0           my $footer;
381 0 0         if ( $self->{footer} ) {
382 0           $footer = $self->{footer};
383 0 0         if ( $self->{_search_regex} ) {
384 0           $footer .= "[$self->{_search_regex}]";
385             }
386             }
387 0 0 0       my $old_row = exists $ENV{TC_POS_AT_SEARCH} && ! $self->{_search_regex} ? delete( $ENV{TC_POS_AT_SEARCH} ) : 0;
388 0           my $auto_jumped_to_row_0 = 0;
389 0           my $row_was_expanded = 0;
390              
391 0           while ( 1 ) {
392 0 0         if ( $width->{term} != get_term_width() + EXTRA_W ) {
393 0           return $window_width_changed;
394             }
395 0 0         if ( ! @{$tbl_print} ) {
  0            
396 0           push @{$tbl_print}, ''; # so that going back requires always the same amount of keystrokes
  0            
397             }
398 0           $ENV{TC_RESET_AUTO_UP} = 0;
399             # Choose
400             my $row = choose(
401 0           @idxs_tbl_print ? [ @{$tbl_print}[@idxs_tbl_print] ]
402             : $tbl_print,
403             { info => $self->{info}, prompt => $header_rows, index => 1, default => $old_row, ll => $width->{table}, layout => 2,
404             clear_screen => 1, mouse => $self->{mouse}, hide_cursor => 0, footer => $footer, color => $self->{color},
405             codepage_mapping => $self->{codepage_mapping}, search => $self->{search}, keep => $self->{keep},
406             page => $self->{page} }
407 0 0         );
408 0 0         if ( ! defined $row ) {
    0          
409 0           return $return;
410             }
411             elsif ( $row < 0 ) {
412 0 0         if ( $row == -1 ) { # with option `ll` set and changed window width `choose` returns -1;
    0          
413 0           return $window_width_changed;
414             }
415             elsif ( $row == -13 ) { # with option `ll` set `choose` returns -13 if `Ctrl-F` was pressed
416 0 0         if ( $self->{_search_regex} ) {
417 0           $self->__reset_search();
418             }
419 0           return $enter_search_string;
420             }
421             else {
422 0           return $last_write_table;
423             }
424             }
425 0 0         if ( ! $self->{table_expand} ) {
426 0 0         return $return if $row == 0;
427 0           next;
428             }
429 0 0         if ( $ENV{TC_RESET_AUTO_UP} ) { # true if any key other than Return/Enter was pressed
430 0           $auto_jumped_to_row_0 = 0;
431 0           $row_was_expanded = 0;
432             }
433             #if ( $old_row == $row ) {
434 0 0         if ( $row_was_expanded ) {
435 0 0         if ( $row == 0 ) {
436 0           return $return;
437             }
438 0           $old_row = 0;
439 0           $auto_jumped_to_row_0 = 1;
440 0           $row_was_expanded = 0;
441 0           next;
442             }
443 0 0         if ( $auto_jumped_to_row_0 ) {
444 0           return $return;
445             }
446             #}
447 0           $old_row = $row;
448 0           $row_was_expanded = 1;
449 0 0 0       if ( $self->{_info_row} && $row == $#{$tbl_print} ) {
  0            
450             # Choose
451             choose(
452             [ 'Close' ],
453 0           { prompt => $self->{_info_row}, clear_screen => 1, mouse => $self->{mouse}, hide_cursor => 0 }
454             );
455 0           next;
456             }
457 0           my $orig_row;
458 0 0         if ( @{$self->{_idx_search_matches}} ) {
  0            
459 0           $orig_row = $self->{_idx_search_matches}[$row];
460             }
461             else {
462 0           $orig_row = $row + 1; # because $tbl_print has no header row while $tbl_orig has a header row
463             }
464 0           $self->__print_single_row( $tbl_orig, $orig_row, $width, $footer );
465 0           delete $ENV{TC_RESET_AUTO_UP};
466             }
467             }
468              
469              
470             sub __copy_table {
471 0     0     my ( $self, $tbl_orig, $progress ) = @_;
472 0           my $tbl_copy = [];
473 0           $progress->set_progress_bar();
474 0           my $str;
475              
476 0           ROW: for my $i ( 0 .. $self->{_last_index} ) {
477 0           my $tmp_row = [];
478              
479 0           COL: for my $j ( @{$self->{_used_cols_tbl_orig}} ) {
  0            
480 0   0       $str = $tbl_orig->[$i][$j] // $self->{undef}; # this is where the copying happens
481 0 0         $str = _handle_reference( $str ) if ref $str;
482 0 0         if ( $self->{color} ) {
483 0           $str =~ s/${\PH}//g;
  0            
484 0           $str =~ s/${\SGR_ES}/${\PH}/g;
  0            
  0            
485             }
486 0 0 0       if ( $self->{binary_filter} && substr( $str, 0, 100 ) =~ /[\x00-\x08\x0B-\x0C\x0E-\x1F]/ ) {
487 0 0         if ( $self->{binary_filter} == 2 ) {
488 0   0       push @$tmp_row, sprintf( '%*v02X', ' ', $tbl_orig->[$i][$j] // $self->{undef} );
489             }
490             else {
491 0           push @$tmp_row, $self->{binary_string};
492             }
493 0           next COL;
494             }
495 0 0         if ( $str =~ /[\p{Cc}\p{Noncharacter_Code_Point}\p{Cs}]/ ) {
496 0           $str =~ s/\t/ /g;
497 0           $str =~ s/\v+/\ \ /g;
498 0           $str =~ s/[\p{Cc}\p{Noncharacter_Code_Point}\p{Cs}]//g;
499             }
500 0 0         if ( $self->{squash_spaces} ) {
501 0           $str =~ s/^\p{Space}+//;
502 0           $str =~ s/\p{Space}+\z//;
503 0           $str =~ s/\p{Space}+/ /g;
504             }
505 0           push @$tmp_row, $str;
506             }
507 0           push @$tbl_copy, $tmp_row;
508 0 0         if ( $progress->{show_progress_bar} ) {
509 0 0         if ( ++$progress->{count} > $progress->{next_update} ) {
510 0           $progress->update_progress_bar();
511             }
512             }
513             }
514 0           return $tbl_copy
515             }
516              
517              
518             sub __calc_col_width {
519 0     0     my ( $self, $tbl_copy, $width, $progress ) = @_;
520 0           $progress->set_progress_bar(); #
521 0           my $ds = $self->{decimal_separator};
522 0           my $regex_int_fract = "^([^${ds}Ee]*)(\Q${ds}\E[0-9]+)?\\z";
523 0           my @col_idx = ( 0 .. $#{$tbl_copy->[0]} );
  0            
524 0           my $col_count = @col_idx;
525 0           $width->{col_names} = [];
526 0           $width->{cols} = [ ( 1 ) x $col_count ];
527 0           $width->{int} = [ ( 0 ) x $col_count ];
528 0           $width->{fract} = [ ( 0 ) x $col_count ];
529 0           for my $col ( @col_idx ) {
530 0           $width->{col_names}[$col] = print_columns( $tbl_copy->[0][$col] );
531             }
532              
533 0           for my $row ( 1 .. $#$tbl_copy ) {
534              
535 0           for my $col ( @col_idx ) {
536 0 0         if ( ! length $tbl_copy->[$row][$col] ) {
    0          
537             # nothing to do
538             }
539             elsif ( looks_like_number $tbl_copy->[$row][$col] ) {
540 0 0         if ( $tbl_copy->[$row][$col] =~ /$regex_int_fract/ ) {
541 0 0 0       if ( ( length $1 // 0 ) > $width->{int}[$col] ) {
542 0           $width->{int}[$col] = length $1;
543             }
544 0 0 0       if ( ( length $2 // 0 ) > $width->{fract}[$col] ) {
545 0           $width->{fract}[$col] = length $2;
546             }
547             }
548             else {
549             # scientific notation
550 0 0         if ( length $tbl_copy->[$row][$col] > $width->{cols}[$col] ) {
551 0           $width->{cols}[$col] = length $tbl_copy->[$row][$col];
552             }
553             }
554             }
555             else {
556 0           my $str_w = print_columns( $tbl_copy->[$row][$col] );
557 0 0         if ( $str_w > $width->{cols}[$col] ) {
558 0           $width->{cols}[$col] = $str_w;
559             }
560             }
561             }
562 0 0         if ( $progress->{show_progress_bar} ) {
563 0 0         if ( ++$progress->{count} > $progress->{next_update} ) {
564 0           $progress->update_progress_bar();
565             }
566             }
567             }
568              
569 0           for my $col ( @col_idx ) {
570 0 0         if ( $width->{int}[$col] + $width->{fract}[$col] > $width->{cols}[$col] ) {
571 0           $width->{cols}[$col] = $width->{int}[$col] + $width->{fract}[$col];
572             }
573             }
574 0           return;
575             }
576              
577              
578             sub __calc_avail_col_width {
579 0     0     my ( $self, $tbl_copy, $width ) = @_;
580 0           $width->{cols_calc} = [ @{$width->{cols}} ];
  0            
581 0           $width->{fract_calc} = [ @{$width->{fract}} ];
  0            
582 0           my $avail_w = $width->{term} - ( $width->{tab} * $#{$width->{cols_calc}} + 2 * $width->{edge} );
  0            
583 0           my $sum = sum( @{$width->{cols_calc}} );
  0            
584 0           my @col_idx = ( 0 .. $#{$width->{cols_calc}} );
  0            
585 0 0         if ( $sum < $avail_w ) {
    0          
586              
587 0           HEAD: while ( 1 ) {
588 0           my $prev_sum = $sum;
589 0           for my $col ( @col_idx ) {
590 0 0         if ( $width->{col_names}[$col] > $width->{cols_calc}[$col] ) {
591 0           ++$width->{cols_calc}[$col];
592 0           ++$sum;
593 0 0         if ( $sum == $avail_w ) {
594 0           last HEAD;
595             }
596             }
597             }
598 0 0         if ( $sum == $prev_sum ) {
599 0           last HEAD;
600             }
601             }
602             }
603             elsif ( $sum > $avail_w ) {
604 0 0         if ( $self->{trunc_fract_first} ) {
605              
606 0           TRUNC_FRACT: while ( 1 ) {
607 0           my $prev_sum = $sum;
608 0           for my $col ( @col_idx ) {
609 0 0 0       if ( $width->{fract_calc}[$col] && $width->{fract_calc}[$col] > 3 # 3 == 1 decimal separator + 2 decimal places
610             #&& $width->{int}[$col] + $width->{fract_calc}[$col] == $width->{cols_calc}[$col] #
611             ## the column width could be larger than width int + width fract_calc, if the column contains non-digit strings
612             ) {
613 0           --$width->{fract_calc}[$col];
614 0           --$width->{cols_calc}[$col];
615 0           --$sum;
616 0 0         if ( $sum == $avail_w ) {
617 0           last TRUNC_FRACT;
618             }
619             }
620             }
621 0 0         if ( $sum == $prev_sum ) {
622 0           last TRUNC_FRACT;
623             }
624             }
625             }
626 0 0         my $min_col_width = $self->{col_trim_threshold} < 2 ? 2 : $self->{col_trim_threshold};
627 0           my $percent = 4;
628              
629 0           TRUNC_COLS: while ( $sum > $avail_w ) {
630 0           ++$percent;
631              
632 0           for my $col ( @col_idx ) {
633 0 0         if ( $width->{cols_calc}[$col] > $min_col_width ) {
634 0           my $reduced_col_w = _minus_x_percent( $width->{cols_calc}[$col], $percent );
635 0 0         if ( $reduced_col_w < $min_col_width ) {
636 0           $reduced_col_w = $min_col_width;
637             }
638 0 0         if ( $width->{fract_calc}[$col] > 2 ) {
639 0           $width->{fract_calc}[$col] -= $width->{cols_calc}[$col] - $reduced_col_w;
640 0 0         if ( $width->{fract_calc}[$col] < 2 ) {
641 0           $width->{fract_calc}[$col] = 2;
642             }
643             }
644             #if ( $width->{fract_calc}[$col] > 0 ) {
645             # $width->{fract_calc}[$col] -= $width->{cols_calc}[$col] - $reduced_col_w;
646             # if ( $width->{fract_calc}[$col] < 1 ) {
647             # $width->{fract_calc}[$col] = "0 but true";
648             # # keep it true eaven if it is 0 for __cols_to_string to work properly.
649             # }
650             #}
651 0           $width->{cols_calc}[$col] = $reduced_col_w;
652             }
653             }
654 0           my $prev_sum = $sum;
655 0           $sum = sum( @{$width->{cols_calc}} );
  0            
656 0 0         if ( $sum == $prev_sum ) {
657 0           --$min_col_width;
658 0 0         if ( $min_col_width < 2 ) { # never
659 0           die "Value less than 1";
660             }
661             }
662             }
663 0           my $remainder_w = $avail_w - $sum;
664 0 0         if ( $remainder_w ) {
665              
666 0           REMAINDER_W: while ( 1 ) {
667 0           my $prev_remainder_w = $remainder_w;
668 0           for my $col ( @col_idx ) {
669 0 0         if ( $width->{cols_calc}[$col] < $width->{cols}[$col] ) {
670 0           ++$width->{cols_calc}[$col];
671 0 0 0       if ( $width->{fract_calc}[$col] < $width->{fract}[$col]
672             && $width->{int}[$col] + $width->{fract_calc}[$col] < $width->{cols_calc}[$col] ##
673             ) {
674 0           ++$width->{fract_calc}[$col]
675             }
676 0           --$remainder_w;
677 0 0         if ( $remainder_w == 0 ) {
678 0           last REMAINDER_W;
679             }
680             }
681             }
682 0 0         if ( $remainder_w == $prev_remainder_w ) {
683 0           last REMAINDER_W;
684             }
685             }
686             }
687             }
688             #else {
689             # #$sum == $avail_w, nothing to do
690             #}
691 0           return;
692             }
693              
694              
695             sub __cols_to_string {
696 0     0     my ( $self, $tbl_orig, $tbl_copy, $width, $progress ) = @_;
697 0           $progress->set_progress_bar();
698 0           my $tab = ( ' ' x int( $width->{tab} / 2 ) ) . '|' . ( ' ' x int( $width->{tab} / 2 ) );
699 0           my $one_precision_w = length sprintf "%.1e", 123;
700 0           my $ds = $self->{decimal_separator};
701 0           my $regex_fract = "(\Q${ds}\E[0-9]+)\\z";
702 0           my $lrb = ' ' x $width->{edge};
703 0           my $header = $lrb;
704              
705 0           HEADER: for my $col ( 0 .. $#{$width->{cols_calc}} ) {
  0            
706 0           $header .= adjust_to_printwidth( $tbl_copy->[0][$col], $width->{cols_calc}[$col] );
707 0 0         $header .= $col == $#{$width->{cols_calc}} ? $lrb : $tab;
  0            
708             }
709 0           $tbl_copy->[0] = $header; # overwrite $tbl_copy to save memory
710 0           my $str;
711              
712 0           ROW: for my $row ( 1 .. $#{$tbl_copy} ) {
  0            
713 0           $str = $lrb;
714              
715 0           COL: for my $col ( 0 .. $#{$width->{cols_calc}} ) {
  0            
716 0 0         if ( ! length $tbl_copy->[$row][$col] ) {
    0          
717 0           $str .= ' ' x $width->{cols_calc}[$col];
718             }
719             elsif ( looks_like_number $tbl_copy->[$row][$col] ) {
720 0 0         if ( $width->{fract_calc}[$col] ) {
721 0 0 0       if ( $tbl_copy->[$row][$col] =~ /$regex_fract/ ) {
    0          
    0          
722 0 0         if ( length $1 > $width->{fract_calc}[$col] ) {
    0          
723 0           $tbl_copy->[$row][$col] = substr( $tbl_copy->[$row][$col], 0, -( length( $1 ) - $width->{fract_calc}[$col] ) );
724             }
725             elsif ( length $1 < $width->{fract_calc}[$col] ) {
726 0           $tbl_copy->[$row][$col] .= ' ' x ( $width->{fract_calc}[$col] - length $1 );
727             }
728             }
729             elsif ( $tbl_copy->[$row][$col] !~ /[eE]/ ) {
730 0           $tbl_copy->[$row][$col] .= ' ' x $width->{fract_calc}[$col];
731             }
732             elsif ( $tbl_copy->[$row][$col] =~ /[eE]-/ && $width->{fract}[$col] != $width->{fract_calc}[$col] ) {
733 0           $tbl_copy->[$row][$col] = sprintf "%.*f", $width->{fract_calc}[$col] - 1, $tbl_copy->[$row][$col];
734             }
735             }
736 0 0         if ( length $tbl_copy->[$row][$col] > $width->{cols_calc}[$col] ) {
    0          
737 0 0         my $precision = $width->{cols_calc}[$col] - ( $one_precision_w + ( $tbl_copy->[$row][$col] < 0 ? 1 : 0 ) ) + 1;
738             # $one_precision_w + 1 if the number is signed (-)
739             # $precision + 1 because $one_precision_w contains already one precision
740 0 0         if ( $precision == -1 ) {
741 0           $precision = 0;
742             # Difference between one-precision-width and zero-precision-width is 2 because zero precision has no dot.
743             }
744 0 0         if ( $precision >= 0 ) {
745 0           while ( length( $tbl_copy->[$row][$col] = sprintf "%.*e", $precision, $tbl_copy->[$row][$col] ) > $width->{cols_calc}[$col] ) { ##
746 0           --$precision;
747 0 0         last if $precision < 0;
748             }
749              
750             }
751 0 0 0       if ( $precision < 0 ) {
    0          
752 0           $str .= ( '-' x $width->{cols_calc}[$col] );
753             }
754             elsif ( $precision == 0 && length $tbl_copy->[$row][$col] < $width->{cols_calc}[$col] ) {
755 0           $str .= ' ' . $tbl_copy->[$row][$col];
756             }
757             else {
758 0           $str .= $tbl_copy->[$row][$col];
759             }
760             }
761             elsif ( length $tbl_copy->[$row][$col] < $width->{cols_calc}[$col] ) {
762 0           $str .= ' ' x ( $width->{cols_calc}[$col] - length $tbl_copy->[$row][$col] ) . $tbl_copy->[$row][$col];
763             }
764             else {
765 0           $str .= $tbl_copy->[$row][$col];
766             }
767             }
768             else {
769 0           $str .= adjust_to_printwidth( $tbl_copy->[$row][$col], $width->{cols_calc}[$col] );
770             }
771 0 0         if ( $self->{color} ) {
772 0           my $orig_col = $self->{_used_cols_tbl_orig}[$col];
773 0 0         if ( defined $tbl_orig->[$row][$orig_col] ) {
774 0           my @color = $tbl_orig->[$row][$orig_col] =~ /(${\SGR_ES})/g;
  0            
775 0 0         if ( @color ) {
776 0           $str =~ s/${\PH}/shift @color/ge;
  0            
  0            
777 0           $str .= "\e[0m";
778             }
779             #if ( @color ) {
780             # if ( $color[-1] !~ /^\e\[0?m/ ) {
781             # push @color, "\e[0m";
782             # }
783             # $str =~ s/${\PH}/shift @color/ge;
784             # if ( @color ) {
785             # $str .= $color[-1];
786             # }
787             #}
788             }
789             }
790 0 0         $str .= $col == $#{$width->{cols_calc}} ? $lrb : $tab;
  0            
791             }
792 0           $tbl_copy->[$row] = $str; # overwrite $tbl_copy to save memory
793 0 0         if ( $progress->{show_progress_bar} ) {
794 0 0         if ( ++$progress->{count} > $progress->{next_update} ) {
795 0           $progress->update_progress_bar();
796             }
797             }
798             }
799 0 0         if ( $progress->{show_progress_bar} ) {
800 0           $progress->update_progress_bar();
801             }
802 0           return $tbl_copy; # $tbl_copy is now $tbl_print
803             }
804              
805              
806             sub __print_single_row {
807 0     0     my ( $self, $tbl_orig, $row, $width, $footer ) = @_;
808 0           my $avail_w = get_term_width() - 1; ##
809 0 0 0       if ( $self->{expanded_max_width} && $self->{expanded_max_width} < $avail_w ) {
810 0           $avail_w = $self->{expanded_max_width};
811             }
812 0           my $max_key_w = max( @{$width->{col_names}} ) + 1;
  0            
813 0 0         if ( $max_key_w > int( $avail_w / 3 ) ) {
814 0           $max_key_w = int( $avail_w / 3 );
815             }
816 0           my $separator = ' : ';
817 0           my $sep_w = length( $separator );
818 0           my $max_value_w = $avail_w - ( $max_key_w + $sep_w );
819 0           my $separator_row = ' ';
820 0           my $row_data = [ ' Close with ENTER' ];
821              
822 0           for my $col ( @{$self->{_desired_cols_tbl_orig}} ) {
  0            
823 0 0         if ( $self->{expanded_line_spacing} ) {
824 0           push @$row_data, $separator_row;
825             }
826 0   0       my $key = $tbl_orig->[0][$col] // $self->{undef};
827 0           my @key_color;
828 0 0         if ( $self->{color} ) {
829 0           $key =~ s/${\PH}//g;
  0            
830 0 0         $key =~ s/(${\SGR_ES})/push( @key_color, $1 ) && ${\PH}/ge;
  0            
  0            
  0            
831             }
832 0 0 0       if ( $self->{binary_filter} && substr( $key, 0, 100 ) =~ /[\x00-\x08\x0B-\x0C\x0E-\x1F]/ ) {
    0          
833 0 0         if ( $self->{binary_filter} == 2 ) {
834 0   0       $key = sprintf( '%*v02X', ' ', $tbl_orig->[0][$col] // $self->{undef} );
835             }
836             else {
837 0           $key = $self->{binary_string};
838             }
839 0 0         if ( @key_color ) {
840 0           @key_color = ();
841             }
842             }
843             elsif ( $key =~ /[\p{Cc}\p{Noncharacter_Code_Point}\p{Cs}]/ ) {
844 0           $key =~ s/\t/ /g;
845 0           $key =~ s/\v+/\ \ /g;
846 0           $key =~ s/[\p{Cc}\p{Noncharacter_Code_Point}\p{Cs}]//g;
847             }
848 0           my $key_w = print_columns( $key );
849 0 0         if ( $key_w > $max_key_w ) {
    0          
850 0           $key = cut_to_printwidth( $key, $max_key_w );
851             }
852             elsif ( $key_w < $max_key_w ) {
853 0           $key = ( ' ' x ( $max_key_w - $key_w ) ) . $key;
854             }
855 0 0         if ( @key_color ) {
856 0           $key =~ s/${\PH}/shift @key_color/ge;
  0            
  0            
857 0           $key .= "\e[0m";
858             }
859 0   0       my $value = $tbl_orig->[$row][$col] // $self->{undef};
860             # $value: color and invalid char handling in `line_fold`
861 0 0         if ( ref $value ) {
862 0           $value = _handle_reference( $value );
863             }
864 0           my $subseq_tab = ' ' x ( $max_key_w + $sep_w );
865 0           my $count;
866              
867 0           for my $line ( line_fold( $value, { width => $max_value_w, color => $self->{color}, binary_filter => $self->{binary_filter}, join => 0 } ) ) {
868 0 0         if ( ! $count++ ) {
869 0           push @$row_data, $key . $separator . $line;
870             }
871             else {
872 0           push @$row_data, $subseq_tab . $line;
873             }
874             }
875             }
876 0           my $regex;
877 0 0         if ( $self->{expanded_line_spacing} ) {
878 0           $regex = qr/^\Q$separator_row\E\z/;
879             }
880             # Choose
881             choose(
882             $row_data,
883             { prompt => '', layout => 2, clear_screen => 1, mouse => $self->{mouse}, hide_cursor => 0, empty => ' ',
884             search => $self->{search}, skip_items => $regex, footer => $footer, page => $self->{page},
885             color => $self->{color} }
886 0           );
887             }
888              
889              
890             sub __search {
891 0     0     my ( $self, $tbl_orig ) = @_;
892 0 0         if ( ! $self->{search} ) {
893 0           return;
894             }
895 0           require Term::Form::ReadLine;
896 0           Term::Form::ReadLine->VERSION(0.544);
897 0           my $term = Term::Form::ReadLine->new();
898 0           my $error_message;
899 0           my $prompt = "> \e[4msearch\e[0m: ";
900 0           my $default = '';
901              
902 0           READ: while ( 1 ) {
903 0 0         my $string = $term->readline(
904             $prompt,
905             { info => $error_message, hide_cursor => 2, clear_screen => defined $error_message ? 1 : 2,
906             default => $default, color => 1 }
907             );
908 0 0         if ( ! length $string ) {
909 0           return;
910             }
911 0           print "\r${prompt}${string}";
912 0 0         if ( ! eval {
913 0 0         $self->{_search_regex} = $self->{search} == 1 ? "(?i:$string)" : $string;
914 0           'Teststring' =~ $self->{_search_regex};
915 0           1
916             } ) {
917 0 0         $default = $default eq $string ? '' : $string;
918 0           $error_message = "$@";
919 0           next READ;
920             }
921 0           last READ;
922             }
923 1     1   9 no warnings 'uninitialized';
  1         2  
  1         864  
924              
925             # skip the header row
926 0           for my $idx_row ( 1 .. $self->{_last_index} ) {
927 0           for ( @{$self->{_desired_cols_tbl_orig}} ) {
  0            
928 0 0         if ( $tbl_orig->[$idx_row][$_] =~ /$self->{_search_regex}/ ) {
929 0           push @{$self->{_idx_search_matches}}, $idx_row;
  0            
930 0           last;
931             }
932             }
933             }
934 0 0         if ( ! @{$self->{_idx_search_matches}} ) {
  0            
935 0           my $message = '/' . $self->{_search_regex} . '/: No matches found.';
936             # Choose
937 0           choose(
938             [ 'Continue with ENTER' ],
939             { prompt => $message, layout => 0, clear_screen => 1, hide_cursor => 0 }
940             );
941 0           $self->{_search_regex} = '';
942 0           return;
943             }
944 0           return;
945             }
946              
947              
948             sub __reset_search {
949 0     0     my ( $self ) = @_;
950 0           $self->{_idx_search_matches} = [];
951 0           $self->{_search_regex} = '';
952             }
953              
954              
955             sub __header_sep {
956 0     0     my ( $self, $width ) = @_;
957 0           my $tab = ( '-' x int( $width->{tab} / 2 ) ) . '|' . ( '-' x int( $width->{tab} / 2 ) );
958 0           my $lrb = '-' x $width->{edge};
959 0           my $header_sep = $lrb;
960 0           for my $col ( 0 .. $#{$width->{cols_calc}} - 1 ) {
  0            
961 0           $header_sep .= '-' x $width->{cols_calc}[$col] . $tab;
962             }
963 0           $header_sep .= '-' x $width->{cols_calc}[$#{$width->{cols_calc}}] . $lrb;
  0            
964 0           return $header_sep;
965             }
966              
967              
968             sub _handle_reference {
969 0     0     require Data::Dumper;
970 0           local $Data::Dumper::Useqq = 1;
971 0           local $Data::Dumper::Indent = 0;
972 0           local $Data::Dumper::Terse = 1;
973 0           local $Data::Dumper::Maxdepth = 2;
974 0           return Data::Dumper::Dumper( $_[0] );
975             }
976              
977              
978             sub _minus_x_percent {
979             #my ( $value, $percent ) = @_;
980 0   0 0     return int( $_[0] - ( $_[0] / 100 * $_[1] ) ) || 1;
981             }
982              
983              
984              
985             1;
986              
987             __END__