File Coverage

blib/lib/Term/Choose/Util.pm
Criterion Covered Total %
statement 47 581 8.0
branch 0 290 0.0
condition 0 70 0.0
subroutine 16 39 41.0
pod 7 13 53.8
total 70 993 7.0


line stmt bran cond sub pod time code
1             package Term::Choose::Util;
2              
3 1     1   137932 use warnings;
  1         3  
  1         71  
4 1     1   7 use strict;
  1         10  
  1         47  
5 1     1   27 use 5.10.1;
  1         5  
6              
7             our $VERSION = '0.147';
8 1     1   8 use Exporter 'import';
  1         1  
  1         101  
9             our @EXPORT_OK = qw( choose_a_directory choose_a_file choose_directories choose_a_number choose_a_subset settings_menu
10             insert_sep get_term_size get_term_width get_term_height unicode_sprintf );
11              
12 1     1   9 use Carp qw( croak );
  1         2  
  1         119  
13 1     1   9 use Cwd qw( realpath );
  1         2  
  1         69  
14 1     1   659 use Encode qw( decode encode );
  1         22000  
  1         147  
15 1     1   16 use File::Basename qw( basename dirname );
  1         2  
  1         155  
16 1     1   603 use File::Spec::Functions qw( catdir catfile );
  1         1074  
  1         98  
17 1     1   23 use List::Util qw( sum any );
  1         3  
  1         79  
18              
19 1     1   656 use Encode::Locale qw();
  1         6743  
  1         40  
20 1     1   652 use File::HomeDir qw();
  1         7751  
  1         47  
21              
22 1     1   920 use Term::Choose qw( choose );
  1         118570  
  1         254  
23 1     1   24 use Term::Choose::Constants qw( SGR_ES PH );
  1         2  
  1         91  
24 1     1   8 use Term::Choose::LineFold qw( cut_to_printwidth print_columns );
  1         21  
  1         70  
25 1     1   7 use Term::Choose::ValidateOptions qw( validate_options );
  1         2  
  1         11680  
26              
27              
28             sub new {
29 0     0 1   my $class = shift;
30 0           my ( $opt ) = @_;
31 0           my $instance_defaults = _defaults();
32 0 0         if ( defined $opt ) {
33 0 0         croak "Options have to be passed as a HASH reference." if ref $opt ne 'HASH';
34 0           my $caller = 'new';
35 0           validate_options( _valid_options( $caller ), $opt, $caller );
36 0           for my $key ( keys %$opt ) {
37 0 0         $instance_defaults->{$key} = $opt->{$key} if defined $opt->{$key};
38             }
39             }
40 0           my $self = bless $instance_defaults, $class;
41 0           $self->{backup_instance_defaults} = { %$instance_defaults };
42 0           return $self;
43             }
44              
45              
46             sub __restore_defaults {
47 0     0     my ( $self ) = @_;
48 0 0         if ( exists $self->{backup_instance_defaults} ) {
49 0           my $instance_defaults = $self->{backup_instance_defaults};
50 0           for my $key ( keys %$self ) {
51 0 0         if ( $key eq 'backup_instance_defaults' ) {
    0          
52 0           next;
53             }
54             elsif ( exists $instance_defaults->{$key} ) {
55 0           $self->{$key} = $instance_defaults->{$key};
56             }
57             else {
58 0           delete $self->{$key};
59             }
60             }
61             }
62             }
63              
64              
65             sub __prepare_opt {
66 0     0     my ( $self, $opt, $subseq_tab ) = @_;
67 0 0         if ( ! defined $opt ) {
68 0           $opt = {};
69             }
70 0 0         croak "Options have to be passed as a HASH reference." if ref $opt ne 'HASH';
71 0 0         if ( %$opt ) {
72 0           my $caller = ( caller( 1 ) )[3];
73 0           $caller =~ s/^.+::(?:__)?([^:]+)\z/$1/;
74 0           validate_options( _valid_options( $caller ), $opt, $caller );
75 0           my $defaults = _defaults();
76 0           for my $key ( keys %$opt ) {
77 0 0 0       if ( ! defined $opt->{$key} && defined $defaults->{$key} ) {
78 0           $self->{$key} = $defaults->{$key};
79             }
80             else {
81 0           $self->{$key} = $opt->{$key};
82             }
83             }
84             }
85 0 0         if ( ! defined $self->{tabs_info} ) {
86 0 0         if ( defined $self->{margin} ) {
87 0   0       $self->{tabs_info} = [ $self->{margin}[3] // 0, $self->{margin}[3] // 0, $self->{margin}[1] // 0 ];
      0        
      0        
88             }
89             }
90 0 0         if ( ! defined $self->{tabs_prompt} ) {
91 0 0         if ( defined $self->{margin} ) {
    0          
92             $self->{tabs_prompt} = [
93             $self->{margin}[3] // 0,
94             ( $self->{margin}[3] // 0 ) + ( $subseq_tab // 0 ),
95 0   0       $self->{margin}[1] // 0
      0        
      0        
      0        
96             ];
97             }
98             elsif ( $subseq_tab ) {
99 0           $self->{tabs_prompt} = [ 0, $subseq_tab, 0 ];
100             }
101             }
102             }
103              
104              
105             sub _valid_options {
106 0     0     my ( $caller ) = @_;
107 0           my %valid = (
108             all_by_default => '[ 0 1 ]',
109             clear_screen => '[ 0 1 ]',
110             decoded => '[ 0 1 ]',
111             hide_cursor => '[ 0 1 ]',
112             index => '[ 0 1 ]',
113             keep_chosen => '[ 0 1 ]',
114             mouse => '[ 0 1 ]',
115             order => '[ 0 1 ]',
116             show_hidden => '[ 0 1 ]',
117             small_first => '[ 0 1 ]',
118             alignment => '[ 0 1 2 ]',
119             color => '[ 0 1 2 ]',
120             layout => '[ 0 1 2 ]',
121             page => '[ 0 1 2 ]', # undocumented
122             keep => '[ 1-9 ][ 0-9 ]*', # undocumented
123             default_number => '[ 0-9 ]+',
124             margin => 'Array_Int',
125             mark => 'Array_Int',
126             tabs_info => 'Array_Int',
127             tabs_prompt => 'Array_Int',
128             busy_string => 'Str',
129             info => 'Str',
130             init_dir => 'Str',
131             back => 'Str',
132             filter => 'Str',
133             footer => 'Str', # undocumented
134             confirm => 'Str',
135             prefix => 'Str',
136             prompt => 'Str',
137             prompt2 => 'Str',
138             cs_begin => 'Str',
139             cs_end => 'Str',
140             cs_label => 'Str',
141             cs_separator => 'Str',
142             thousands_separator => 'Str',
143             );
144 0           my $options;
145 0 0         if ( $caller eq 'new' ) {
146 0           $options = [ keys %valid ];
147             }
148             else {
149 0           $options = _routine_options( $caller );
150             }
151 0           return { map { $_ => $valid{$_} } @$options };
  0            
152             };
153              
154              
155             sub _defaults {
156             return {
157 0     0     alignment => 0,
158             all_by_default => 0,
159             back => 'BACK',
160             #busy_string => undef,
161             clear_screen => 0,
162             color => 0,
163             confirm => 'CONFIRM',
164             cs_begin => '',
165             cs_end => '',
166             #cs_label => undef,
167             cs_separator => ', ',
168             decoded => 1,
169             #default_number => undef,
170             #filter => undef,
171             #footer => undef,
172             hide_cursor => 1,
173             index => 0,
174             #info => undef,
175             #init_dir => undef,
176             keep_chosen => 0,
177             #keep => undef,
178             layout => 1,
179             #margin => undef,
180             #mark => undef,
181             mouse => 0,
182             order => 1,
183             #page => undef,
184             parent_dir => '..',
185             prefix => '',
186             prompt => 'Your choice: ',
187             #prompt2 => undef,
188             show_hidden => 1,
189             small_first => 0,
190             #tabs_info => undef,
191             #tabs_prompt => undef,
192             thousands_separator => ',',
193              
194             ## intern:
195             reset => 'reset',
196             };
197             };
198              
199              
200             sub _routine_options {
201 0     0     my ( $caller ) = @_;
202 0           my @every = ( qw( back clear_screen color confirm cs_label footer hide_cursor info keep margin mouse page prompt tabs_info tabs_prompt ) );
203 0           my $options;
204 0 0         if ( $caller eq 'choose_directories' ) {
    0          
    0          
    0          
    0          
    0          
205 0           $options = [ @every, qw( init_dir layout order alignment show_hidden decoded prompt2 ) ];
206             }
207             elsif ( $caller eq 'choose_a_directory' ) {
208 0           $options = [ @every, qw( init_dir layout order alignment show_hidden decoded ) ];
209             }
210             elsif ( $caller eq 'choose_a_file' ) {
211 0           $options = [ @every, qw( init_dir layout order alignment show_hidden decoded filter prompt2 ) ];
212             }
213             elsif ( $caller eq 'choose_a_number' ) {
214 0           $options = [ @every, qw( small_first reset thousands_separator default_number ) ];
215             }
216             elsif ( $caller eq 'choose_a_subset' ) {
217 0           $options = [ @every, qw( layout order alignment keep_chosen index prefix all_by_default cs_begin cs_end cs_separator mark busy_string ) ];
218             }
219             elsif ( $caller eq 'settings_menu' ) {
220 0           $options = [ @every, qw( cs_begin cs_end cs_separator ) ];
221             }
222 0           return $options;
223             }
224              
225              
226             sub __prepare_path {
227 0     0     my ( $self ) = @_;
228 0           my $init_dir_fs;
229 0 0         if ( defined $self->{init_dir} ) {
230 0           $init_dir_fs = encode( 'locale_fs', $self->{init_dir} );
231 0 0         if ( ! -d $init_dir_fs ) {
232 0           my $prompt = 'Could not find the directory "';
233 0           $prompt .= decode 'locale_fs', $init_dir_fs;
234 0           $prompt .= '". Falling back to the home directory.';
235             # Choose
236             choose(
237             [ 'Press ENTER to continue' ],
238             { prompt => $prompt, hide_cursor => $self->{hide_cursor}, mouse => $self->{mouse}, page => $self->{page},
239             footer => $self->{footer}, keep => $self->{keep}, margin => $self->{margin} }
240 0           );
241 0           $init_dir_fs = File::HomeDir->my_home();
242             }
243             }
244             else {
245 0           $init_dir_fs = File::HomeDir->my_home();
246             }
247 0 0         if ( ! -d $init_dir_fs ) {
248 0           croak "Could not find the home directory.";
249             }
250 0 0         my $dir_fs = realpath( $init_dir_fs ) or die "$init_dir_fs: $!";
251 0           my $dir = decode( 'locale_fs', $dir_fs );
252 0           return $dir;
253             }
254              
255              
256             sub __available_dirs {
257 0     0     my ( $self, $dir ) = @_;
258 0           my $dir_fs = encode( 'locale_fs', $dir );
259 0           my $dh;
260 0 0         if ( ! eval {
261 0 0         opendir( $dh, $dir_fs ) or croak "$dir_fs: $!";
262 0           1 }
263             ) {
264 0           print "$@";
265             # Choose
266             choose(
267             [ 'Press Enter:' ],
268             { prompt => '', hide_cursor => $self->{hide_cursor}, mouse => $self->{mouse}, page => $self->{page},
269             footer => $self->{footer}, keep => $self->{keep}, margin => $self->{margin} }
270 0           );
271 0           return;
272             }
273 0           my @dirs;
274 0           while ( my $file_fs = readdir $dh ) {
275 0 0         next if $file_fs =~ /^\.\.?\z/;
276 0 0 0       next if $file_fs =~ /^\./ && ! $self->{show_hidden};
277 0 0         if ( -d catdir $dir_fs, $file_fs ) {
278 0           push @dirs, decode( 'locale_fs', $file_fs );
279             }
280             }
281 0           closedir $dh;
282 0           return [ sort @dirs ];
283             }
284              
285              
286             sub choose_directories {
287 0 0   0 1   if ( ref $_[0] ne __PACKAGE__ ) {
288 0           my $ob = __PACKAGE__->new();
289 0           delete $ob->{backup_instance_defaults};
290 0           return $ob->choose_directories( @_ );
291             }
292 0           my ( $self, $opt ) = @_;
293 0           my $subseq_tab = 2;
294 0           $self->__prepare_opt( $opt, $subseq_tab );
295 0           my $dir = $self->__prepare_path();
296 0           my $chosen_dirs = [];
297 0           my ( $confirm, $change_path, $add_dirs ) = ( $self->{confirm}, '- Change Location', '- Add Directories' );
298 0           my @bu;
299              
300 0           CHOOSE_MODE: while ( 1 ) {
301 0   0       my $key_dirs = $self->{cs_label} // 'Chosen Dirs: ';
302 0 0         my $dirs_chosen = $key_dirs . ( @$chosen_dirs ? join( ', ', @$chosen_dirs ) : '---' );
303 0           my $key_path = 'Location: ';
304 0           my $path = $key_path . $dir;
305 0           my $prompt = $dirs_chosen . "\n" . $path;
306             # Choose
307             my $choice = choose(
308             [ undef, $confirm, $change_path, $add_dirs ],
309             { info => $self->{info}, prompt => $prompt, layout => 2, mouse => $self->{mouse}, margin => $self->{margin},
310             clear_screen => $self->{clear_screen}, hide_cursor => $self->{hide_cursor}, page => $self->{page},
311             footer => $self->{footer}, keep => $self->{keep}, undef => $self->{back}, color => $self->{color},
312             tabs_info => $self->{tabs_info}, tabs_prompt => $self->{tabs_prompt} }
313 0           );
314 0 0         if ( ! defined $choice ) {
    0          
    0          
    0          
315 0 0         if ( @bu ) {
316 0           ( $dir, $chosen_dirs ) = @{pop @bu};
  0            
317 0           next CHOOSE_MODE;
318             }
319 0           $self->__restore_defaults(); #
320 0           return;
321             }
322             elsif ( $choice eq $confirm ) {
323 0           my $decoded = $self->{decoded};
324 0           $self->__restore_defaults();
325 0 0         return $decoded ? $chosen_dirs : [ map { encode 'locale_fs', $_ } @$chosen_dirs ];
  0            
326             }
327             elsif ( $choice eq $change_path ) {
328 0           my $prompt_fmt = $key_path . "%s";
329 0 0         if ( length $self->{prompt} ) {
330 0           $prompt_fmt .= "\n" . $self->{prompt};
331             }
332 0           my $tmp_dir = $self->__choose_a_path( $dir, $prompt_fmt, '<<', 'OK' );
333 0 0         if ( defined $tmp_dir ) {
334 0           $dir = $tmp_dir;
335             }
336             }
337             elsif ( $choice eq $add_dirs ) {
338 0           my $avail_dirs = $self->__available_dirs( $dir );
339 0 0         if ( ! defined $avail_dirs ) {
340 0           next CHOOSE_MODE;
341             }
342 0           my %bu_opt;
343 0           my $options = _routine_options( 'choose_directories' );
344 0           for my $o ( @$options ) {
345 0           $bu_opt{$o} = $self->{$o};
346             }
347 0           my $cs_label = $dirs_chosen . "\n" . $path . "\n" . 'Add: ';
348 0   0       my $prompt = $self->{prompt2} // $self->{prompt};
349             # choose_a_subset
350 0           my $idxs = $self->choose_a_subset(
351             [ sort @$avail_dirs ],
352             { cs_label => $cs_label, back => '<<', confirm => 'OK', cs_begin => undef, index => 1, keep_chosen => 1,
353             prompt => $prompt, # $self->{option} alreday in $self:
354             #info => $self->{info}, page => $self->{page}, footer => $self->{footer}, keep => $self->{keep},
355             # margin => $self->{margin}, tabs_info => $self->{tabs_info}, tabs_prompt => $self->{tabs_prompt}
356             }
357             );
358 0           for my $o ( keys %bu_opt ) {
359 0           $self->{$o} = $bu_opt{$o};
360             }
361 0 0 0       if ( defined $idxs && @$idxs ) {
362 0           push @bu, [ $dir, [ @$chosen_dirs ] ];
363 0           push @$chosen_dirs, map { catdir $dir, $_ } @{$avail_dirs}[@$idxs];
  0            
  0            
364             }
365             }
366             }
367             }
368              
369              
370             sub choose_a_file {
371 0 0   0 1   if ( ref $_[0] ne __PACKAGE__ ) {
372 0           my $ob = __PACKAGE__->new();
373 0           delete $ob->{backup_instance_defaults};
374 0           return $ob->choose_a_file( @_ );
375             }
376 0           my ( $self, $opt ) = @_;
377 0           $self->__prepare_opt( $opt );
378 0           my $init_dir = $self->__prepare_path();
379 0           my $prompt_fmt = "Source directory: %s";
380 0 0         if ( length $self->{prompt} ) {
381 0           $prompt_fmt .= "\n" . $self->{prompt};
382             }
383              
384 0           CHOOSE_DIR: while ( 1 ) {
385 0           my $chosen_dir = $self->__choose_a_path( $init_dir, $prompt_fmt, '<<', 'OK' );
386 0 0         if ( ! defined $chosen_dir ) {
387 0           $self->__restore_defaults(); #
388 0           return;
389             }
390 0           my $chosen_file = $self->__a_file( $chosen_dir );
391 0 0         if ( ! defined $chosen_file ) {
392 0           next CHOOSE_DIR;
393             }
394 0           my $decoded = $self->{decoded};
395 0           $self->__restore_defaults();
396 0 0         return $decoded ? $chosen_file : encode( 'locale_fs', $chosen_file );
397             }
398             }
399              
400              
401             sub choose_a_directory {
402 0 0   0 1   if ( ref $_[0] ne __PACKAGE__ ) {
403 0           my $ob = __PACKAGE__->new();
404 0           delete $ob->{backup_instance_defaults};
405 0           return $ob->choose_a_directory( @_ );
406             }
407 0           my ( $self, $opt ) = @_;
408 0           $self->__prepare_opt( $opt );
409 0           my $init_dir = $self->__prepare_path();
410 0   0       my $prompt_fmt = ( $opt->{cs_label} // 'Directory: ' ) . "%s";
411 0 0         if ( length $self->{prompt} ) {
412 0           $prompt_fmt .= "\n" . $self->{prompt};
413             }
414 0           my $chosen_dir = $self->__choose_a_path( $init_dir, $prompt_fmt, $self->{back}, $self->{confirm} );
415 0           my $decoded = $self->{decoded};
416 0           $self->__restore_defaults();
417 0 0         if ( ! defined $chosen_dir ) {
418 0           return;
419             }
420 0 0         return $decoded ? $chosen_dir : encode( 'locale_fs', $chosen_dir );
421             }
422              
423              
424             sub __choose_a_path {
425 0     0     my ( $self, $dir, $prompt_fmt, $back, $confirm ) = @_;
426 0           my $prev_dir = $dir;
427              
428 0           while ( 1 ) {
429 0           my ( $dh, @dirs );
430 0           my $dir_fs = encode( 'locale_fs', $dir );
431 0 0         if ( ! eval {
432 0 0         opendir( $dh, $dir_fs ) or croak "$dir_fs: $!";
433 0           1 }
434             ) {
435 0           print "$@";
436             # Choose
437             choose(
438             [ 'Press Enter:' ],
439             { prompt => '', hide_cursor => $self->{hide_cursor}, mouse => $self->{mouse}, page => $self->{page},
440             footer => $self->{footer}, keep => $self->{keep}, margin => $self->{margin} }
441 0           );
442 0           $dir = dirname $dir;
443 0           next;
444             }
445 0           while ( my $file_fs = readdir $dh ) {
446 0 0         next if $file_fs =~ /^\.\.?\z/;
447 0 0 0       next if $file_fs =~ /^\./ && ! $self->{show_hidden};
448 0 0         if ( -d catdir $dir_fs, $file_fs ) {
449 0           push @dirs, decode( 'locale_fs', $file_fs );
450             }
451             }
452 0           closedir $dh;
453 0           my $parent_dir = $self->{parent_dir};
454 0           my @pre = ( undef, $confirm, $parent_dir );
455 0           my $prompt = sprintf $prompt_fmt, $prev_dir;
456             # Choose
457             my $choice = choose(
458             [ @pre, sort( @dirs ) ],
459             { info => $self->{info}, prompt => $prompt, alignment => $self->{alignment},
460             layout => $self->{layout}, order => $self->{order}, mouse => $self->{mouse},
461             clear_screen => $self->{clear_screen}, hide_cursor => $self->{hide_cursor}, margin => $self->{margin},
462             color => $self->{color}, tabs_info => $self->{tabs_info}, tabs_prompt => $self->{tabs_prompt},
463 0           page => $self->{page}, footer => $self->{footer}, keep => $self->{keep}, undef => $back }
464             );
465 0 0         if ( ! defined $choice ) {
    0          
    0          
466 0           return;
467             }
468             elsif ( $choice eq $confirm ) {
469 0           return $prev_dir;
470             }
471             elsif ( $choice eq $parent_dir ) {
472 0           $dir = dirname $dir;
473             }
474             else {
475 0           $dir = catdir $dir, $choice;
476             }
477 0           $prev_dir = $dir;
478             }
479             }
480              
481              
482             sub __a_file {
483 0     0     my ( $self, $dir ) = @_;
484 0           my $prev_dir = '';
485 0           my $chosen_file;
486              
487 0           while ( 1 ) {
488 0           my @files_fs;
489 0           my $dir_fs = encode( 'locale_fs', $dir );
490 0 0         if ( ! eval {
491 0 0         if ( $self->{filter} ) {
492 0           @files_fs = map { basename $_} grep { -e $_ } glob( encode( 'locale_fs', catfile $dir, $self->{filter} ) );
  0            
  0            
493             }
494             else {
495 0 0         opendir( my $dh, $dir_fs ) or croak "$dir_fs: $!";
496 0           @files_fs = readdir $dh;
497 0           closedir $dh;
498             }
499 0           1 }
500             ) {
501 0           print "$@";
502             # Choose
503             choose(
504             [ 'Press Enter:' ],
505             { prompt => '', hide_cursor => $self->{hide_cursor}, mouse => $self->{mouse}, page => $self->{page},
506             footer => $self->{footer}, keep => $self->{keep}, margin => $self->{margin} }
507 0           );
508 0           return;
509             }
510 0           my @files;
511 0           for my $file_fs ( @files_fs ) {
512 0 0         next if $file_fs =~ /^\.\.?\z/;
513 0 0 0       next if $file_fs =~ /^\./ && ! $self->{show_hidden};
514 0 0         next if -d catdir $dir_fs, $file_fs;
515 0           push @files, decode( 'locale_fs', $file_fs );
516             }
517 0           my $chosen_dir = "Source directory: $dir";
518 0           my @tmp_prompt;
519 0           push @tmp_prompt, $chosen_dir;
520 0 0 0       push @tmp_prompt, ( $self->{cs_label} // 'File: ' ) . ( length $prev_dir ? $prev_dir : '' );
521 0   0       my $prompt2 = $self->{prompt2} // $self->{prompt};
522 0 0         if ( length $prompt2 ) {
523 0           push @tmp_prompt, $prompt2;
524             }
525 0           my $prompt = join( "\n", @tmp_prompt );
526 0 0         if ( ! @files ) {
527 0           $prompt .= "\n";
528 0 0         if ( $self->{filter} ) {
529 0           $prompt .= 'No matches for filter "' . $self->{filter} . '".';
530             }
531             else {
532 0           $prompt .= 'No files.';
533             }
534             # Choose
535             choose(
536             [ ' < ' ],
537             { info => $self->{info}, prompt => $prompt, hide_cursor => $self->{hide_cursor},
538             mouse => $self->{mouse}, color => $self->{color}, page => $self->{page}, footer => $self->{footer},
539             keep => $self->{keep}, margin => $self->{margin} }
540 0           );
541 0           return;
542             }
543 0           my @pre = ( undef );
544 0 0         if ( $chosen_file ) {
545 0           push @pre, $self->{confirm}; ##
546             }
547             # Choose
548             $chosen_file = choose(
549             [ @pre, sort( @files ) ],
550             { info => $self->{info}, prompt => $prompt, alignment => $self->{alignment}, layout => $self->{layout},
551             order => $self->{order}, mouse => $self->{mouse}, clear_screen => $self->{clear_screen},
552             hide_cursor => $self->{hide_cursor}, color => $self->{color}, tabs_info => $self->{tabs_info},
553             tabs_prompt => $self->{tabs_prompt}, page => $self->{page}, footer => $self->{footer},
554             keep => $self->{keep}, undef => $self->{back}, margin => $self->{margin} }
555 0           );
556 0 0         if ( ! length $chosen_file ) {
    0          
557 0 0         if ( length $prev_dir ) {
558 0           $prev_dir = '';
559 0           next;
560             }
561 0           return;
562             }
563             elsif ( $chosen_file eq $self->{confirm} ) {
564 0 0         return if ! length $prev_dir;
565 0           return catfile $dir, $prev_dir;
566             }
567             else {
568 0           $prev_dir = $chosen_file;
569             }
570             }
571             }
572              
573              
574             sub choose_a_number {
575 0 0   0 1   if ( ref $_[0] ne __PACKAGE__ ) {
576 0           my $ob = __PACKAGE__->new();
577 0           delete $ob->{backup_instance_defaults};
578 0           return $ob->choose_a_number( @_ );
579             }
580 0           my ( $self, $digits, $opt ) = @_;
581 0           my $default_digits = 7;
582 0 0         if ( ref $digits ) {
    0          
583 0           $opt = $digits;
584 0           $digits = $default_digits;
585             }
586             elsif ( ! $digits ) {
587 0           $digits = $default_digits;
588             }
589 0           $self->__prepare_opt( $opt );
590 0           my $tab = ' - ';
591 0           my $tab_w = print_columns( $tab );
592 0           my $sep_w = print_columns_ext( $self->{thousands_separator}, $self->{color} );
593 0           my $longest = $digits + int( ( $digits - 1 ) / 3 ) * $sep_w;
594 0           my @ranges = ();
595 0           for my $di ( 0 .. $digits - 1 ) {
596 0           my $begin = 1 . '0' x $di;
597 0 0         $begin = 0 if $di == 0;
598 0           $begin = insert_sep( $begin, $self->{thousands_separator} );
599 0           ( my $end = $begin ) =~ s/^[01]/9/;
600             unshift @ranges, unicode_sprintf( $begin, $longest, { right_justify => 1, color => $self->{color} } )
601             . $tab
602 0           . unicode_sprintf( $end, $longest, { right_justify => 1, color => $self->{color} } );
603             }
604 0           my $back_tmp = unicode_sprintf( $self->{back}, $longest * 2 + $tab_w + 1, { color => $self->{color} } );
605 0           my $confirm_tmp = unicode_sprintf( $self->{confirm}, $longest * 2 + $tab_w + 1, { color => $self->{color} } );
606 0 0         if ( print_columns_ext( $ranges[0], $self->{color} ) > get_term_width() ) {
607 0           @ranges = ();
608 0           for my $di ( 0 .. $digits - 1 ) {
609 0           my $begin = 1 . '0' x $di;
610 0 0         $begin = 0 if $di == 0;
611 0           $begin = insert_sep( $begin, $self->{thousands_separator} );
612 0           unshift @ranges, unicode_sprintf( $begin, $longest, { color => $self->{color} } );
613             }
614 0           $confirm_tmp = $self->{confirm};
615 0           $back_tmp = $self->{back};
616             }
617 0           my %numbers;
618             my $result;
619 0 0 0       if ( defined $self->{default_number} && length $self->{default_number} <= $digits ) {
620 0           my $count_zeros = 0;
621 0           for my $d ( reverse split '', $self->{default_number} ) {
622 0           $numbers{$count_zeros} = $d * 10 ** $count_zeros;
623 0           $count_zeros++;
624             }
625 0           $result = sum( @numbers{keys %numbers} );
626 0           $result = insert_sep( $result, $self->{thousands_separator} );
627             }
628              
629 0           NUMBER: while ( 1 ) {
630 0           my $cs_row;
631 0 0 0       if ( defined $self->{cs_label} || length $result ) {
632 0 0         my $tmp_result = length $result ? $result : '';
633 0   0       my $tmp_cs_label = $self->{cs_label} // '';
634 0           $cs_row = sprintf( "%s%*s", $tmp_cs_label, $longest, $tmp_result );
635 0 0         if ( print_columns( $cs_row ) > get_term_width() ) {
636 0           $cs_row = $tmp_result;
637             }
638             }
639 0           my @tmp_prompt;
640 0 0         if ( defined $cs_row ) {
641 0           push @tmp_prompt, $cs_row;
642             }
643 0 0         if ( length $self->{prompt} ) {
644 0           push @tmp_prompt, $self->{prompt};
645             }
646 0           my $prompt = join "\n", @tmp_prompt;
647 0           my @pre = ( undef, $confirm_tmp ); # confirm if $result ?
648             # Choose
649             my $range = choose(
650             $self->{small_first} ? [ @pre, reverse @ranges ] : [ @pre, @ranges ],
651             { info => $self->{info}, prompt => $prompt, layout => 2, alignment => 1, mouse => $self->{mouse},
652             clear_screen => $self->{clear_screen}, hide_cursor => $self->{hide_cursor}, color => $self->{color},
653             tabs_info => $self->{tabs_info}, tabs_prompt => $self->{tabs_prompt}, page => $self->{page},
654             footer => $self->{footer}, keep => $self->{keep}, undef => $back_tmp, margin => $self->{margin} }
655 0 0         );
656 0 0         if ( ! defined $range ) {
657 0 0         if ( defined $result ) {
658 0           $result = undef;
659 0           %numbers = ();
660 0           next NUMBER;
661             }
662             else {
663 0           $self->__restore_defaults();
664 0           return;
665             }
666             }
667 0 0         if ( $range eq $confirm_tmp ) {
668 0           $result = _remove_thousands_separators( $result, $self->{thousands_separator} );
669 0           $self->__restore_defaults();
670 0           return $result;
671             }
672 0           my $zeros = ( split /\s*-\s*/, $range )[0];
673 0           $zeros =~ s/^\s*\d//;
674 0           my $zeros_no_sep = _remove_thousands_separators( $zeros, $self->{thousands_separator} );
675 0           my $count_zeros = length $zeros_no_sep;
676 0 0         my @choices = $count_zeros ? map( $_ . $zeros, 1 .. 9 ) : ( 0 .. 9 );
677             # Choose
678             my $number = choose(
679             [ undef, @choices, $self->{reset} ],
680             { info => $self->{info}, prompt => $prompt, layout => 1, alignment => 2, order => 0,
681             mouse => $self->{mouse}, clear_screen => $self->{clear_screen}, hide_cursor => $self->{hide_cursor},
682             color => $self->{color}, tabs_info => $self->{tabs_info}, tabs_prompt => $self->{tabs_prompt},
683             page => $self->{page}, footer => $self->{footer}, keep => $self->{keep}, undef => '<<',
684             margin => $self->{margin} }
685 0           );
686 0 0         next if ! defined $number;
687 0 0         if ( $number eq $self->{reset} ) {
688 0           delete $numbers{$count_zeros};
689             }
690             else {
691 0           $numbers{$count_zeros} = _remove_thousands_separators( $number, $self->{thousands_separator} );
692             }
693 0           $result = sum( @numbers{keys %numbers} );
694 0           $result = insert_sep( $result, $self->{thousands_separator} );
695             }
696             }
697              
698              
699             sub _remove_thousands_separators {
700 0     0     my ( $str, $sep ) = @_;
701             # https://stackoverflow.com/questions/13119241/substitution-with-empty-string-unexpected-result
702 0 0 0       if ( defined $str && $sep ne '' ) {
703 0           $str =~ s/\Q$sep\E//g;
704             }
705 0           return $str;
706             }
707              
708              
709             sub choose_a_subset {
710 0 0   0 1   if ( ref $_[0] ne __PACKAGE__ ) {
711 0           my $ob = __PACKAGE__->new();
712 0           delete $ob->{backup_instance_defaults};
713 0           return $ob->choose_a_subset( @_ );
714             }
715 0           my ( $self, $available, $opt ) = @_;
716 0 0 0       my $subseq_tab = length $opt->{cs_label} && $opt->{cs_label} !~ /\R\z/ ? 2 : 0; ##
717 0           $self->__prepare_opt( $opt, $subseq_tab );
718 0           my $new_idx = [];
719 0           my $curr_avail = [ @$available ];
720 0           my $bu = [];
721 0           my @pre = ( undef, $self->{confirm} );
722 0           my $mark;
723 0 0 0       if ( defined $self->{mark} && @{$self->{mark}} ) {
  0            
724 0           $mark = [ map { $_ + @pre } @{$self->{mark}} ];
  0            
  0            
725             }
726              
727 0           while ( 1 ) {
728 0           my @tmp_prompt;
729             my $cs;
730 0 0         if ( defined $self->{cs_label} ) {
731 0           $cs .= $self->{cs_label};
732             }
733 0 0         if ( @$new_idx ) {
    0          
734 0 0         $cs .= $self->{cs_begin} . join( $self->{cs_separator}, map { defined $_ ? $_ : '' } @{$available}[@$new_idx] ) . $self->{cs_end};
  0            
  0            
735             }
736             elsif ( $opt->{all_by_default} ) {
737 0           $cs .= $self->{cs_begin} . '*' . $self->{cs_end};
738             }
739 0 0         if ( defined $cs ) {
740 0           @tmp_prompt = ( $cs );
741             }
742 0 0         if ( length $self->{prompt} ) {
743 0           push @tmp_prompt, $self->{prompt};
744             }
745 0           my $meta_items = [ 0 .. $#pre ];
746 0           my $prompt = join "\n", @tmp_prompt;
747             # Choose
748             my @idx = choose(
749 0 0         [ @pre, length( $self->{prefix} ) ? map { $self->{prefix} . ( defined $_ ? $_ : '' ) } @$curr_avail : @$curr_avail ],
750             { info => $self->{info}, prompt => $prompt, layout => $self->{layout}, index => 1,
751             alignment => $self->{alignment}, order => $self->{order}, mouse => $self->{mouse},
752             meta_items => $meta_items, mark => $mark, include_highlighted => 2,
753             clear_screen => $self->{clear_screen}, hide_cursor => $self->{hide_cursor},
754             color => $self->{color}, tabs_info => $self->{tabs_info}, tabs_prompt => $self->{tabs_prompt},
755             page => $self->{page}, footer => $self->{footer}, keep => $self->{keep}, undef => $self->{back},
756             busy_string => $self->{busy_string}, margin => $self->{margin} }
757 0 0         );
758 0           $mark = undef;
759 0 0 0       if ( ! defined $idx[0] || $idx[0] == 0 ) {
760 0 0         if ( @$bu ) {
761 0           ( $curr_avail, $new_idx ) = @{pop @$bu};
  0            
762 0           next;
763             }
764 0           $self->__restore_defaults();
765 0           return;
766             }
767 0           push @$bu, [ [ @$curr_avail ], [ @$new_idx ] ];
768 0           my $ok;
769 0 0         if ( $idx[0] == $#pre ) {
770 0           $ok = shift @idx;
771             }
772 0           my @tmp_idx;
773 0           for my $i ( reverse @idx ) {
774 0           $i -= @pre;
775 0 0         if ( ! $self->{keep_chosen} ) {
776 0           splice( @$curr_avail, $i, 1 );
777 0           for my $used_i ( sort { $a <=> $b } @$new_idx ) {
  0            
778 0 0         last if $used_i > $i;
779 0           ++$i;
780             }
781             }
782 0           push @tmp_idx, $i;
783             }
784 0           push @$new_idx, reverse @tmp_idx;
785 0 0         if ( $ok ) {
786 0 0 0       if ( ! @$new_idx && $opt->{all_by_default} ) {
787 0           $new_idx = [ 0 .. $#{$available} ];
  0            
788             }
789 0           my $return_indexes = $self->{index}; # because __restore_defaults resets $self->{index}
790 0           $self->__restore_defaults();
791 0 0         return $return_indexes ? $new_idx : [ @{$available}[@$new_idx] ];
  0            
792             }
793             }
794             }
795              
796              
797             sub settings_menu {
798 0 0   0 1   if ( ref $_[0] ne __PACKAGE__ ) {
799 0           my $ob = __PACKAGE__->new();
800 0           delete $ob->{backup_instance_defaults};
801 0           return $ob->settings_menu( @_ );
802             }
803 0           my ( $self, $menu, $curr, $opt ) = @_;
804 0           $self->__prepare_opt( $opt );
805 0           my $longest = 0;
806 0           my $new = {};
807 0           my $name_w = {};
808 0           for my $sub ( @$menu ) {
809 0           my ( $key, $name, $values ) = @$sub;
810 0           $name_w->{$key} = print_columns_ext( $name, $self->{color} );
811 0 0         if ( $name_w->{$key} > $longest ) {
812 0           $longest = $name_w->{$key};
813             }
814 0 0         $curr->{$key} = 0 if ! defined $curr->{$key};
815 0 0         $curr->{$key} = 0 if $curr->{$key} > $#$values;
816 0 0         $curr->{$key} = 0 if ! defined $values->[$curr->{$key}];
817              
818 0           while ( ! defined $values->[$curr->{$key}] ) {
819 0           ++$curr->{$key};
820 0 0         if ( $curr->{$key} > $#$values ) {
821 0           $curr->{$key} = 0;
822 0           last;
823             }
824             }
825 0           $new->{$key} = $curr->{$key};
826             }
827 0           my @print_keys;
828 0           for my $sub ( @$menu ) {
829 0           my ( $key, $name, $values ) = @$sub;
830 0   0       my $current = $values->[$new->{$key}] // '';
831 0           push @print_keys, $name . ( ' ' x ( $longest - $name_w->{$key} ) ) . " [$current]";
832             }
833 0           my @pre = ( undef, $self->{confirm} );
834 0           $ENV{TC_RESET_AUTO_UP} = 0;
835 0           my $default = 0;
836 0           my $count = 0;
837              
838 0           while ( 1 ) {
839 0           my @tmp_prompt;
840 0 0         if ( defined $self->{cs_label} ) {
841 0           push @tmp_prompt, $self->{cs_label} . $self->{cs_begin} . join( $self->{cs_separator}, map { "$_=$new->{$_}" } keys %$new ) . $self->{cs_end};
  0            
842             }
843 0 0         if ( length $self->{prompt} ) {
844 0           push @tmp_prompt, $self->{prompt};
845             }
846 0           my $prompt = join( "\n", @tmp_prompt );
847             # Choose
848             my $idx = choose(
849             [ @pre, @print_keys ],
850             { info => $self->{info}, prompt => $prompt, index => 1, default => $default, layout => 2, alignment => 0,
851             mouse => $self->{mouse}, clear_screen => $self->{clear_screen}, hide_cursor => $self->{hide_cursor},
852             color => $self->{color}, tabs_info => $self->{tabs_info}, tabs_prompt => $self->{tabs_prompt},
853             page => $self->{page}, footer => $self->{footer}, keep => $self->{keep}, undef => $self->{back},
854             margin => $self->{margin} }
855 0           );
856 0 0         if ( ! $idx ) {
    0          
857 0           $self->__restore_defaults();
858 0           return;
859             }
860             elsif ( $idx == $#pre ) {
861 0           my $change = 0;
862 0           for my $sub ( @$menu ) {
863 0           my $key = $sub->[0];
864 0 0         if ( $curr->{$key} == $new->{$key} ) {
865 0           next;
866             }
867 0           $curr->{$key} = $new->{$key};
868 0           $change++;
869             }
870 0           $self->__restore_defaults();
871 0           return $change; #
872             }
873 0           my $i = $idx - @pre;
874 0           my $key = $menu->[$i][0];
875 0           my $values = $menu->[$i][2];
876 0 0         if ( $default == $idx ) {
877 0 0         if ( $ENV{TC_RESET_AUTO_UP} ) {
    0          
878 0           $count = 0;
879             }
880             elsif ( $count == @$values ) {
881 0           $default = 0;
882 0           $count = 0;
883 0           next;
884             }
885             }
886             else {
887 0           $count = 0;
888 0           $default = $idx;
889             }
890 0   0       my $curr_value = $values->[$new->{$key}] // '';
891 0           my $new_value;
892              
893 0           while ( 1 ) {
894 0           ++$count;
895 0 0         if ( ++$new->{$key} > $#$values ) {
896 0           $new->{$key} = 0;
897             }
898 0           $new_value = $values->[$new->{$key}];
899 0 0         if ( defined $new_value ) {
900 0           last;
901             }
902 0 0         if ( $count == @$values ) {
903 0           $new_value = '';
904 0           last;
905             }
906             }
907 0           $print_keys[$i] =~ s/ \[ \Q$curr_value\E \] \z /[$new_value]/x;
908             }
909             }
910              
911              
912              
913             sub insert_sep {
914 0     0 0   my ( $number, $separator ) = @_;
915 0 0         return $number if ! length $number;
916 0 0         $separator = ',' if ! defined $separator;
917 0 0         return $number if $separator eq '';
918 0 0         return $number if $number =~ /\Q$separator\E/;
919 0           $number =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1$separator/g;
920             # http://perldoc.perl.org/perlfaq5.html#How-can-I-output-my-numbers-with-commas-added?
921 0           return $number;
922             }
923              
924              
925             sub get_term_size {
926 0     0 0   require Term::Choose::Screen;
927 0           return Term::Choose::Screen::get_term_size();
928             }
929              
930              
931             sub get_term_width {
932 0     0 0   require Term::Choose::Screen;
933 0           my $term_width = ( Term::Choose::Screen::get_term_size() )[0];
934 0           return $term_width;
935             }
936              
937             sub get_term_height {
938 0     0 0   require Term::Choose::Screen;
939 0           my $term_height = ( Term::Choose::Screen::get_term_size() )[1];
940 0           return $term_height;
941             }
942              
943              
944             sub unicode_sprintf {
945 0     0 0   my ( $str, $avail_w, $opt ) = @_;
946 0   0       $opt ||= {};
947 0           my @color;
948 0 0         if ( $opt->{color} ) {
949 0           $str =~ s/${\PH}//g;
  0            
950 0 0         $str =~ s/(${\SGR_ES})/push( @color, $1 ) && ${\PH}/ge;
  0            
  0            
  0            
951             }
952 0           my $str_w = print_columns( $str );
953 0 0         if ( $str_w > $avail_w ) {
954 0 0         if ( @{$opt->{suffix_on_truncate}||[]} ) {
  0 0          
955 0           $str = cut_to_printwidth( $str, $avail_w - $opt->{suffix_on_truncate}[1] ) . $opt->{suffix_on_truncate}[0];
956             }
957 0           $str = cut_to_printwidth( $str, $avail_w );
958             }
959 0 0         if ( @color ) {
960 0           $str =~ s/${\PH}/shift @color/ge;
  0            
  0            
961 0 0         if ( @color ) {
962 0           $str .= join '', @color;
963             }
964             }
965 0 0         if ( $str_w < $avail_w ) {
966 0 0         if ( $opt->{right_justify} ) {
967 0           return " " x ( $avail_w - $str_w ) . $str;
968             }
969             else {
970 0           return $str . " " x ( $avail_w - $str_w );
971             }
972             }
973             else {
974 0           return $str;
975             }
976             }
977              
978              
979             sub print_columns_ext {
980 0     0 0   my ( $str, $color ) = @_;
981 0 0         if ( $color ) {
982 0           $str =~ s/${\SGR_ES}//g;
  0            
983             }
984 0           return print_columns( $str );
985             }
986              
987              
988              
989              
990             1;
991              
992             __END__