File Coverage

blib/lib/Term/Choose/Util.pm
Criterion Covered Total %
statement 47 584 8.0
branch 0 292 0.0
condition 0 60 0.0
subroutine 16 40 40.0
pod 7 13 53.8
total 70 989 7.0


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