File Coverage

blib/lib/Term/TablePrint.pm
Criterion Covered Total %
statement 45 576 7.8
branch 1 296 0.3
condition 0 57 0.0
subroutine 16 36 44.4
pod 2 2 100.0
total 64 967 6.6


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