File Coverage

blib/lib/Term/TablePrint.pm
Criterion Covered Total %
statement 45 578 7.7
branch 1 302 0.3
condition 0 66 0.0
subroutine 16 35 45.7
pod 2 2 100.0
total 64 983 6.5


line stmt bran cond sub pod time code
1             package Term::TablePrint;
2              
3 1     1   91614 use warnings;
  1         3  
  1         93  
4 1     1   16 use strict;
  1         2  
  1         29  
5 1     1   15 use 5.16.0;
  1         4  
6              
7             our $VERSION = '0.179';
8 1     1   7 use Exporter 'import';
  1         4  
  1         68  
9             our @EXPORT_OK = qw( print_table );
10              
11 1     1   8 use Carp qw( croak );
  1         2  
  1         73  
12              
13 1     1   34 use List::Util qw( sum max );
  1         3  
  1         113  
14 1     1   7 use Scalar::Util qw( looks_like_number );
  1         2  
  1         56  
15              
16 1     1   905 use Term::Choose qw( choose );
  1         109368  
  1         123  
17 1     1   13 use Term::Choose::Constants qw( EXTRA_W PH SGR_ES );
  1         6  
  1         70  
18 1     1   5 use Term::Choose::LineFold qw( print_columns cut_to_printwidth adjust_to_printwidth line_fold );
  1         1  
  1         46  
19 1     1   3 use Term::Choose::Screen qw( hide_cursor show_cursor );
  1         2  
  1         30  
20 1     1   3 use Term::Choose::ValidateOptions qw( validate_options );
  1         2  
  1         31  
21 1     1   613 use Term::Choose::Util qw( get_term_width insert_sep );
  1         35225  
  1         72  
22 1     1   1924 use Term::TablePrint::ProgressBar qw();
  1         2  
  1         46  
23              
24              
25             BEGIN {
26 1 50   1   8337 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       ( $str = sprintf( "%v02X", $tbl_orig->[$i][$j] // $self->{undef} ) ) =~ tr/./ /;
489 0           push @$tmp_row, $str;
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           --$remainder_w;
673 0 0         if ( $remainder_w == 0 ) {
674 0           last REMAINDER_W;
675             }
676             }
677             }
678 0 0         if ( $remainder_w == $prev_remainder_w ) {
679 0           last REMAINDER_W;
680             }
681             }
682             }
683             }
684             #else {
685             # #$sum == $avail_w, nothing to do
686             #}
687 0           return;
688             }
689              
690              
691             sub __cols_to_string {
692 0     0     my ( $self, $tbl_orig, $tbl_copy, $width, $progress ) = @_;
693 0           $progress->set_progress_bar();
694 0           my $tab = ( ' ' x int( $width->{tab} / 2 ) ) . '|' . ( ' ' x int( $width->{tab} / 2 ) );
695 0           my $one_precision_w = length sprintf "%.1e", 123;
696 0           my $ds = $self->{decimal_separator};
697 0           my $regex_fract = "(\Q${ds}\E[0-9]+)\\z";
698 0           my $lrb = ' ' x $width->{edge};
699 0           my $str;
700              
701 0           ROW: for my $row ( 0 .. $#{$tbl_copy} ) {
  0            
702 0           $str = $lrb;
703              
704 0           COL: for my $col ( 0 .. $#{$width->{cols_calc}} ) {
  0            
705 0 0         if ( ! length $tbl_copy->[$row][$col] ) {
    0          
706 0           $str .= ' ' x $width->{cols_calc}[$col];
707             }
708             elsif ( looks_like_number $tbl_copy->[$row][$col] ) {
709 0 0         if ( $width->{fract_calc}[$col] ) {
710 0 0 0       if ( $tbl_copy->[$row][$col] =~ /$regex_fract/ ) {
    0          
    0          
711 0 0         if ( length $1 > $width->{fract_calc}[$col] ) {
    0          
712 0           $tbl_copy->[$row][$col] = substr( $tbl_copy->[$row][$col], 0, -( length( $1 ) - $width->{fract_calc}[$col] ) );
713             }
714             elsif ( length $1 < $width->{fract_calc}[$col] ) {
715 0           $tbl_copy->[$row][$col] .= ' ' x ( $width->{fract_calc}[$col] - length $1 );
716             }
717             }
718             elsif ( $tbl_copy->[$row][$col] !~ /[eE]/ ) {
719 0           $tbl_copy->[$row][$col] .= ' ' x $width->{fract_calc}[$col];
720             }
721             elsif ( $tbl_copy->[$row][$col] =~ /[eE]-/ && $width->{fract}[$col] != $width->{fract_calc}[$col] ) {
722 0           $tbl_copy->[$row][$col] = sprintf "%.*f", $width->{fract_calc}[$col] - 1, $tbl_copy->[$row][$col];
723             }
724             }
725 0 0         if ( length $tbl_copy->[$row][$col] > $width->{cols_calc}[$col] ) {
    0          
726 0 0         my $precision = $width->{cols_calc}[$col] - ( $one_precision_w + ( $tbl_copy->[$row][$col] < 0 ? 1 : 0 ) ) + 1;
727             # $one_precision_w + 1 if the number is signed (-)
728             # $precision + 1 because $one_precision_w contains already one precision
729 0 0         if ( $precision == -1 ) {
730 0           $precision = 0;
731             # Difference between one-precision-width and zero-precision-width is 2 because zero precision has no dot.
732             }
733 0 0         if ( $precision >= 0 ) {
734 0           while ( length( $tbl_copy->[$row][$col] = sprintf "%.*e", $precision, $tbl_copy->[$row][$col] ) > $width->{cols_calc}[$col] ) { ##
735 0           --$precision;
736 0 0         last if $precision < 0;
737             }
738              
739             }
740 0 0 0       if ( $precision < 0 ) {
    0          
741 0           $str .= ( '-' x $width->{cols_calc}[$col] );
742             }
743             elsif ( $precision == 0 && length $tbl_copy->[$row][$col] < $width->{cols_calc}[$col] ) {
744 0           $str .= ' ' . $tbl_copy->[$row][$col];
745             }
746             else {
747 0           $str .= $tbl_copy->[$row][$col];
748             }
749             }
750             elsif ( length $tbl_copy->[$row][$col] < $width->{cols_calc}[$col] ) {
751 0           $str .= ' ' x ( $width->{cols_calc}[$col] - length $tbl_copy->[$row][$col] ) . $tbl_copy->[$row][$col];
752             }
753             else {
754 0           $str .= $tbl_copy->[$row][$col];
755             }
756             }
757             else {
758 0           $str .= adjust_to_printwidth( $tbl_copy->[$row][$col], $width->{cols_calc}[$col] );
759             }
760 0 0         if ( $self->{color} ) {
761 0           my $orig_col = $self->{_used_cols_tbl_orig}[$col];
762 0 0         if ( defined $tbl_orig->[$row][$orig_col] ) {
763 0           my @color = $tbl_orig->[$row][$orig_col] =~ /(${\SGR_ES})/g;
  0            
764 0 0         if ( @color ) {
765 0           $str =~ s/${\PH}/shift @color/ge;
  0            
  0            
766 0           $str .= "\e[0m";
767             }
768             #if ( @color ) {
769             # if ( $color[-1] !~ /^\e\[0?m/ ) {
770             # push @color, "\e[0m";
771             # }
772             # $str =~ s/${\PH}/shift @color/ge;
773             # if ( @color ) {
774             # $str .= $color[-1];
775             # }
776             #}
777             }
778             }
779 0 0         $str .= $col == $#{$width->{cols_calc}} ? $lrb : $tab;
  0            
780             }
781 0           $tbl_copy->[$row] = $str; # overwrite $tbl_copy to save memory
782 0 0         if ( $progress->{show_progress_bar} ) {
783 0 0         if ( ++$progress->{count} > $progress->{next_update} ) {
784 0           $progress->update_progress_bar();
785             }
786             }
787             }
788 0 0         if ( $progress->{show_progress_bar} ) {
789 0           $progress->update_progress_bar();
790             }
791 0           return $tbl_copy; # $tbl_copy is now $tbl_print
792             }
793              
794              
795             sub __print_single_row {
796 0     0     my ( $self, $tbl_orig, $row, $width, $footer ) = @_;
797 0           my $avail_w = get_term_width() - 1; ##
798 0 0 0       if ( $self->{expanded_max_width} && $self->{expanded_max_width} < $avail_w ) {
799 0           $avail_w = $self->{expanded_max_width};
800             }
801 0           my $max_key_w = max( @{$width->{col_names}} ) + 1;
  0            
802 0 0         if ( $max_key_w > int( $avail_w / 3 ) ) {
803 0           $max_key_w = int( $avail_w / 3 );
804             }
805 0           my $separator = ' : ';
806 0           my $sep_w = length( $separator );
807 0           my $max_value_w = $avail_w - ( $max_key_w + $sep_w );
808 0           my $separator_row = ' ';
809 0           my $row_data = [ ' Close with ENTER' ];
810              
811 0           for my $col ( @{$self->{_desired_cols_tbl_orig}} ) {
  0            
812 0 0         if ( $self->{expanded_line_spacing} ) {
813 0           push @$row_data, $separator_row;
814             }
815 0   0       my $key = $tbl_orig->[0][$col] // $self->{undef};
816 0           my @key_color;
817 0 0         if ( $self->{color} ) {
818 0           $key =~ s/${\PH}//g;
  0            
819 0 0         $key =~ s/(${\SGR_ES})/push( @key_color, $1 ) && ${\PH}/ge;
  0            
  0            
  0            
820             }
821 0 0 0       if ( $self->{binary_filter} && substr( $key, 0, 100 ) =~ /[\x00-\x08\x0B-\x0C\x0E-\x1F]/ ) {
    0          
822 0 0         if ( $self->{binary_filter} == 2 ) {
823 0   0       ( $key = sprintf("%v02X", $tbl_orig->[0][$col] // $self->{undef} ) ) =~ tr/./ /;
824             }
825             else {
826 0           $key = $self->{binary_string};
827             }
828 0 0         if ( @key_color ) {
829 0           @key_color = ();
830             }
831             }
832             elsif ( $key =~ /[\p{Cc}\p{Noncharacter_Code_Point}\p{Cs}]/ ) {
833 0           $key =~ s/\t/ /g;
834 0           $key =~ s/\v+/\ \ /g;
835 0           $key =~ s/[\p{Cc}\p{Noncharacter_Code_Point}\p{Cs}]//g;
836             }
837 0           my $key_w = print_columns( $key );
838 0 0         if ( $key_w > $max_key_w ) {
    0          
839 0           $key = cut_to_printwidth( $key, $max_key_w );
840             }
841             elsif ( $key_w < $max_key_w ) {
842 0           $key = ( ' ' x ( $max_key_w - $key_w ) ) . $key;
843             }
844 0 0         if ( @key_color ) {
845 0           $key =~ s/${\PH}/shift @key_color/ge;
  0            
  0            
846 0           $key .= "\e[0m";
847             }
848 0   0       my $value = $tbl_orig->[$row][$col] // $self->{undef};
849             # $value: color and invalid char handling in `line_fold`
850 0 0         if ( ref $value ) {
851 0           $value = _handle_reference( $value );
852             }
853 0           my $subseq_tab = ' ' x ( $max_key_w + $sep_w );
854 0           my $count;
855              
856 0           for my $line ( line_fold( $value, { width => $max_value_w, color => $self->{color}, binary_filter => $self->{binary_filter}, join => 0 } ) ) {
857 0 0         if ( ! $count++ ) {
858 0           push @$row_data, $key . $separator . $line;
859             }
860             else {
861 0           push @$row_data, $subseq_tab . $line;
862             }
863             }
864             }
865 0           my $regex;
866 0 0         if ( $self->{expanded_line_spacing} ) {
867 0           $regex = qr/^\Q$separator_row\E\z/;
868             }
869             # Choose
870             choose(
871             $row_data,
872             { prompt => '', layout => 2, clear_screen => 1, mouse => $self->{mouse}, hide_cursor => 0, empty => ' ',
873             search => $self->{search}, skip_items => $regex, footer => $footer, page => $self->{page},
874             color => $self->{color} }
875 0           );
876             }
877              
878              
879             sub __search {
880 0     0     my ( $self, $tbl_orig ) = @_;
881 0 0         if ( ! $self->{search} ) {
882 0           return;
883             }
884 0           require Term::Form::ReadLine;
885 0           Term::Form::ReadLine->VERSION(0.544);
886 0           my $term = Term::Form::ReadLine->new();
887 0           my $error_message;
888 0           my $prompt = "> \e[4msearch\e[0m: ";
889 0           my $default = '';
890              
891 0           READ: while ( 1 ) {
892 0 0         my $string = $term->readline(
893             $prompt,
894             { info => $error_message, hide_cursor => 2, clear_screen => defined $error_message ? 1 : 2,
895             default => $default, color => 1 }
896             );
897 0 0         if ( ! length $string ) {
898 0           return;
899             }
900 0           print "\r${prompt}${string}";
901 0 0         if ( ! eval {
902 0 0         $self->{_search_regex} = $self->{search} == 1 ? "(?i:$string)" : $string;
903 0           'Teststring' =~ $self->{_search_regex};
904 0           1
905             } ) {
906 0 0         $default = $default eq $string ? '' : $string;
907 0           $error_message = "$@";
908 0           next READ;
909             }
910 0           last READ;
911             }
912 1     1   12 no warnings 'uninitialized';
  1         2  
  1         748  
913              
914             # skip the header row
915 0           for my $idx_row ( 1 .. $self->{_last_index} ) {
916 0           for ( @{$self->{_desired_cols_tbl_orig}} ) {
  0            
917 0 0         if ( $tbl_orig->[$idx_row][$_] =~ /$self->{_search_regex}/ ) {
918 0           push @{$self->{_idx_search_matches}}, $idx_row;
  0            
919 0           last;
920             }
921             }
922             }
923 0 0         if ( ! @{$self->{_idx_search_matches}} ) {
  0            
924 0           my $message = '/' . $self->{_search_regex} . '/: No matches found.';
925             # Choose
926 0           choose(
927             [ 'Continue with ENTER' ],
928             { prompt => $message, layout => 0, clear_screen => 1, hide_cursor => 0 }
929             );
930 0           $self->{_search_regex} = '';
931 0           return;
932             }
933 0           return;
934             }
935              
936              
937             sub __reset_search {
938 0     0     my ( $self ) = @_;
939 0           $self->{_idx_search_matches} = [];
940 0           $self->{_search_regex} = '';
941             }
942              
943              
944             sub __header_sep {
945 0     0     my ( $self, $width ) = @_;
946 0           my $tab = ( '-' x int( $width->{tab} / 2 ) ) . '|' . ( '-' x int( $width->{tab} / 2 ) );
947 0           my $lrb = '-' x $width->{edge};
948 0           my $header_sep = $lrb;
949 0           for my $col ( 0 .. $#{$width->{cols_calc}} - 1 ) {
  0            
950 0           $header_sep .= '-' x $width->{cols_calc}[$col] . $tab;
951             }
952 0           $header_sep .= '-' x $width->{cols_calc}[$#{$width->{cols_calc}}] . $lrb;
  0            
953 0           return $header_sep;
954             }
955              
956              
957             sub _handle_reference {
958 0     0     require Data::Dumper;
959 0           local $Data::Dumper::Useqq = 1;
960 0           local $Data::Dumper::Indent = 0;
961 0           local $Data::Dumper::Terse = 1;
962 0           local $Data::Dumper::Maxdepth = 2;
963 0           return Data::Dumper::Dumper( $_[0] );
964             }
965              
966              
967             sub _minus_x_percent {
968             #my ( $value, $percent ) = @_;
969 0   0 0     return int( $_[0] - ( $_[0] / 100 * $_[1] ) ) || 1;
970             }
971              
972              
973              
974             1;
975              
976             __END__