File Coverage

blib/lib/Term/TablePrint.pm
Criterion Covered Total %
statement 45 596 7.5
branch 1 312 0.3
condition 0 69 0.0
subroutine 16 35 45.7
pod 2 2 100.0
total 64 1014 6.3


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