File Coverage

blib/lib/IO/Prompter.pm
Criterion Covered Total %
statement 424 833 50.9
branch 233 540 43.1
condition 110 304 36.1
subroutine 50 80 62.5
pod 1 1 100.0
total 818 1758 46.5


line stmt bran cond sub pod time code
1 36     36   2940034 use 5.010;
  36         488  
2             package IO::Prompter;
3 36     36   22261 use utf8;
  36         536  
  36         190  
4              
5 36     36   1188 use warnings;
  36         73  
  36         1397  
6 36     36   22857 no if $] >= 5.018000, warnings => 'experimental';
  36         486  
  36         222  
7 36     36   3258 use strict;
  36         82  
  36         782  
8 36     36   178 use Carp;
  36         69  
  36         2499  
9 36     36   32734 use Contextual::Return qw< PUREBOOL BOOL SCALAR METHOD VOID LIST RETOBJ >;
  36         665667  
  36         222  
10 36     36   60611 use Scalar::Util qw< openhandle looks_like_number >;
  36         90  
  36         2036  
11 36     36   16822 use Symbol qw< qualify_to_ref >;
  36         29432  
  36         2385  
12 36     36   15788 use match::smart qw< match >;
  36         206899  
  36         317  
13              
14             our $VERSION = '0.005001';
15              
16             my $fake_input; # Flag that we're faking input from the source
17              
18             my $DEFAULT_TERM_WIDTH = 80;
19             my $DEFAULT_VERBATIM_KEY = "\cV";
20              
21             # Completion control...
22             my $COMPLETE_DISPLAY_FIELDS = 4; #...per line
23             my $COMPLETE_DISPLAY_GAP = 3; #...spaces
24              
25             my $COMPLETE_KEY = $ENV{IO_PROMPTER_COMPLETE_KEY} // qq{\t};
26             my $COMPLETE_HIST = $ENV{IO_PROMPTER_HISTORY_KEY} // qq{\cR};
27             my $COMPLETE_NEXT = qq{\cN};
28             my $COMPLETE_PREV = qq{\cP};
29              
30             my $COMPLETE_INIT = qr{ [\Q$COMPLETE_KEY$COMPLETE_HIST\E] }xms;
31             my $COMPLETE_CYCLE = qr{ [$COMPLETE_NEXT$COMPLETE_PREV] }xms;
32              
33             my %COMPLETE_MODE = (
34             $COMPLETE_KEY
35             => [split /\s+/, $ENV{IO_PROMPTER_COMPLETE_MODES}//q{list+longest full}],
36             $COMPLETE_HIST
37             => [split /\s+/, $ENV{IO_PROMPTER_HISTORY_MODES} // q{full}],
38             );
39              
40             my $FAKE_ESC = "\e";
41             my $FAKE_INSERT = "\cF";
42             my $MENU_ESC = "\e";
43             my $MENU_MK = '__M_E_N_U__';
44              
45             my %EDIT = (
46             BACK => qq{\cB},
47             FORWARD => qq{\cF},
48             START => qq{\cA},
49             END => qq{\cE},
50             );
51             my $EDIT_KEY = '['.join(q{},values %EDIT).']';
52              
53             # Extracting key letters...
54             my $KL_EXTRACT = qr{ (?| \[ ( [[:alnum:]]++ ) \]
55             | \( ( [[:alnum:]]++ ) \)
56             | \< ( [[:alnum:]]++ ) \>
57             | \{ ( [[:alnum:]]++ ) \}
58             )
59             }xms;
60             my $KL_DEF_EXTRACT = qr{ \[ ( [[:alnum:]]++ ) \] }xms;
61              
62              
63             # Auxiliary prompts for -Yes => N construct...
64             my @YESNO_PROMPTS = (
65             q{Really?},
66             q{You're quite certain?},
67             q{Definitely?},
68             q{You mean it?},
69             q{You truly mean it?},
70             q{You're sure?},
71             q{Have you thought this through?},
72             q{You understand the consequences?},
73             );
74              
75              
76             # Remember returned values for history completion...
77             my %history_cache;
78              
79             # Track lexically-scoped default options and wrapper subs...
80             my @lexical_options = [];
81             my @lexical_wrappers = [];
82              
83             # Export the prompt() sub...
84             sub import {
85 42     42   1520 my (undef, $config_data, @other_args) = @_;
86              
87             # Handle -argv requests...
88 42 50 66     393 if (defined $config_data && $config_data eq '-argv') {
    100          
    100          
    100          
89 0         0 scalar prompt(-argv, @other_args);
90             }
91              
92             # Handle lexical options...
93             elsif (ref $config_data eq 'ARRAY') {
94 3         6 push @lexical_options, $config_data;
95 3         14 $^H{'IO::Prompter::scope_number'} = $#lexical_options;
96             }
97              
98             # Handle lexical wrappers...
99             elsif (ref $config_data eq 'HASH') {
100 2         4 push @lexical_options, [];
101 2         5 $lexical_wrappers[ $#lexical_options ] = $config_data;
102 2         8 $^H{'IO::Prompter::scope_number'} = $#lexical_options;
103 2         3 for my $subname (keys %{$config_data}) {
  2         8  
104 2         3 my @args = @{$config_data->{$subname}};
  2         5  
105 36     36   27395 no strict 'refs';
  36         134  
  36         1499  
106 36     36   297 no warnings 'redefine';
  36         84  
  36         6138  
107 2         7 *{caller().'::'.$subname} = sub {
108 3     3   2648 my $scope_number = (caller 0)[10]{'IO::Prompter::scope_number'};
109 3   50     63 return prompt(@{$lexical_wrappers[$scope_number]{$subname}//[]}, @_);
  3         16  
110 2         9 };
111             }
112             }
113              
114             # Handler faked input specifications...
115             elsif (defined $config_data) {
116 2         4 $fake_input = $config_data;
117             }
118              
119 36     36   312 no strict 'refs';
  36         85  
  36         12437  
120 42         147 *{caller().'::prompt'} = \&prompt;
  42         189  
121             }
122              
123             # Prompt for, read, vet, and return input...
124             sub prompt {
125             # Reclaim full control of print statements while prompting...
126 99     99 1 63796 local $\ = '';
127              
128             # Locate any lexical default options...
129 99   100     341 my $hints_hash = (caller 0)[10] // {};
130 99   100     2819 my $scope_num = $hints_hash->{'IO::Prompter::scope_number'} // 0;
131              
132             # Extract and sanitize configuration arguments...
133 99         173 my $opt_ref = _decode_args(@{$lexical_options[$scope_num]}, @_);
  99         346  
134              
135             _warn( void => 'Useless use of prompt() in void context' )
136 97 100 100     382 if VOID && !$opt_ref->{-void};
137              
138             # Set up yesno prompts if required...
139             my @yesno_prompts
140 97 50 100     2820 = ($opt_ref->{-yesno}{count}//0) > 1 ? @YESNO_PROMPTS : ();
141              
142             # Work out where the prompts go, and where the input comes from...
143 97   66     361 my $in_filehandle = $opt_ref->{-in} // _open_ARGV();
144 97   66     629 my $out_filehandle = $opt_ref->{-out} // qualify_to_ref(select);
145 97 50       2881 if (!openhandle $in_filehandle) {
146 0 0       0 open my $fh, '<', $in_filehandle
147             or _opt_err('Unacceptable', '-in', 'valid filehandle or filename');
148 0         0 $in_filehandle = $fh;
149             }
150 97 50       364 if (!openhandle $out_filehandle) {
151 0 0       0 open my $fh, '>', $out_filehandle
152             or _opt_err('Unacceptable', '-out', 'valid filehandle or filename');
153 0         0 $out_filehandle = $fh;
154             }
155              
156             # Track timeouts...
157 36   50 36   290 my $in_pos = do { no warnings; tell $in_filehandle } // 0;
  36         78  
  36         35662  
  97         160  
  97         445  
158              
159             # Short-circuit if not valid handles...
160 97 50 33     532 return if !openhandle($in_filehandle) || !openhandle($out_filehandle);
161              
162             # Work out how they're arriving and departing...
163 97 50 33     560 my $outputter_ref = -t $in_filehandle && -t $out_filehandle
164             ? _std_printer_to($out_filehandle, $opt_ref)
165             : _null_printer()
166             ;
167 97         273 my $inputter_ref = _generate_unbuffered_reader_from(
168             $in_filehandle, $outputter_ref, $opt_ref
169             );
170              
171             # Clear the screen if requested to...
172 97 50       343 if ($opt_ref->{-wipe}) {
173 0         0 $outputter_ref->(-nostyle => "\n" x 1000);
174             }
175              
176             # Handle menu structures...
177 97         143 my $input;
178             eval {
179             REPROMPT_YESNO:
180 97 100       274 if ($opt_ref->{-menu}) {
181             # Remember top of (possibly nested) menu...
182 1         3 my @menu = ( $opt_ref->{-menu} );
183 1         3 my $top_prompt = $opt_ref->{-prompt};
184 1         17 $top_prompt =~ s{$MENU_MK}{$opt_ref->{-menu}{prompt}}xms;
185 1         4 $menu[-1]{prompt} = $top_prompt;
186              
187             MENU:
188 1         2 while (1) {
189             # Track the current level...
190 1         4 $opt_ref->{-menu_curr_level} = $menu[-1]{value_for};
191              
192             # Show menu and retreive choice...
193 1         14 $outputter_ref->(-style => $menu[-1]{prompt});
194 1         4 my $tag = $inputter_ref->($menu[-1]{constraint});
195              
196             # Handle a failure by exiting the loop...
197 1 50       4 last MENU if !defined $tag;
198 1         7 $tag =~ s{\A\s*(\S*).*}{$1}xms;
199              
200             # Handle by moving up menu stack...
201 1 50       4 if ($tag eq $MENU_ESC) {
202 0         0 $input = undef;
203 0 0       0 last MENU if @menu <= 1;
204 0         0 pop @menu;
205 0         0 next MENU;
206             }
207              
208             # Handle defaults by selecting and ejecting...
209 1 50 33     8 if ($tag =~ /\A\R?\Z/ && exists $opt_ref->{-def}) {
210 1         10 $input = $tag;
211 1         5 last MENU;
212             }
213              
214             # Otherwise, retrieve value for selected tag and exit if not a nested menu...
215 0         0 $input = $menu[-1]{value_for}{$tag};
216 0 0       0 last MENU if !ref $input;
217              
218             # Otherwise, go down the menu one level...
219             push @menu,
220             _build_menu($input,
221             "Select from $menu[-1]{key_for}{$tag}: ",
222             $opt_ref->{-number} || $opt_ref->{-integer}
223 0   0     0 );
224 0         0 $menu[-1]{prompt} .= '> ';
225             }
226             }
227              
228             # Otherwise, simply ask and ye shall receive...
229             else {
230 96         345 $outputter_ref->(-style => $opt_ref->{-prompt});
231 96         255 $input = $inputter_ref->();
232             }
233 97         313 1;
234             }
235 97   33     228 // do {
236             # Supply the missing newline if requested...
237             $outputter_ref->(-echostyle => $opt_ref->{-return}(q{}))
238 0 0       0 if exists $opt_ref->{-return};
239              
240             # Rethrow any other exception...
241 0         0 my $error = $@;
242 0 0       0 die $@ unless ref($error) eq 'IO::Prompter::Cancellation';
243              
244             # Return failure on cancellation...
245 0 0       0 return if $opt_ref->{-verbatim};
246 0     0   0 return PUREBOOL { 0 }
247 0     0   0 BOOL { 0 }
248 0     0   0 SCALAR { ${$error} }
  0         0  
249 0     0   0 METHOD { defaulted => sub { 0 }, timedout => sub { 0 } };
  0         0  
  0         0  
  0         0  
250             };
251              
252             # Provide default value if available and necessary...
253 97         171 my $defaulted = 0;
254 97 50 100     654 if (defined $input && $input =~ /\A\R?\Z/ && exists $opt_ref->{-def}) {
      66        
255 5         12 $input = $opt_ref->{-def};
256 5         26 $defaulted = 1;
257             }
258              
259             # The input line is usually chomped before being returned...
260 97 100 100     450 if (defined $input && !$opt_ref->{-line}) {
261 82         149 chomp $input;
262             }
263              
264             # Check for a value indicating failure...
265 97 100 100     296 if (exists $opt_ref->{-fail} && match($input, $opt_ref->{-fail})) {
266 2         227 $input = undef;
267             }
268              
269             # Setting @ARGV is a special case; process it like a command-line...
270 97 100       345 if ($opt_ref->{-argv}) {
271 5         11 @ARGV = map { _shell_expand($_) }
272 1         17 grep {defined}
  15         24  
273             $input =~ m{
274             ( ' [^'\\]* (?: \\. [^'\\]* )* ' )
275             | ( " [^"\\]* (?: \\. [^"\\]* )* " )
276             | (?: ^ | \s) ( [^\s"'] \S* )
277             }gxms;
278 1         19 return 1;
279             }
280              
281             # "Those who remember history are enabled to repeat it"...
282 96 100 66     436 if (defined $input and $opt_ref->{-history} ne 'NONE') {
283 84   100     399 my $history_set = $history_cache{ $opt_ref->{-history} } //= [] ;
284 84         151 @{ $history_set } = ($input, grep { $_ ne $input } @{ $history_set });
  84         284  
  179         379  
  84         185  
285             }
286              
287             # If input timed out insert the default, if any...
288 36   50 36   296 my $timedout = $in_pos == do{ no warnings; tell $in_filehandle } // 0;
  36         88  
  36         43730  
  96         195  
  96         395  
289 96 50 66     255 if ($timedout && exists $opt_ref->{-def}) {
290 0         0 $input = $opt_ref->{-def};
291 0         0 $defaulted = 1;
292             }
293              
294             # A defined input is a successful input...
295 96         197 my $succeeded = defined $input;
296              
297             # The -yesno variants also need a 'y' to be successful...
298 96 100       262 if ($opt_ref->{-yesno}{count}) {
299 27   66     152 $succeeded &&= $input =~ m{\A \s* y}ixms;
300 27 50 66     87 if ($succeeded && $opt_ref->{-yesno}{count} > 1) {
301 0         0 my $count = --$opt_ref->{-yesno}{count};
302             $opt_ref->{-prompt}
303 0 0       0 = @yesno_prompts ? shift(@yesno_prompts) . q{ }
    0          
304             : $count > 1 ? qq{Please confirm $count more times }
305             : q{Please confirm one last time }
306             ;
307 0         0 goto REPROMPT_YESNO; # Gasp, yes goto is the cleanest way!
308             }
309             }
310              
311             # Verbatim return doesn't do fancy tricks...
312 96 100       268 if ($opt_ref->{-verbatim}) {
313 6   66     138 return $input // ();
314             }
315              
316             # Failure in a list context returns nothing...
317 90 100 100     331 return if LIST && !$succeeded;
318              
319             # Otherwise, be context sensitive...
320             return
321 43     43   19250 PUREBOOL { $_ = RETOBJ; next handler; }
  43         1082  
322 75     75   17434 BOOL { $succeeded; }
323 48     48   19731 SCALAR { $input; }
324             METHOD {
325 0         0 defaulted => sub { $defaulted },
326             timedout => sub {
327 0 0       0 return q{} if !$timedout;
328             return "timed out after $opt_ref->{-timeout} second"
329 0 0       0 . ($opt_ref->{-timeout} == 1 ? q{} : q{s});
330             },
331 86     0   2122 };
  0         0  
332             }
333              
334              
335             # Simulate a command line expansion for the -argv option...
336             sub _shell_expand {
337 5     5   12 my ($text) = @_;
338              
339             # Single-quoted text is literal...
340 5 100       17 if ($text =~ m{\A ' (.*) ' \z}xms) {
341 1         6 return $1;
342             }
343              
344             # Everything else has shell variables expanded...
345 4         80 my $ENV_PAT = join '|', reverse sort keys %ENV;
346 4         204 $text =~ s{\$ ($ENV_PAT)}{$ENV{$1}}gxms;
347              
348             # Double-quoted text isn't globbed...
349 4 100       56 if ($text =~ m{\A " (.*) " \z}xms) {
350 2         6 return $1;
351             }
352              
353             # Everything else is...
354 2         72 return glob($text);
355             }
356              
357             # No completion is the default...
358             my $DEFAULT_COMPLETER = sub { q{} };
359              
360             # Translate std constraints...
361             my %STD_CONSTRAINT = (
362             positive => sub { $_ > 0 },
363             negative => sub { $_ < 0 },
364             zero => sub { $_ == 0 },
365             even => sub { $_ % 2 == 0 },
366             odd => sub { $_ % 2 != 0 },
367             );
368              
369             # Create abbreviations...
370             $STD_CONSTRAINT{pos} = $STD_CONSTRAINT{positive};
371             $STD_CONSTRAINT{neg} = $STD_CONSTRAINT{negative};
372              
373             # Create antitheses...
374             for my $constraint (keys %STD_CONSTRAINT) {
375             my $implementation = $STD_CONSTRAINT{$constraint};
376             $STD_CONSTRAINT{"non$constraint"}
377             = sub { ! $implementation->(@_) };
378             }
379              
380             # Special style specifications require decoding...
381              
382             sub _decode_echo {
383 1     1   3 my $style = shift;
384              
385             # Not a special style...
386 1 50 33     14 return $style if ref $style || $style !~ m{/};
387              
388             # A slash means yes/no echoes...
389 0         0 my ($yes, $no) = split m{/}, $style;
390 0 0   0   0 return sub{ /y/i ? $yes : $no };
  0         0  
391             }
392              
393             sub _decode_echostyle {
394 0     0   0 my $style = shift;
395              
396             # Not a special style...
397 0 0 0     0 return $style if ref $style || $style !~ m{/};
398              
399             # A slash means yes/no styles...
400 0         0 my ($yes, $no) = split m{/}, $style;
401 0 0   0   0 return sub{ /y/i ? $yes : $no };
  0         0  
402             }
403              
404             sub _decode_style {
405             # No special prompt styles (yet)...
406 0     0   0 return shift;
407             }
408              
409             # Generate safe closure around active sub...
410             sub _gen_wrapper_for {
411 1     1   4 my ($arg) = @_;
412             return ref $arg ne 'CODE'
413 0     0   0 ? sub { $arg }
414 36 50 0 36   334 : sub { eval { for (shift) { no warnings; return $arg->($_) // $_ } } };
  36     0   150  
  36         117499  
  1         9  
  0         0  
  0         0  
  0         0  
415             }
416              
417             # Create recognizer...
418             my $STD_CONSTRAINT
419             = '^(?:' . join('|', reverse sort keys %STD_CONSTRAINT) . ')';
420              
421             # Translate name constraints to implementations...
422             sub _standardize_constraint {
423 3     3   10 my ($option_type, $constraint_spec) = @_;
424              
425 3 100       17 return ("be an acceptable $option_type", $constraint_spec)
426             if ref $constraint_spec;
427              
428 1         6 my @constraint_names = split /\s+/, $constraint_spec;
429             my @constraints =
430 1   33     4 map { $STD_CONSTRAINT{$_}
  2         8  
431             // _opt_err('invalid',-$option_type,'"pos", "neg", "even", etc.', qq{"$_"})
432             } @constraint_names;
433              
434             return (
435             'be ' . join(' and ', @constraint_names),
436             sub {
437 4     4   168 my ($compare_val) = @_;
438 4         8 for my $constraint (@constraints) {
439 6 100       12 return 0 if !$constraint->($compare_val);
440             }
441 1         4 return 1;
442             }
443 1         8 );
444             }
445              
446              
447             # Convert args to prompt + options hash...
448             sub _decode_args {
449             my %option = (
450             -prompt => undef,
451             -complete => $DEFAULT_COMPLETER,
452             -must => {},
453             -history => 'DEFAULT',
454 0     0   0 -style => sub{ q{} },
455 0     0   0 -nostyle => sub{ q{} },
456 0     0   0 -echostyle => sub{ q{} },
457 30 50   30   169 -echo => sub { my $char = shift; $char eq "\t" ? q{ } : $char },
  30         399  
458 0     0   0 -return => sub { "\n" },
459 99     99   1243 );
460              
461             DECODING:
462 99         403 while (defined(my $arg = shift @_)) {
463 220 50       545 if (my $type = ref $arg) {
464 0         0 _warn( reserved =>
465             'prompt(): Unexpected argument (' . lc($type) . ' ref) ignored'
466             );
467             }
468             else {
469 220         290 state $already_wiped;
470 220         282 my $redo;
471             # The sound of one hand clapping...
472 220 100       5155 if ($arg =~ /^-_/) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    100          
    50          
    50          
    50          
    50          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    50          
    100          
    50          
    50          
    50          
    50          
    50          
    100          
    50          
    100          
473 6         22 $redo = 1;
474             }
475              
476             # Non-chomping option...
477             elsif ($arg =~ /^-line$/) {
478 1         3 $option{-line}++;
479             }
480             elsif ($arg =~ /^-l/) {
481 4         16 $option{-line}++;
482 4         8 $redo = 1;
483             }
484              
485             # The -yesno variants...
486             elsif ($arg =~ /^-YesNo$/) {
487 4 50 33     13 my $count = @_ && looks_like_number($_[0]) ? shift @_ : 1;
488             $option{-yesno} = {
489 4         55 must => { '[YN]' => qr{\A \s* [YN] }xms },
490             count => $count,
491             };
492             }
493             elsif ($arg =~ /^-YN/) {
494             $option{-yesno} = {
495 1         16 must => { '[YN]' => qr{\A \s* [YN] }xms },
496             count => 1,
497             };
498 1         3 $redo = 2;
499             }
500             elsif ($arg =~ /^-yesno$/) {
501 4 50 33     17 my $count = @_ && looks_like_number($_[0]) ? shift @_ : 1;
502             $option{-yesno} = {
503 4         28 must => { '[yn]' => qr{\A \s* [YN] }ixms },
504             count => $count,
505             };
506             }
507             elsif ($arg =~ /^-yn/) {
508             $option{-yesno} = {
509 4         49 must => { '[yn]' => qr{\A \s* [YN] }ixms },
510             count => 1,
511             };
512 4         11 $redo = 2;
513             }
514             elsif ($arg =~ /^-Yes$/) {
515 7 50 33     29 my $count = @_ && looks_like_number($_[0]) ? shift @_ : 1;
516             $option{-yesno} = {
517 7         38 must => { '[Y for yes]' => qr{\A \s* (?: [^y] | \Z) }xms },
518             count => $count,
519             };
520             }
521             elsif ($arg =~ /^-Y/) {
522             $option{-yesno} = {
523 1         10 must => { '[Y for yes]' => qr{\A \s* (?: [^y] | \Z) }xms },
524             count => 1,
525             };
526 1         2 $redo = 1;
527             }
528             elsif ($arg =~ /^-yes$/) {
529 5 50 33     18 my $count = @_ && looks_like_number($_[0]) ? shift @_ : 1;
530 5         15 $option{-yesno} = { count => $count };
531             }
532             elsif ($arg =~ /^-y/) {
533 1         6 $option{-yesno} = { count => 1 };
534 1         3 $redo = 1;
535             }
536              
537             # Load @ARGV...
538             elsif ($arg =~ /^-argv$/) {
539 1         3 $option{-argv} = 1;
540             }
541              
542             elsif ($arg =~ /^-a/) {
543 0         0 $option{-argv} = 1;
544 0         0 $redo = 1;
545             }
546              
547             # Clear screen before prompt...
548             elsif ($arg =~ /^-wipe(first)?$/) {
549 0 0       0 $option{-wipe} = $1 ? !$already_wiped : 1;
550 0         0 $already_wiped = 1;
551             }
552             elsif ($arg =~ /^-w/) {
553 0         0 $option{-wipe} = 1;
554 0         0 $already_wiped = 1;
555 0         0 $redo = 1;
556             }
557              
558             # Specify a failure condition...
559             elsif ($arg =~ /^-fail$/) {
560 3 50       7 _opt_err('Missing', -fail, 'failure condition') if !@_;
561 3         8 $option{-fail} = shift @_;
562             }
563              
564             # Specify an immediate failure condition...
565             elsif ($arg =~ /^-cancel/) {
566 0 0       0 _opt_err('Missing', -cancel, 'cancellation condition') if !@_;
567 0         0 $option{-cancel} = shift @_;
568             }
569              
570             # Specify a file request...
571             elsif ($arg =~ /^-f(?:ilenames?)?$/) {
572 0     0   0 $option{-must}{'0: be an existing file'} = sub { -e $_[0] };
  0         0  
573 0     0   0 $option{-must}{'1: be readable'} = sub { -r $_[0] };
  0         0  
574 0         0 $option{-complete} = 'filenames';
575             }
576              
577             # Specify prompt echoing colour/style...
578             elsif ($arg =~ /^-style/) {
579 0 0       0 _opt_err('Missing -style specification') if !@_;
580 0         0 my $style = _decode_style(shift @_);
581 0         0 $option{-style} = _gen_wrapper_for($style);
582             }
583              
584             # Specify input colour/style...
585             elsif ($arg =~ /^-echostyle/) {
586 0 0       0 _opt_err('Missing -echostyle specification') if !@_;
587 0         0 my $style = _decode_echostyle(shift @_);
588 0         0 $option{-echostyle} = _gen_wrapper_for($style);
589             }
590              
591              
592             # Specify input and output filehandles...
593 0         0 elsif ($arg =~ /^-stdio$/) { $option{-in} = *STDIN;
594 0         0 $option{-out} = *STDOUT;
595             }
596 17         43 elsif ($arg =~ /^-in$/) { $option{-in} = shift @_; }
597 6         27 elsif ($arg =~ /^-out$/) { $option{-out} = shift @_; }
598              
599              
600             # Specify integer and number return value...
601             elsif ($arg =~ /^-integer$/) {
602 6         13 $option{-integer} = 1;
603 6 100 100     176 if (@_ && (ref $_[0] || $_[0] =~ $STD_CONSTRAINT)) {
      66        
604 3         10 my ($errmsg, $constraint)
605             = _standardize_constraint('integer',shift);
606 3         10 $option{-must}{$errmsg} = $constraint;
607             }
608             }
609             elsif ($arg =~ /^-num(?:ber)?$/) {
610 2         6 $option{-number} = 1;
611 2 50 33     170 if (@_ && (ref $_[0] || $_[0] =~ $STD_CONSTRAINT)) {
      33        
612 0         0 my ($errmsg, $constraint)
613             = _standardize_constraint('number',shift);
614 0         0 $option{-must}{$errmsg} = $constraint;
615             }
616             }
617 1         3 elsif ($arg =~ /^-i/) { $option{-integer} = 1; $redo = 1; }
  1         2  
618 1         3 elsif ($arg =~ /^-n/) { $option{-number} = 1; $redo = 1; }
  1         2  
619              
620             # Specify void context is okay...
621 1         3 elsif ($arg =~ /^-void$/) { $option{-void} = 1; }
622              
623             # Specify verbatim return value...
624 3         10 elsif ($arg =~ /^-verb(?:atim)?$/) { $option{-verbatim} = 1; }
625 3         8 elsif ($arg =~ /^-v/) { $option{-verbatim} = 1; $redo = 1;}
  3         6  
626              
627             # Specify single character return...
628 1         4 elsif ($arg =~ /^-sing(?:le)?$/) { $option{-single} = 1; }
629 5         13 elsif ($arg =~ /^-[s1]/) { $option{-single} = 1; $redo = 1; }
  5         8  
630              
631             # Specify a default...
632             elsif ($arg =~ /^-DEF(?:AULT)?/) {
633 0 0       0 _opt_err('Missing', '-DEFAULT', 'string') if !@_;
634 0         0 $option{-def} = shift @_;
635 0         0 $option{-def_nocheck} = 1;
636             _opt_err('Invalid', '-DEFAULT', 'string', 'reference')
637 0 0       0 if ref($option{-def});
638             }
639             elsif ($arg =~ /^-def(?:ault)?/) {
640 5 50       31 _opt_err('Missing', '-default', 'string') if !@_;
641 5         16 $option{-def} = shift @_;
642             _opt_err('Invalid', '-default', 'string', 'reference')
643 5 50       15 if ref($option{-def});
644             }
645 2         7 elsif ($arg =~ /^-d(.+)$/) { $option{-def} = $1; }
646              
647             # Specify a timeout...
648             elsif ($arg =~ /^-t(\d+)/) {
649 1         5 $option{-timeout} = $1;
650 1         6 $arg =~ s{\d+}{}xms;
651 1         2 $redo = 1;
652             }
653             elsif ($arg =~ /^-timeout$/) {
654 2 100       9 _opt_err('Missing', -timeout, 'number of seconds') if !@_;
655 1         4 $option{-timeout} = shift @_;
656             _opt_err('Invalid', -timeout,'number of seconds', qq{'$option{-timeout}'})
657 1 50       11 if !looks_like_number($option{-timeout});
658             }
659              
660             # Specify a set of input constraints...
661             elsif ($arg =~ /^-g.*/) {
662 8 50       32 _opt_err('Missing', -guarantee, 'input restriction') if !@_;
663 8         13 my $restriction = shift @_;
664 8         17 my $restriction_type = ref $restriction;
665              
666 8         19 $option{-must}{'be a valid input'} = $restriction;
667              
668             # Hashes restrict input to their keys...
669 8 100       20 if ($restriction_type eq 'HASH') {
670 2         4 $restriction_type = 'ARRAY';
671 2         3 $restriction = [ keys %{$restriction} ];
  2         8  
672             }
673             # Arrays of strings matched (and completed) char-by-char...
674 8 100       21 if ($restriction_type eq 'ARRAY') {
    50          
675 7         11 my @restrictions = @{$restriction};
  7         22  
676             $option{-guarantee}
677             = '\A(?:'
678             . join('|', map {
679 7         18 join(q{}, map { "(?:\Q$_\E" } split(q{}, $_))
  30         67  
  34         121  
680             . ')?' x length($_)
681             } @restrictions)
682             . ')\z'
683             ;
684 7 50       23 if ($option{-complete} == $DEFAULT_COMPLETER) {
685 7         17 $option{-complete} = \@restrictions;
686             }
687             }
688             # Regexes matched as-is...
689             elsif ($restriction_type eq 'Regexp') {
690 1         3 $option{-guarantee} = $restriction;
691             }
692             else {
693 0         0 _opt_err( 'Invalid', -guarantee,
694             'array or hash reference, or regex'
695             );
696             }
697             }
698              
699             # Specify a set of key letters...
700             elsif ($arg =~ '-keyletters_implement') {
701             # Extract all keys and default keys...
702 4         56 my @keys = ($option{-prompt} =~ m{$KL_EXTRACT}gxms);
703              
704             # Convert default to a -default...
705 4         23 my @defaults = ($option{-prompt} =~ m{$KL_DEF_EXTRACT}gxms);
706 4 50       14 if (@defaults > 1) {
    100          
707 0         0 _warn( ambiguous =>
708             "prompt(): -keyletters found too many defaults"
709             )
710             }
711             elsif (@defaults) {
712 2         8 push @_, -default => $defaults[0];
713             }
714              
715             # Convert key letters to a -guarantee...
716 4         11 @keys = ( map({uc} @keys), map({lc} @keys) );
  10         23  
  10         25  
717 4 100       10 if (@defaults == 1) {
718 2         4 push @keys, q{};
719             }
720 4         13 push @_, -guarantee => \@keys;
721              
722             }
723             elsif ($arg =~ /^-key(?:let(?:ter)?)(?:s)?/) {
724 2         6 push @_, '-keyletters_implement';
725             }
726             elsif ($arg =~ /^-k/) {
727 2         8 push @_, '-keyletters_implement';
728 2         4 $redo = 1;
729             }
730              
731             # Specify a set of return constraints...
732             elsif ($arg =~ /^-must$/) {
733 6 50       21 _opt_err('Missing', -must, 'constraint hash') if !@_;
734 6         11 my $must = shift @_;
735 6 50       16 _opt_err('Invalid', -must, 'hash reference')
736             if ref($must) ne 'HASH';
737 6         10 for my $errmsg (keys %{$must}) {
  6         23  
738 6         19 $option{-must}{$errmsg} = $must->{$errmsg};
739             }
740             }
741              
742             # Specify a history set...
743             elsif ($arg =~ /^-history/) {
744             $option{-history}
745 0 0 0     0 = @_ && $_[0] !~ /^-/ ? shift @_
746             : undef;
747             _opt_err('Invalid', -history, 'history set name', qq{'$option{-history}'})
748 0 0       0 if ref($option{-history});
749             }
750 0 0       0 elsif ($arg =~ /^-h(.*)/) { $option{-history} = length($1) ? $1 : undef; }
751              
752             # Specify completions...
753             elsif ($arg =~ /^-comp(?:lete)?/) {
754 0 0       0 _opt_err('Missing', -complete, 'completions') if !@_;
755 0         0 my $comp_spec = shift @_;
756 0   0     0 my $comp_type = ref($comp_spec) || $comp_spec || '???';
757 0 0       0 if ($comp_type =~ m{\A(?: file\w* | dir\w* | ARRAY | HASH | CODE )\Z}xms) {
758 0         0 $option{-complete} = $comp_spec;
759             }
760             else {
761 0         0 _opt_err( 'Invalid', -complete,
762             '"filenames", "dirnames", or reference to array, hash, or subroutine');
763             }
764             }
765              
766             # Specify what to echo when a character is keyed...
767             elsif ($arg =~ /^-(echo|ret(?:urn)?)$/) {
768 0 0       0 my $flag = $1 eq 'echo' ? '-echo' : '-return';
769 36 0 0 36   356 if ($flag eq '-echo' && !eval { no warnings 'deprecated'; require Term::ReadKey }) {
  36         98  
  36         6780  
  0         0  
  0         0  
770 0         0 _warn( bareword => "Warning: next input will be in plaintext\n");
771             }
772 0 0 0     0 my $arg = @_ && $_[0] !~ /^-/ ? shift(@_)
    0          
773             : $flag eq '-echo' ? q{}
774             : qq{\n};
775 0         0 $option{$flag} = _gen_wrapper_for(_decode_echo($arg));
776             }
777             elsif ($arg =~ /^-e(.*)/) {
778 36 50   36   309 if (!eval { no warnings 'deprecated'; require Term::ReadKey }) {
  36         81  
  36         80171  
  1         7  
  1         141  
779 1         7 _warn( bareword => "Warning: next input will be in plaintext\n");
780             }
781 1         11 my $arg = $1;
782 1         7 $option{-echo} = _gen_wrapper_for(_decode_echo($arg));
783             }
784             elsif ($arg =~ /^-r(.+)/) {
785 0         0 my $arg = $1;
786 0         0 $option{-return} = _gen_wrapper_for(_decode_echo($arg));
787             }
788             elsif ($arg =~ /^-r/) {
789 0     0   0 $option{-return} = sub{ "\n" };
  0         0  
790             }
791              
792             # Specify an initial input...
793             elsif ($arg =~ /^-prefill/) {
794 0 0       0 _opt_err('Missing', '-prefill', 'string') if !@_;
795 0         0 $option{-prefill} = shift @_;
796             _opt_err('Invalid', '-prefill', 'string', 'reference')
797 0 0       0 if ref($option{-prefill});
798             }
799              
800             # Explicit prompt replaces implicit prompts...
801             elsif ($arg =~ /^-prompt$/) {
802 0 0       0 _opt_err('Missing', '-prompt', 'prompt string') if !@_;
803 0         0 $option{-prompt} = shift @_;
804             _opt_err('Invalid', '-prompt', 'string', 'reference')
805 0 0       0 if ref($option{-prompt});
806             }
807             elsif ($arg =~ /^-p(\S*)$/) {
808 0         0 $option{-prompt} = $1;
809             }
810              
811             # Menus inject a placeholder in the prompt string...
812             elsif ($arg =~ /^-menu$/) {
813 1 50       5 _opt_err('Missing', '-menu', 'menu specification') if !@_;
814 1 50       6 $option{-menu} = ref $_[0] ? shift(@_) : \shift(@_);
815 1         3 $option{-prompt} .= $MENU_MK;
816 1         2 $option{-def_nocheck} = 1;
817             }
818              
819             # A monitoring sub is called on every input character...
820             elsif ($arg =~ /^-monitor/) {
821 0 0 0     0 _opt_err('Missing', '-monitor', 'a monitor subref')
822             if !@_ || ref $_[0] ne 'CODE';
823 0         0 $option{-monitor} = shift(@_);
824             }
825              
826             # Anything else of the form '-...' is a misspelt option...
827 2         8 elsif ($arg =~ /^-\w+$/) { _warn(misc => "prompt(): Unknown option $arg ignored"); }
828              
829             # Anything else is part fo the prompt...
830 96         288 else { $option{-prompt} .= $arg; }
831              
832             # Handle option bundling...
833 218 100 100     2380 redo DECODING if $redo && $arg =~ s{\A -.{$redo} (?=.)}{-}xms;
834             }
835             }
836              
837             # Precompute top-level menu, if menuing...
838 97 100       270 if (exists $option{-menu}) {
839             $option{-menu} = _build_menu($option{-menu},
840             undef,
841             $option{-number}||$option{-integer}
842 1   33     6 );
843             }
844              
845             # Handle return magic on -single...
846 97 100 50     329 if (defined $option{-single} && length($option{-echo}('X')//'echoself')) {
      66        
847 6   50 0   28 $option{-return} //= sub{ "\n" };
  0         0  
848             }
849              
850             # Adjust prompt as necessary...
851 97 100       614 if ($option{-argv}) {
    100          
    100          
    50          
852 1   33     4 my $progname = $option{-prompt} // $0;
853 1         2 $progname =~ s{^.*/}{}xms;
854              
855 1         3 my $HINT = '[enter command line args here]';
856 1         5 $option{-prompt} = "> $progname $HINT\r> $progname ";
857              
858 1         2 $option{-complete} = 'filenames';
859              
860 1         2 my $not_first;
861             $option{-echo} = sub{
862 0     0   0 my $char = shift;
863 0         0 $option{-prompt} = "> $progname "; # Sneaky resetting to handle completions
864 0 0       0 return $char if $not_first++;
865 0         0 return "\r> $progname " . (q{ } x length $HINT) . "\r> $progname $char";
866             }
867 1         18 }
868             elsif (!defined $option{-prompt}) {
869 3         15 $option{-prompt} = '> ';
870             }
871             elsif ($option{-prompt} =~ m{ \S \z}xms) {
872             # If prompt doesn't end in whitespace, make it so...
873 79         220 $option{-prompt} .= ' ';
874             }
875             elsif ($option{-prompt} =~ m{ (.*) \n \z}xms) {
876             # If prompt ends in a newline, remove it...
877 0         0 $option{-prompt} = $1;
878             }
879              
880             # Steal history set name if -h given without a specification...
881 97   33     285 $option{-history} //= $option{-prompt};
882              
883             # Verify any default satisfies any constraints...
884 97 100 100     301 if (exists $option{-def} && !$option{-def_nocheck}) {
885 6 50       21 if (!_verify_input_constraints(\q{},undef,undef,\%option)) {
886 0         0 _warn( misc =>
887             'prompt(): -default value does not satisfy -must constraints'
888             );
889             }
890             }
891              
892 97         254 return \%option;
893             }
894              
895             #====[ Error Handlers ]=========================================
896              
897             sub _opt_err {
898 2     2   7 my ($problem, $option, $expectation, $found) = @_;
899 2 100       6 if (@_ > 3) {
900 1         8 Carp::croak "prompt(): $problem value for $option (expected $expectation, but found $found)";
901             }
902             else {
903 1         8 Carp::croak "prompt(): $problem value for $option (expected $expectation)";
904             }
905             }
906              
907             sub _warn {
908 5     5   46 my ($category, @message) = @_;
909              
910 5 100       124 return if !warnings::enabled($category);
911              
912 3         526 my $message = join(q{},@message);
913 3 100       99 warn $message =~ /\n$/ ? $message : Carp::shortmess($message);
914             }
915              
916              
917             #====[ Utility subroutines ]====================================
918              
919             # Return the *ARGV filehandle, "magic-opening" it if necessary...
920             sub _open_ARGV {
921 80 50   80   398 if (!openhandle \*ARGV) {
922 0   0     0 $ARGV = shift @ARGV // '-';
923 0 0       0 open *ARGV or Carp::croak(qq{prompt(): Can't open *ARGV: $!});
924             }
925 80         252 return \*ARGV;
926             }
927              
928             my $INTEGER_PAT = qr{ \A \s*+ [+-]?+ \d++ (?: [Ee] \+? \d+ )? \s*+ \Z }xms;
929              
930             my $NUMBER_PAT = qr{
931             \A \s*+ [+-]?+
932             (?:
933             \d++ (?: [.,] \d*+ )?
934             | [.,] \d++
935             )
936             (?: [eE] [+-]?+ \d++ )?
937             \s*+ \Z
938             }xms;
939              
940             # Verify interactive constraints...
941             sub _verify_input_constraints {
942 121     121   329 my ($input_ref, $local_fake_input_ref, $outputter_ref, $opt_ref, $extras)
943             = @_;
944              
945             # Use default if appropriate (but short-circuit checks if -DEFAULT set)...
946 121         193 my $input = ${$input_ref};
  121         269  
947 121 50 66     211 if (${$input_ref} =~ m{^\R?$}xms && exists $opt_ref->{-def}) {
  121         709  
948 11 100       30 return 1 if $opt_ref->{-def_nocheck};
949             $input = $opt_ref->{-def}
950 10         20 }
951 120         279 chomp $input;
952              
953 120         187 my $failed;
954             # Integer constraint is hard-coded...
955 120 100 100     511 if ($opt_ref->{-integer} && $input !~ $INTEGER_PAT) {
956 4         12 $failed = $opt_ref->{-prompt} . "(must be an integer) ";
957             }
958              
959             # Numeric constraint is hard-coded...
960 120 100 100     678 if (!$failed && $opt_ref->{-number} && $input !~ $NUMBER_PAT) {
      100        
961 2         6 $failed = $opt_ref->{-prompt} . "(must be a number) ";
962             }
963              
964             # Sort and clean up -must list...
965 120   50     327 my $must_ref = $opt_ref->{-must} // {};
966 120         186 my @must_keys = sort keys %{$must_ref};
  120         464  
967 120 50       304 my %clean_key_for = map { $_ => (/^\d+[.:]?\s*(.*)/s ? $1 : $_) } @must_keys;
  36         170  
968 120         233 my @must_kv_list = map { $clean_key_for{$_} => $must_ref->{$_} } @must_keys;
  36         93  
969              
970             # Combine -yesno and -must constraints...
971             my %constraint_for = (
972 120   50     510 %{ $extras // {} },
973 120   100     178 %{ $opt_ref->{-yesno}{must} // {} },
  120         611  
974             @must_kv_list,
975             );
976             my @constraints = (
977 120   50     519 keys %{ $extras // {} },
978 120   100     602 keys %{ $opt_ref->{-yesno}{must} // {} },
979 120         359 @clean_key_for{@must_keys},
980             );
981              
982             # User-specified constraints...
983 120 100 100     629 if (!$failed && keys %constraint_for) {
984             CONSTRAINT:
985 62         129 for my $msg (@constraints) {
986 62         91 my $constraint = $constraint_for{$msg};
987 36 100   36   383 next CONSTRAINT if eval { no warnings; local $_ = $input; match($input, $constraint); };
  36         79  
  36         8848  
  62         100  
  62         116  
  62         188  
988             $failed = $msg =~ m{\A [[:upper:]] }xms ? "$msg "
989             : $msg =~ m{\A \W }xms ? $opt_ref->{-prompt}
990             . "$msg "
991             : $opt_ref->{-prompt}
992 22 100       2088 . "(must $msg) "
    100          
993             ;
994 22         54 last CONSTRAINT;
995             }
996             }
997              
998             # If any constraint not satisfied...
999 120 100       4161 if ($failed) {
1000             # Return failure if not actually prompting at the moment...
1001 28 50       74 return 0 if !$outputter_ref;
1002              
1003             # Redraw post-menu prompt with failure message appended...
1004 28         180 $failed =~ s{.*$MENU_MK}{}xms;
1005 28         73 $outputter_ref->(-style => _wipe_line(), $failed);
1006              
1007             # Reset input collector...
1008 28         50 ${$input_ref} = q{};
  28         52  
1009              
1010             # Reset faked input, if any...
1011 28 50 33     89 if (defined $fake_input && length($fake_input) > 0) {
1012 0         0 $fake_input =~ s{ \A (.*) \R? }{}xm;
1013 0         0 ${$local_fake_input_ref} = $1;
  0         0  
1014             }
1015              
1016 36     36   317 no warnings 'exiting';
  36         77  
  36         27050  
1017 28         135 next INPUT;
1018             }
1019              
1020             # Otherwise succeed...
1021 92         409 return 1;
1022             }
1023              
1024             # Build a sub to read from specified filehandle, with or without timeout...
1025             sub _generate_buffered_reader_from {
1026 97     97   227 my ($in_fh, $outputter_ref, $opt_ref) = @_;
1027              
1028             # Set-up for timeouts...
1029 97   50     410 my $fileno = fileno($in_fh) // -1;
1030 97   66     430 my $has_timeout = exists $opt_ref->{-timeout} && $fileno >= 0;
1031 97         179 my $timeout = $opt_ref->{-timeout};
1032 97         228 my $readbits = q{};
1033 97 50 33     333 if ($has_timeout && $fileno >= 0) {
1034 0         0 vec($readbits,$fileno,1) = 1;
1035             }
1036              
1037             # Set up local faked input, if any...
1038 97         256 my $local_fake_input;
1039             my $orig_fake_input;
1040 97 100 66     580 if (defined $fake_input && length($fake_input) > 0) {
1041 4         50 $fake_input =~ s{ \A (.*) \R? }{}xm;
1042 4         22 $orig_fake_input = $local_fake_input = $1;
1043             }
1044              
1045             return sub {
1046 97     97   220 my ($extra_constraints) = @_;
1047              
1048             INPUT:
1049 97         192 while (1) {
1050 125 50 33     476 if (!$has_timeout || select $readbits, undef, undef, $timeout) {
1051 125         205 my $input;
1052              
1053             # Real input comes from real filehandles...
1054 125 100       283 if (!defined $local_fake_input) {
1055 121         492 $input = readline $in_fh;
1056             }
1057             # Fake input has to be typed...
1058             else {
1059 4         9 $input = $local_fake_input;
1060 4         4000845 sleep 1;
1061 4         237 for ($local_fake_input =~ m/\X/g) {
1062 24         137 _simulate_typing();
1063 24         542 $outputter_ref->(-echostyle => $opt_ref->{-echo}($_));
1064             }
1065 4         61 readline $in_fh;
1066              
1067             # Check for simulated EOF...
1068 4 50       126 if ($input =~ m{^ \s* (?: \cD | \cZ ) }xms) {
1069 0         0 $input = undef;
1070             }
1071             }
1072              
1073 125 50       389 if (exists $opt_ref->{-cancel}) {
1074 0         0 for my $nextchar (split q{}, $input) {
1075             die bless \$input, 'IO::Prompter::Cancellation'
1076 0 0       0 if match($nextchar, $opt_ref->{-cancel});
1077             }
1078             }
1079              
1080 125 100       338 if (defined $input) {
1081 115         451 _verify_input_constraints(
1082             \$input, \$local_fake_input, $outputter_ref, $opt_ref, $extra_constraints
1083             );
1084             }
1085              
1086             return defined $input && $opt_ref->{-single}
1087 97 100 100     611 ? substr($input, 0, 1)
1088             : $input;
1089             }
1090             else {
1091 0         0 return;
1092             }
1093             }
1094             }
1095 97         990 }
1096              
1097             sub _autoflush {
1098 0     0   0 my ($fh) = @_;
1099 0         0 my $prev_selected = select $fh;
1100 0         0 $| = 1;
1101 0         0 select $prev_selected;
1102 0         0 return;
1103             }
1104              
1105             sub _simulate_typing {
1106 24     24   65 state $TYPING_SPEED = 0.07; # seconds per character
1107 24         920634 select undef, undef, undef, rand $TYPING_SPEED;
1108             }
1109              
1110             sub _term_width {
1111 36     36   340 my ($term_width) = eval { no warnings 'deprecated'; Term::ReadKey::GetTerminalSize(\*STDERR) };
  36     28   109  
  36         36874  
  28         45  
  28         323  
1112 28   33     265 return $term_width // $DEFAULT_TERM_WIDTH;
1113             }
1114              
1115             sub _wipe_line {
1116 28     28   65 return qq{\r} . q{ } x (_term_width()-1) . qq{\r};
1117             }
1118              
1119             # Convert a specification into a list of possible completions...
1120             sub _current_completions_for {
1121 0     0   0 my ($input_text, $opt_ref) = @_;
1122 0         0 my $completer = $opt_ref->{-complete};
1123              
1124             # Isolate the final whitespace-separated word...
1125 0         0 my ($prefix, $lastword)
1126             = $input_text =~ m{
1127             (?| ^ (.*\s+) (.*)
1128             | ^ () (.*)
1129             )
1130             }xms;
1131              
1132             # Find candidates...
1133 0         0 my @candidates;
1134 0   0     0 for my $completer_type (ref($completer) || $completer // q{}) {
      0        
1135             # If completer is sub, recursively call it with input words...
1136 0 0       0 if ($completer_type eq 'CODE') {
    0          
    0          
    0          
    0          
1137             ($prefix, @candidates)
1138             = _current_completions_for(
1139             $input_text,
1140 0         0 { %{$opt_ref},
  0         0  
1141             -complete => $completer->(split /\s+/, $input_text, -1)
1142             }
1143             );
1144             }
1145              
1146             # If completer is array, grep the appropriate elements...
1147             elsif ($completer_type eq 'ARRAY') {
1148 0         0 @candidates = grep { /\A\Q$lastword\E/ } @{$completer};
  0         0  
  0         0  
1149             }
1150              
1151             # If completer is hash, grep the appropriate keys...
1152             elsif ($completer_type eq 'HASH') {
1153 0         0 @candidates = grep { /\A\Q$lastword\E/ } keys %{$completer};
  0         0  
  0         0  
1154             }
1155              
1156             # If completer is 'file...', glob up the appropriate filenames...
1157             elsif ($completer_type eq /^file\w*$/) {
1158 0         0 @candidates = glob($lastword.'*');
1159             }
1160              
1161             # If completer is 'dir...', glob up the appropriate directories...
1162             elsif ($completer_type eq /^dir\w*$/) {
1163 0         0 @candidates = grep {-d} glob($lastword.'*');
  0         0  
1164             }
1165             }
1166              
1167 0         0 chomp @candidates;
1168 0         0 return ($prefix, @candidates);
1169             }
1170              
1171              
1172             sub _current_history_for {
1173 0     0   0 my ($prefix, $opt_ref) = @_;
1174              
1175 0         0 my $prefix_len = length($prefix);
1176 0 0       0 return q{}, map { /\A (.*?) \R \Z/x ? $1 : $_ }
1177 0         0 grep { substr($_,0,$prefix_len) eq $prefix }
1178 0         0 @{ $history_cache{$opt_ref->{-history}} };
  0         0  
1179             }
1180              
1181             sub _longest_common_prefix_for {
1182 0     0   0 my $prefix = shift @_;
1183 0         0 for my $comparison (@_) {
1184 0         0 ($comparison ^ $prefix) =~ m{ \A (\0*) }xms;
1185 0         0 my $common_length = length($1);
1186 0 0       0 return q{} if !$common_length;
1187 0         0 $prefix = substr($prefix, 0, $common_length);
1188             }
1189 0         0 return $prefix;
1190             }
1191              
1192             sub _display_completions {
1193 0     0   0 my ($input, @candidates) = @_;
1194              
1195 0 0       0 return q{} if @candidates <= 1;
1196              
1197             # How big is each field in the table?
1198 0         0 my $field_width
1199             = _term_width() / $COMPLETE_DISPLAY_FIELDS - $COMPLETE_DISPLAY_GAP;
1200              
1201             # Crop the possibilities intelligently to that width...
1202 0         0 for my $candidate (@candidates) {
1203 0         0 substr($candidate, 0, length($input)) =~ s{ \A .* [/\\] }{}xms;
1204 0         0 $candidate
1205             = sprintf "%-*s", $field_width, substr($candidate,0,$field_width);
1206             }
1207              
1208             # Collect them into rows...
1209 0         0 my $display = "\n";
1210 0         0 my $gap = q{ } x $COMPLETE_DISPLAY_GAP;
1211 0         0 while (@candidates) {
1212 0         0 $display .= $gap
1213             . join($gap, splice(@candidates, 0, $COMPLETE_DISPLAY_FIELDS))
1214             . "\n";
1215             }
1216              
1217 0         0 return $display;
1218             }
1219              
1220             sub _generate_unbuffered_reader_from {
1221 97     97   255 my ($in_fh, $outputter_ref, $opt_ref) = @_;
1222              
1223 36     36   346 my $has_readkey = eval { no warnings 'deprecated'; require Term::ReadKey };
  36         91  
  36         13730  
  97         179  
  97         14087  
1224              
1225             # If no per-character reads, fall back on buffered input...
1226 97 50 33     723 if (!-t $in_fh || !$has_readkey) {
1227 97         357 return _generate_buffered_reader_from($in_fh, $outputter_ref, $opt_ref);
1228             }
1229              
1230             # Adapt to local control characters...
1231 0         0 my %ctrl = eval { Term::ReadKey::GetControlChars($in_fh) };
  0         0  
1232 0         0 delete $ctrl{$_} for grep { $ctrl{$_} eq "\cA" } keys %ctrl;
  0         0  
1233              
1234 0   0     0 $ctrl{EOF} //= "\4";
1235 0   0     0 $ctrl{INTERRUPT} //= "\3";
1236 0 0 0     0 $ctrl{ERASE} //= $^O eq 'MSWin32' ? "\10" : "0177";
1237              
1238 0         0 my $ctrl = join '|', values %ctrl;
1239              
1240 0   0     0 my $VERBATIM_KEY = $ctrl{QUOTENEXT} // $DEFAULT_VERBATIM_KEY;
1241              
1242             # Translate timeout for ReadKey (with 32-bit MAXINT workaround for Windows)...
1243             my $timeout = !defined $opt_ref->{-timeout} ? 0x7FFFFFFF # 68 years
1244             : $opt_ref->{-timeout} == 0 ? -1
1245             : $opt_ref->{-timeout}
1246 0 0       0 ;
    0          
1247              
1248             return sub {
1249 0     0   0 my ($extra_constraints) = @_;
1250              
1251             # Short-circuit on unreadable filehandle...
1252 0 0       0 return if !openhandle($in_fh);
1253              
1254             # Set up direct reading, and prepare to clean up on abnormal exit...
1255 0         0 Term::ReadKey::ReadMode('raw', $in_fh);
1256 0         0 my $prev_SIGINT = $SIG{INT};
1257 0 0       0 local $SIG{INT} = sub { return if $prev_SIGINT eq 'IGNORE';
1258 0         0 Term::ReadKey::ReadMode('restore', $in_fh);
1259 0 0 0     0 exit(1) if !defined $prev_SIGINT
1260             || $prev_SIGINT eq 'DEFAULT';
1261             {
1262 0         0 package main;
1263 36     36   318 no strict 'refs';
  36         87  
  36         154030  
1264 0         0 $prev_SIGINT->()
1265             }
1266 0         0 };
1267              
1268             # Set up local faked input, if any...
1269 0         0 my $local_fake_input;
1270             my $orig_fake_input;
1271 0 0 0     0 if (defined $fake_input && length($fake_input) > 0) {
1272 0         0 $fake_input =~ s{ \A (.*) \R? }{}xm;
1273 0         0 $orig_fake_input = $local_fake_input = $1;
1274             }
1275              
1276 0 0       0 my $input = exists $opt_ref->{-prefill} ? $opt_ref->{-prefill} : q{};
1277 0 0       0 if (exists $opt_ref->{-prefill}) {
1278 0 0       0 if (exists $opt_ref->{-monitor}) {
1279             my %opts = ( -cursor_pos => length($input),
1280             -prompt => $opt_ref->{-prompt},
1281             -style => $opt_ref->{-style}->(),
1282 0         0 -echostyle => $opt_ref->{-echostyle}->(),
1283             );
1284 0         0 my $input_copy = $input;
1285 0         0 eval { $opt_ref->{-monitor}->($input_copy, \%opts) };
  0         0  
1286             }
1287 0         0 $outputter_ref->( -style => $opt_ref->{-style}, _wipe_line(), $opt_ref->{-prompt});
1288 0         0 $outputter_ref->( -echostyle => join(q{}, map { $opt_ref->{-echo}($_) } $input =~ m/\X/g) );
  0         0  
1289             }
1290              
1291 0         0 my $insert_offset = 0;
1292             INPUT:
1293 0         0 while (1) {
1294 0         0 state $prev_was_verbatim = 0;
1295 0         0 state $completion_level = 0;
1296 0         0 state $completion_type = q{};
1297              
1298             # Get next character entered...
1299 0         0 my $next = Term::ReadKey::ReadKey($timeout, $in_fh);
1300              
1301             # Check for cancellation...
1302 0 0 0     0 if (exists $opt_ref->{-cancel} && match($next, $opt_ref->{-cancel})) {
1303 0         0 Term::ReadKey::ReadMode('restore', $in_fh);
1304 0         0 die bless \$input, 'IO::Prompter::Cancellation';
1305             }
1306              
1307             # Finished with completion mode?
1308 0 0 0     0 if (($next//q{}) !~ m{ $COMPLETE_INIT | $COMPLETE_CYCLE }xms) {
1309 0         0 $completion_level = 0;
1310 0         0 $completion_type = q{};
1311             }
1312              
1313             # Are we faking input?
1314 0         0 my $faking = defined $local_fake_input;
1315              
1316             # If not EOF...
1317 0 0       0 if (defined $next) {
1318             # Remember where we were parked...
1319 0         0 my $prev_insert_offset = $insert_offset;
1320              
1321             # Handle interrupts...
1322 0 0 0     0 if ($next eq $ctrl{INTERRUPT}) {
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
      0        
1323 0         0 $SIG{INT}();
1324 0         0 next INPUT;
1325             }
1326              
1327             # Handle verbatim quoter...
1328             elsif (!$prev_was_verbatim && $next eq $VERBATIM_KEY) {
1329 0         0 $prev_was_verbatim = 1;
1330 0         0 next INPUT;
1331             }
1332              
1333             # Handle completions...
1334             elsif (!$prev_was_verbatim
1335             && ( $next =~ $COMPLETE_INIT
1336             || $completion_level > 0 && $next =~ $COMPLETE_CYCLE
1337             )
1338             ) {
1339 0         0 state @completion_list; # ...all candidates for completion
1340 0         0 state @completion_ring; # ..."next" candidate cycle
1341 0         0 state $completion_ring_first; # ...special case first time
1342 0         0 state $completion_prefix; # ...skipped before completing
1343              
1344             # Track completion type and level (switch if necessary)...
1345 0 0 0     0 if ($next =~ $COMPLETE_INIT && index($completion_type, $next) < 0) {
1346 0 0       0 $completion_type = index($COMPLETE_KEY, $next) >= 0 ? $COMPLETE_KEY : $COMPLETE_HIST;
1347 0         0 $completion_level = 1;
1348             }
1349             else {
1350 0         0 $completion_level++;
1351             }
1352              
1353             # If starting completion, cache completions...
1354 0 0       0 if ($completion_level == 1) {
1355 0 0       0 ($completion_prefix, @completion_list)
1356             = index($COMPLETE_KEY, $next) >= 0
1357             ? _current_completions_for($input, $opt_ref)
1358             : _current_history_for($input, $opt_ref);
1359 0         0 @completion_ring = (@completion_list, q{});
1360 0         0 $completion_ring_first = 1;
1361             }
1362              
1363             # Can only complete if there are completions to be had...
1364 0 0       0 if (@completion_list) {
1365             # Select the appropriate mode...
1366             my $mode = $COMPLETE_MODE{$completion_type}[$completion_level-1]
1367 0   0     0 // $COMPLETE_MODE{$completion_type}[-1];
1368              
1369             # 'longest mode' finds longest consistent prefix...
1370 0 0       0 if ($mode =~ /longest/) {
    0          
1371 0         0 $input
1372             = $completion_prefix
1373             . _longest_common_prefix_for(@completion_list);
1374             }
1375             # 'full mode' suggests next full match...
1376             elsif ($mode =~ /full/) {
1377 0 0       0 if (!$completion_ring_first) {
1378 0 0       0 if ($next eq $COMPLETE_PREV) {
1379 0         0 unshift @completion_ring,
1380             pop @completion_ring;
1381             }
1382             else {
1383 0         0 push @completion_ring,
1384             shift @completion_ring;
1385             }
1386             }
1387 0         0 $input = $completion_prefix . $completion_ring[0];
1388 0         0 $completion_ring_first = 0;
1389             }
1390             # 'list mode' lists all possibilities...
1391 0 0       0 my $list_display = $mode =~ /list/
1392             ? _display_completions($input, @completion_list)
1393             : q{};
1394              
1395             # Update prompt with selected completion...
1396             $outputter_ref->( -style =>
1397             $list_display,
1398             _wipe_line(),
1399 0         0 $opt_ref->{-prompt}, $input
1400             );
1401              
1402             # If last completion was unique choice, completed...
1403 0 0       0 if (@completion_list <= 1) {
1404 0         0 $completion_level = 0;
1405             }
1406             }
1407 0         0 next INPUT;
1408             }
1409              
1410             # Handle erasures (including pushbacks if faking)...
1411             elsif (!$prev_was_verbatim && $next eq $ctrl{ERASE}) {
1412 0 0       0 if (!length $input) {
    0          
1413             # Do nothing...
1414             }
1415             elsif ($insert_offset) {
1416             # Can't erase past start of input...
1417 0 0       0 next INPUT if $insert_offset >= length($input);
1418              
1419             # Erase character just before cursor...
1420 0         0 substr($input, -$insert_offset-1, 1, q{});
1421              
1422             # Redraw...
1423 0         0 my $input_pre = substr($input.' ',0,length($input)-$insert_offset+1);
1424 0         0 my $input_post = substr($input.' ',length($input)-$insert_offset);
1425             my $display_pre
1426 0         0 = join q{}, map { $opt_ref->{-echo}($_) } $input_pre =~ m/\X/g;
  0         0  
1427             my $display_post
1428 0         0 = join q{}, map { $opt_ref->{-echo}($_) } $input_post =~ m/\X/g;
  0         0  
1429             $outputter_ref->( -echostyle =>
1430             "\b" x length($display_pre)
1431 0         0 . join(q{}, map { $opt_ref->{-echo}($_) } $input =~ m/\X/g)
1432 0         0 . q{ } x length($opt_ref->{-echo}(q{ }))
1433             . "\b" x length($display_post)
1434             );
1435             }
1436             else {
1437 0         0 my $erased = substr($input, -1, 1, q{});
1438 0 0       0 if ($faking) {
1439 0         0 substr($local_fake_input,0,0,$erased);
1440             }
1441             $outputter_ref->( -nostyle =>
1442 0   0     0 map { $_ x (length($opt_ref->{-echo}($_)//'X')) }
  0         0  
1443             "\b", ' ', "\b"
1444             );
1445             }
1446 0         0 next INPUT;
1447             }
1448              
1449             # Handle EOF (including cancelling any remaining fake input)...
1450             elsif (!$prev_was_verbatim && $next eq $ctrl{EOF}) {
1451 0         0 Term::ReadKey::ReadMode('restore', $in_fh);
1452 0         0 close $in_fh;
1453 0         0 undef $fake_input;
1454 0 0       0 return length($input) ? $input : undef;
1455             }
1456              
1457             # Handle escape from faking...
1458             elsif (!$prev_was_verbatim && $faking && $next eq $FAKE_ESC) {
1459 0         0 my $lookahead = Term::ReadKey::ReadKey(0, $in_fh);
1460              
1461             # Two implies the current faked line is deferred...
1462 0 0       0 if ($lookahead eq $FAKE_ESC) {
1463 0         0 $fake_input =~ s{ \A }{$orig_fake_input\n}xm;
1464             }
1465             # Only one implies the current faked line is replaced...
1466             else {
1467 0         0 $in_fh->ungetc(ord($lookahead));
1468             }
1469 0         0 undef $local_fake_input;
1470 0         0 $faking = 0;
1471 0         0 next INPUT;
1472             }
1473              
1474             # Handle returns...
1475             elsif (!$prev_was_verbatim && $next =~ /\A\R\z/) {
1476             # Complete faked line, if faked input incomplete...
1477 0 0 0     0 if ($faking && length($local_fake_input)) {
1478 0         0 for ($local_fake_input =~ m/\X/g) {
1479 0         0 _simulate_typing();
1480 0         0 $outputter_ref->(-echostyle => $opt_ref->{-echo}($_));
1481             }
1482 0         0 $input .= $local_fake_input;
1483             }
1484              
1485             # Add newline to the accumulated input string...
1486 0         0 $input .= $next;
1487              
1488             # Check that input satisfied any constraints...
1489 0         0 _verify_input_constraints(
1490             \$input, \$local_fake_input, $outputter_ref,
1491             $opt_ref, $extra_constraints,
1492             );
1493              
1494             # Echo a default value if appropriate...
1495 0 0 0     0 if ($input =~ m{\A\R?\Z}xms && defined $opt_ref->{-def}) {
1496 0         0 my $def_val = $opt_ref->{-def};
1497              
1498             # Try to find the key, for a menu...
1499 0 0       0 if (exists $opt_ref->{-menu_curr_level}) {
1500 0         0 for my $key ( keys %{$opt_ref->{-menu_curr_level}}) {
  0         0  
1501 0 0       0 if (match($def_val, $opt_ref->{-menu_curr_level}{$key})) {
1502 0         0 $def_val = $key;
1503 0         0 last;
1504             }
1505             }
1506             }
1507              
1508             # Echo it as if it had been typed...
1509 0         0 $outputter_ref->(-echostyle => $opt_ref->{-echo}($def_val));
1510             }
1511              
1512             # Echo the return (or otherwise, as specified)...
1513 0         0 $outputter_ref->(-echostyle => $opt_ref->{-return}($next));
1514              
1515             # Clean up, and return the input...
1516 0         0 Term::ReadKey::ReadMode('restore', $in_fh);
1517              
1518             # Handle fake EOF...
1519 0 0 0     0 if ($faking && $input =~ m{^ (?: \cD | \cZ) }xms) {
1520 0         0 return undef;
1521             }
1522              
1523 0         0 return $input;
1524             }
1525              
1526             # Handle anything else...
1527             elsif ($prev_was_verbatim || $next !~ /$ctrl/) {
1528             # If so, get the next fake character...
1529 0 0       0 if ($faking) {
1530 0 0       0 $next = length($local_fake_input)
1531             ? substr($local_fake_input,0,1,q{})
1532             : q{};
1533             }
1534              
1535             # Handle editing...
1536 0 0       0 if ($next eq $EDIT{BACK}) {
    0          
    0          
    0          
1537 0 0       0 $insert_offset += ($insert_offset < length $input) ? 1 : 0;
1538             }
1539             elsif ($next eq $EDIT{FORWARD}) {
1540 0 0       0 $insert_offset += ($insert_offset > 0) ? -1 : 0;
1541             }
1542             elsif ($next eq $EDIT{START}) {
1543 0         0 $insert_offset = length($input);
1544             }
1545             elsif ($next eq $EDIT{END}) {
1546 0         0 $insert_offset = 0;
1547             }
1548              
1549             # Handle non-editing...
1550             else {
1551             # Check for input restrictions...
1552 0 0       0 if (exists $opt_ref->{-guarantee}) {
1553 0 0       0 next INPUT if ($input.$next) !~ $opt_ref->{-guarantee};
1554             }
1555              
1556             # Add the new input char to the accumulated input string...
1557 0 0       0 if ($insert_offset) {
1558 0         0 substr($input, -$insert_offset, 0) = $next;
1559 0         0 $prev_insert_offset++;
1560             }
1561             else {
1562 0         0 $input .= $next;
1563             }
1564             }
1565              
1566             # Display the character (or whatever was specified)...
1567              
1568 0 0 0     0 if ($insert_offset || $prev_insert_offset) {
    0          
1569 0         0 my $input_pre = substr($input,0,length($input)-$prev_insert_offset);
1570 0         0 my $input_post = substr($input,length($input)-$insert_offset);
1571             my $display_pre
1572 0         0 = join q{}, map { $opt_ref->{-echo}($_) } $input_pre =~ m/\X/g;
  0         0  
1573             my $display_post
1574 0         0 = join q{}, map { $opt_ref->{-echo}($_) } $input_post =~ m/\X/g;
  0         0  
1575             $outputter_ref->( -echostyle =>
1576             "\b" x length($display_pre)
1577 0         0 . join(q{}, map { $opt_ref->{-echo}($_) } $input =~ m/\X/g)
  0         0  
1578             . "\b" x length($display_post)
1579             );
1580             }
1581             elsif ($next !~ $EDIT_KEY) {
1582 0         0 $outputter_ref->(-echostyle => $opt_ref->{-echo}($next));
1583             }
1584              
1585             # Not verbatim after this...
1586 0         0 $prev_was_verbatim = 0;
1587             }
1588             else {
1589             # Not verbatim after mysterious ctrl input...
1590 0         0 $prev_was_verbatim = 0;
1591              
1592 0         0 next INPUT;
1593             }
1594             }
1595              
1596 0 0 0     0 if ($opt_ref->{-single} || !defined $next || $input =~ m{\Q$/\E$}) {
      0        
1597             # Did we get an acceptable value?
1598 0 0       0 if (defined $next) {
1599 0         0 _verify_input_constraints(
1600             \$input, \$local_fake_input, $outputter_ref,
1601             $opt_ref, $extra_constraints,
1602             );
1603             }
1604              
1605             # Reset terminal...
1606 0         0 Term::ReadKey::ReadMode('restore', $in_fh);
1607              
1608             # Return failure if failed before input or cancelled...
1609 0 0 0     0 if (!defined $next && length($input) == 0
      0        
      0        
1610             || exists $opt_ref->{-cancel} && match($next, $opt_ref->{-cancel})) {
1611 0 0       0 return if $opt_ref->{-verbatim};
1612 0         0 return PUREBOOL { 0 }
1613 0         0 BOOL { 0 }
1614 0         0 SCALAR { undef }
1615 0         0 METHOD { defaulted => sub { 0 }, timedout => sub { 0 } };
  0         0  
  0         0  
  0         0  
1616             }
1617              
1618             # Otherwise supply a final newline if necessary...
1619 0 0 0     0 if ( $opt_ref->{-single}
      0        
1620             && exists $opt_ref->{-return}
1621             && $input !~ /\A\R\z/ ) {
1622 0         0 $outputter_ref->(-echostyle => $opt_ref->{-return}(q{}));
1623             }
1624              
1625 0         0 return $input;
1626             }
1627             }
1628             continue {
1629             # Perform monitor (if any) and redraw prompt (if required)...
1630 0 0       0 if ($opt_ref->{-monitor}) {
1631             my %opts = ( -cursor_pos => length($input) - $insert_offset,
1632             -prompt => $opt_ref->{-prompt},
1633             -style => $opt_ref->{-style}->(),
1634 0         0 -echostyle => $opt_ref->{-echostyle}->(),
1635             );
1636 0         0 my $input_copy = $input;
1637 0         0 my $output_pos = $outputter_ref->(-tell);
1638 0 0 0     0 if (!defined eval { $opt_ref->{-monitor}->($input_copy, \%opts) }
  0         0  
1639             || $output_pos != $outputter_ref->(-tell)) {
1640 0         0 my $input_pre = substr($input.' ',0,length($input)-$insert_offset+1);
1641 0         0 my $input_post = substr($input.' ',length($input)-$insert_offset);
1642             my $display_pre
1643 0         0 = join q{}, map { $opt_ref->{-echo}($_) } $input_pre =~ m/\X/g;
  0         0  
1644             my $display_post
1645 0         0 = join q{}, map { $opt_ref->{-echo}($_) } $input_post =~ m/\X/g;
  0         0  
1646 0         0 $outputter_ref->( -style => $opt_ref->{-style}, _wipe_line(), $opt_ref->{-prompt});
1647             $outputter_ref->( -echostyle =>
1648 0         0 join(q{}, map { $opt_ref->{-echo}($_) } $input =~ m/\X/g)
  0         0  
1649             . "\b" x (length($display_post)-1)
1650             );
1651             }
1652             }
1653             }
1654             }
1655 0         0 }
1656              
1657             # Build a menu...
1658             sub _build_menu {
1659 1     1   4 my ($source_ref, $initial_prompt, $is_numeric) = @_;
1660 1   50     8 my $prompt = ($initial_prompt//q{}) . qq{\n};
1661 1         2 my $final = q{};
1662 1         3 my %value_for;
1663             my %key_for;
1664 1         0 my @selectors;
1665              
1666 1         3 my $source_type = ref $source_ref;
1667 1 50       6 if ($source_type eq 'HASH') {
    50          
1668 0         0 my @sorted_keys = sort(keys(%{$source_ref}));
  0         0  
1669 0 0       0 @selectors = $is_numeric ? (1..@sorted_keys) : ('a'..'z','A'..'Z');
1670 0         0 @key_for{@selectors} = @sorted_keys;
1671 0         0 @value_for{@selectors} = @{$source_ref}{@sorted_keys};
  0         0  
1672 0         0 $source_ref = \@sorted_keys;
1673             }
1674             elsif ($source_type eq 'SCALAR') {
1675 0         0 $source_ref = [ split "\n", ${$source_ref} ];
  0         0  
1676             }
1677              
1678 1         2 my @source = @{$source_ref};
  1         4  
1679 1 50       5 @selectors = $is_numeric ? (1..@source) : ('a'..'z','A'..'Z');
1680 1 50       5 if (!keys %value_for) {
1681 1         10 @value_for{@selectors} = @source;
1682             }
1683              
1684             ITEM:
1685 1         4 for my $tag (@selectors) {
1686 10   50     21 my $item = shift(@source) // last ITEM;
1687 10         13 chomp $item;
1688 10         24 $prompt .= sprintf("%4s. $item\n", $tag);
1689 10         15 $final = $tag;
1690             }
1691              
1692 1 50       4 if (@source) {
1693 0         0 _warn( misc =>
1694             "prompt(): Too many menu items. Ignoring the final " . @source
1695             );
1696             }
1697              
1698 1 0       7 my $constraint = $is_numeric ? '(?:' . join('|',@selectors) .')'
    50          
1699             : $final =~ /[A-Z]/ ? "[a-zA-$final]"
1700             : "[a-$final]";
1701 1 50       4 my $constraint_desc = $is_numeric ? "[1-$selectors[-1]]" : $constraint;
1702 1         3 $constraint = '\A\s*' . $constraint . '\s*\Z';
1703              
1704             return {
1705 1         64 data => $source_ref,
1706             key_for => \%key_for,
1707             value_for => \%value_for,
1708             prompt => "$prompt\n",
1709             is_numeric => $is_numeric,
1710             constraint => { "Enter $constraint_desc: " => qr/$constraint|$MENU_ESC/ },
1711             };
1712             }
1713              
1714             # Vocabulary that _stylize understands...
1715             my %synonyms = (
1716             bold => [qw],
1717             dark => [qw],
1718             faint => [qw],
1719             underline => [qw],
1720             italic => [qw],
1721             blink => [qw],
1722             reverse => [qw],
1723             concealed => [qw],
1724             reset => [qw],
1725             bright_ => [qw< bright\s+ vivid\s+ >],
1726             red => [qw< scarlet vermilion crimson ruby cherry cerise cardinal carmine
1727             burgundy claret chestnut copper garnet geranium russet
1728             salmon titian coral cochineal rose cinnamon ginger gules >],
1729             yellow => [qw< gold golden lemon cadmium daffodil mustard primrose tawny
1730             amber aureate canary champagne citrine citron cream goldenrod honey straw >],
1731             green => [qw< olive jade pea emerald lime chartreuse forest sage vert >],
1732             cyan => [qw< aqua aquamarine teal turquoise ultramarine >],
1733             blue => [qw< azure cerulean cobalt indigo navy sapphire >],
1734             magenta => [qw< amaranthine amethyst lavender lilac mauve mulberry orchid periwinkle
1735             plum pomegranate violet purple aubergine cyclamen fuchsia modena puce
1736             purpure >],
1737             black => [qw< charcoal ebon ebony jet obsidian onyx raven sable slate >],
1738             white => [qw< alabaster ash chalk ivory milk pearl silver argent >],
1739             );
1740              
1741             # Back-mapping to standard terms...
1742             my %normalize
1743             = map { join('|', map { "$_\\b" } reverse sort @{$synonyms{$_}}) => $_ }
1744             keys %synonyms;
1745              
1746             my $BACKGROUND = qr{
1747             (\S+) \s+ (?: behind | beneath | below | under(?:neath)? )\b
1748             | \b (?:upon|over|on) \s+ (?:an?)? \s+ (.*?) \s+ (?:background|bg|field) \b
1749             | \b (?:upon\s+ | over\s+ | (?:(on|upon|over)\s+a\s+)? (?:background|bg|field) \s+ (?:of\s+|in\s+)? | on\s+) (\S+)
1750             }ixms;
1751              
1752             # Convert a description to ANSI colour codes...
1753             sub _stylize {
1754 0   0 0   0 my $spec = shift // q{};
1755              
1756             # Handle arrays and hashes as args...
1757 0 0       0 if (ref($spec) eq 'ARRAY') {
    0          
1758 0         0 $spec = join q{ }, @{$spec};
  0         0  
1759             }
1760             elsif (ref($spec) eq 'HASH') {
1761 0         0 $spec = join q{ }, keys %{$spec};
  0         0  
1762             }
1763              
1764             # Ignore punctuation...
1765 0         0 $spec =~ s/[^\w\s]//g;
1766              
1767             # Handle backgrounds...
1768 0         0 $spec =~ s/$BACKGROUND/on_$+/g;
1769              
1770             # Apply standard translations...
1771 0         0 for my $pattern (keys %normalize) {
1772 0   0     0 $spec =~ s{\b(on_|\b) $pattern}{($1//q{}).$normalize{$pattern}}geixms;
  0         0  
1773             }
1774              
1775             # Ignore anything unknown...
1776 0 0 0     0 $spec =~ s{((?:on_)?(?:(ansi\d+|rgb\d+)|(\S+)))}{ $2 || exists $synonyms{$3} ? $1 : q{} }gxmse;
  0         0  
1777              
1778             # Build ANSI terminal codes around text...
1779 0         0 my $raw_text = join q{}, @_;
1780 0         0 my ($prews, $text, $postws) = $raw_text =~ m{\A (\s*) (.*?) (\s*) \Z}xms;
1781 0         0 my @style = split /\s+/, $spec;
1782 0 0       0 return $prews
1783             . ( @style ? Term::ANSIColor::colored(\@style, $text) : $text )
1784             . $postws;
1785             }
1786              
1787             # Build a subroutine that prints printable chars to the specified filehandle...
1788             sub _std_printer_to {
1789 0     0   0 my ($out_filehandle, $opt_ref) = @_;
1790 36     36   343 no strict 'refs';
  36         78  
  36         14215  
1791 0         0 _autoflush($out_filehandle);
1792 0 0       0 if (eval { require Term::ANSIColor}) {
  0         0  
1793             return sub {
1794 0     0   0 my $style = shift;
1795 0 0       0 return tell($out_filehandle) if $style eq -tell;
1796 0         0 my @loc = (@_);
1797 0         0 s{\e}{^}gxms for @loc;
1798 0         0 print {$out_filehandle} _stylize($opt_ref->{$style}(@loc), @loc);
  0         0  
1799 0         0 };
1800             }
1801             else {
1802             return sub {
1803 0     0   0 my $style = shift;
1804 0 0       0 return tell($out_filehandle) if $style eq -tell;
1805 0         0 my @loc = (@_);
1806 0         0 s{\e}{^}gxms for @loc;
1807 0         0 print {$out_filehandle} @loc;
  0         0  
1808 0         0 };
1809             }
1810             }
1811              
1812             # Build a subroutine that prints to nowhere...
1813             sub _null_printer {
1814 97     149   386 return sub {};
        97      
1815             }
1816              
1817             1; # Magic true value required at end of module
1818             __END__