File Coverage

blib/lib/Term/TablePrint.pm
Criterion Covered Total %
statement 45 570 7.8
branch 1 296 0.3
condition 0 60 0.0
subroutine 16 35 45.7
pod 2 2 100.0
total 64 963 6.6


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