File Coverage

blib/lib/IO/Prompter.pm
Criterion Covered Total %
statement 424 837 50.6
branch 234 542 43.1
condition 110 304 36.1
subroutine 50 80 62.5
pod 1 1 100.0
total 819 1764 46.4


line stmt bran cond sub pod time code
1 36     36   2983119 use 5.010;
  36         468  
2             package IO::Prompter;
3 36     36   21965 use utf8;
  36         542  
  36         187  
4              
5 36     36   1242 use warnings;
  36         74  
  36         1376  
6 36     36   22646 no if $] >= 5.018000, warnings => 'experimental';
  36         498  
  36         244  
7 36     36   3230 use strict;
  36         88  
  36         741  
8 36     36   180 use Carp;
  36         62  
  36         2458  
9 36     36   33344 use Contextual::Return qw< PUREBOOL BOOL SCALAR METHOD VOID LIST RETOBJ >;
  36         673686  
  36         242  
10 36     36   60964 use Scalar::Util qw< openhandle looks_like_number >;
  36         98  
  36         2031  
11 36     36   16961 use Symbol qw< qualify_to_ref >;
  36         30332  
  36         2331  
12 36     36   16282 use match::smart qw< match >;
  36         212474  
  36         349  
13              
14             our $VERSION = '0.005000';
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   1404 my (undef, $config_data, @other_args) = @_;
86              
87             # Handle -argv requests...
88 42 50 66     408 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         7 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         5 push @lexical_options, [];
101 2         5 $lexical_wrappers[ $#lexical_options ] = $config_data;
102 2         7 $^H{'IO::Prompter::scope_number'} = $#lexical_options;
103 2         4 for my $subname (keys %{$config_data}) {
  2         7  
104 2         5 my @args = @{$config_data->{$subname}};
  2         5  
105 36     36   27276 no strict 'refs';
  36         138  
  36         1601  
106 36     36   278 no warnings 'redefine';
  36         96  
  36         6285  
107 2         7 *{caller().'::'.$subname} = sub {
108 3     3   2732 my $scope_number = (caller 0)[10]{'IO::Prompter::scope_number'};
109 3   50     64 return prompt(@{$lexical_wrappers[$scope_number]{$subname}//[]}, @_);
  3         15  
110 2         8 };
111             }
112             }
113              
114             # Handler faked input specifications...
115             elsif (defined $config_data) {
116 2         7 $fake_input = $config_data;
117             }
118              
119 36     36   331 no strict 'refs';
  36         97  
  36         12386  
120 42         147 *{caller().'::prompt'} = \&prompt;
  42         153  
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 59274 local $\ = '';
127              
128             # Locate any lexical default options...
129 99   100     346 my $hints_hash = (caller 0)[10] // {};
130 99   100     2899 my $scope_num = $hints_hash->{'IO::Prompter::scope_number'} // 0;
131              
132             # Extract and sanitize configuration arguments...
133 99         171 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     381 if VOID && !$opt_ref->{-void};
137              
138             # Set up yesno prompts if required...
139             my @yesno_prompts
140 97 50 100     2879 = ($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     422 my $in_filehandle = $opt_ref->{-in} // _open_ARGV();
144 97   66     636 my $out_filehandle = $opt_ref->{-out} // qualify_to_ref(select);
145 97 50       2943 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       300 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   270 my $in_pos = do { no warnings; tell $in_filehandle } // 0;
  36         80  
  36         35221  
  97         141  
  97         473  
158              
159             # Short-circuit if not valid handles...
160 97 50 33     506 return if !openhandle($in_filehandle) || !openhandle($out_filehandle);
161              
162             # Work out how they're arriving and departing...
163 97 50 33     566 my $outputter_ref = -t $in_filehandle && -t $out_filehandle
164             ? _std_printer_to($out_filehandle, $opt_ref)
165             : _null_printer()
166             ;
167 97         283 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       331 if ($opt_ref->{-wipe}) {
173 0         0 $outputter_ref->(-nostyle => "\n" x 1000);
174             }
175              
176             # Handle menu structures...
177 97         189 my $input;
178             eval {
179             REPROMPT_YESNO:
180 97 100       292 if ($opt_ref->{-menu}) {
181             # Remember top of (possibly nested) menu...
182 1         10 my @menu = ( $opt_ref->{-menu} );
183 1         2 my $top_prompt = $opt_ref->{-prompt};
184 1         21 $top_prompt =~ s{$MENU_MK}{$opt_ref->{-menu}{prompt}}xms;
185 1         20 $menu[-1]{prompt} = $top_prompt;
186              
187             MENU:
188 1         3 while (1) {
189             # Track the current level...
190 1         5 $opt_ref->{-menu_curr_level} = $menu[-1]{value_for};
191              
192             # Show menu and retreive choice...
193 1         6 $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       11 last MENU if !defined $tag;
198 1         6 $tag =~ s{\A\s*(\S*).*}{$1}xms;
199              
200             # Handle by moving up menu stack...
201 1 50       6 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     15 if ($tag =~ /\A\R?\Z/ && exists $opt_ref->{-def}) {
210 1         3 $input = $tag;
211 1         4 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         344 $outputter_ref->(-style => $opt_ref->{-prompt});
231 96         225 $input = $inputter_ref->();
232             }
233 97         312 1;
234             }
235 97   33     178 // 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         193 my $defaulted = 0;
254 97 50 100     604 if (defined $input && $input =~ /\A\R?\Z/ && exists $opt_ref->{-def}) {
      66        
255 5         13 $input = $opt_ref->{-def};
256 5         12 $defaulted = 1;
257             }
258              
259             # The input line is usually chomped before being returned...
260 97 100 100     478 if (defined $input && !$opt_ref->{-line}) {
261 82         168 chomp $input;
262             }
263              
264             # Check for a value indicating failure...
265 97 100 100     308 if (exists $opt_ref->{-fail} && match($input, $opt_ref->{-fail})) {
266 2         233 $input = undef;
267             }
268              
269             # Setting @ARGV is a special case; process it like a command-line...
270 97 100       340 if ($opt_ref->{-argv}) {
271 5         21 @ARGV = map { _shell_expand($_) }
272 1         16 grep {defined}
  15         24  
273             $input =~ m{
274             ( ' [^'\\]* (?: \\. [^'\\]* )* ' )
275             | ( " [^"\\]* (?: \\. [^"\\]* )* " )
276             | (?: ^ | \s) ( [^\s"'] \S* )
277             }gxms;
278 1         21 return 1;
279             }
280              
281             # "Those who remember history are enabled to repeat it"...
282 96 100 66     519 if (defined $input and $opt_ref->{-history} ne 'NONE') {
283 84   100     463 my $history_set = $history_cache{ $opt_ref->{-history} } //= [] ;
284 84         180 @{ $history_set } = ($input, grep { $_ ne $input } @{ $history_set });
  84         263  
  179         393  
  84         540  
285             }
286              
287             # If input timed out insert the default, if any...
288 36   50 36   320 my $timedout = $in_pos == do{ no warnings; tell $in_filehandle } // 0;
  36         82  
  36         44127  
  96         185  
  96         375  
289 96 50 66     259 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         185 my $succeeded = defined $input;
296              
297             # The -yesno variants also need a 'y' to be successful...
298 96 100       257 if ($opt_ref->{-yesno}{count}) {
299 27   66     151 $succeeded &&= $input =~ m{\A \s* y}ixms;
300 27 50 66     89 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       244 if ($opt_ref->{-verbatim}) {
313 6   66     142 return $input // ();
314             }
315              
316             # Failure in a list context returns nothing...
317 90 100 100     379 return if LIST && !$succeeded;
318              
319             # Otherwise, be context sensitive...
320             return
321 43     43   19273 PUREBOOL { $_ = RETOBJ; next handler; }
  43         1152  
322 75     75   17308 BOOL { $succeeded; }
323 48     48   20742 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   2200 };
  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       18 if ($text =~ m{\A ' (.*) ' \z}xms) {
341 1         5 return $1;
342             }
343              
344             # Everything else has shell variables expanded...
345 4         85 my $ENV_PAT = join '|', reverse sort keys %ENV;
346 4         220 $text =~ s{\$ ($ENV_PAT)}{$ENV{$1}}gxms;
347              
348             # Double-quoted text isn't globbed...
349 4 100       20 if ($text =~ m{\A " (.*) " \z}xms) {
350 2         6 return $1;
351             }
352              
353             # Everything else is...
354 2         71 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     16 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   3 my ($arg) = @_;
412             return ref $arg ne 'CODE'
413 0     0   0 ? sub { $arg }
414 36 50 0 36   322 : sub { eval { for (shift) { no warnings; return $arg->($_) // $_ } } };
  36     0   121  
  36         118166  
  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   39 my ($option_type, $constraint_spec) = @_;
424              
425 3 100       21 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     3 map { $STD_CONSTRAINT{$_}
  2         9  
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   171 my ($compare_val) = @_;
438 4         9 for my $constraint (@constraints) {
439 6 100       14 return 0 if !$constraint->($compare_val);
440             }
441 1         5 return 1;
442             }
443 1         9 );
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   139 -echo => sub { my $char = shift; $char eq "\t" ? q{ } : $char },
  30         333  
458 0     0   0 -return => sub { "\n" },
459 99     99   1233 );
460              
461             DECODING:
462 99         392 while (defined(my $arg = shift @_)) {
463 220 50       521 if (my $type = ref $arg) {
464 0         0 _warn( reserved =>
465             'prompt(): Unexpected argument (' . lc($type) . ' ref) ignored'
466             );
467             }
468             else {
469 220         292 state $already_wiped;
470 220         288 my $redo;
471             # The sound of one hand clapping...
472 220 100       5158 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         16 $redo = 1;
474             }
475              
476             # Non-chomping option...
477             elsif ($arg =~ /^-line$/) {
478 1         3 $option{-line}++;
479             }
480             elsif ($arg =~ /^-l/) {
481 4         15 $option{-line}++;
482 4         12 $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         21 must => { '[YN]' => qr{\A \s* [YN] }xms },
490             count => $count,
491             };
492             }
493             elsif ($arg =~ /^-YN/) {
494             $option{-yesno} = {
495 1         9 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     14 my $count = @_ && looks_like_number($_[0]) ? shift @_ : 1;
502             $option{-yesno} = {
503 4         22 must => { '[yn]' => qr{\A \s* [YN] }ixms },
504             count => $count,
505             };
506             }
507             elsif ($arg =~ /^-yn/) {
508             $option{-yesno} = {
509 4         25 must => { '[yn]' => qr{\A \s* [YN] }ixms },
510             count => 1,
511             };
512 4         9 $redo = 2;
513             }
514             elsif ($arg =~ /^-Yes$/) {
515 7 50 33     22 my $count = @_ && looks_like_number($_[0]) ? shift @_ : 1;
516             $option{-yesno} = {
517 7         35 must => { '[Y for yes]' => qr{\A \s* (?: [^y] | \Z) }xms },
518             count => $count,
519             };
520             }
521             elsif ($arg =~ /^-Y/) {
522             $option{-yesno} = {
523 1         6 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     17 my $count = @_ && looks_like_number($_[0]) ? shift @_ : 1;
530 5         15 $option{-yesno} = { count => $count };
531             }
532             elsif ($arg =~ /^-y/) {
533 1         4 $option{-yesno} = { count => 1 };
534 1         3 $redo = 1;
535             }
536              
537             # Load @ARGV...
538             elsif ($arg =~ /^-argv$/) {
539 1         4 $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       8 _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         41 elsif ($arg =~ /^-in$/) { $option{-in} = shift @_; }
597 6         34 elsif ($arg =~ /^-out$/) { $option{-out} = shift @_; }
598              
599              
600             # Specify integer and number return value...
601             elsif ($arg =~ /^-integer$/) {
602 6         14 $option{-integer} = 1;
603 6 100 100     193 if (@_ && (ref $_[0] || $_[0] =~ $STD_CONSTRAINT)) {
      66        
604 3         10 my ($errmsg, $constraint)
605             = _standardize_constraint('integer',shift);
606 3         12 $option{-must}{$errmsg} = $constraint;
607             }
608             }
609             elsif ($arg =~ /^-num(?:ber)?$/) {
610 2         7 $option{-number} = 1;
611 2 50 33     162 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         2 elsif ($arg =~ /^-i/) { $option{-integer} = 1; $redo = 1; }
  1         3  
618 1         2 elsif ($arg =~ /^-n/) { $option{-number} = 1; $redo = 1; }
  1         2  
619              
620             # Specify void context is okay...
621 1         8 elsif ($arg =~ /^-void$/) { $option{-void} = 1; }
622              
623             # Specify verbatim return value...
624 3         8 elsif ($arg =~ /^-verb(?:atim)?$/) { $option{-verbatim} = 1; }
625 3         11 elsif ($arg =~ /^-v/) { $option{-verbatim} = 1; $redo = 1;}
  3         4  
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         10  
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       33 _opt_err('Missing', '-default', 'string') if !@_;
641 5         14 $option{-def} = shift @_;
642             _opt_err('Invalid', '-default', 'string', 'reference')
643 5 50       13 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         4 $arg =~ s{\d+}{}xms;
651 1         3 $redo = 1;
652             }
653             elsif ($arg =~ /^-timeout$/) {
654 2 100       11 _opt_err('Missing', -timeout, 'number of seconds') if !@_;
655 1         8 $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         15 my $restriction = shift @_;
664 8         28 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       26 if ($restriction_type eq 'HASH') {
670 2         4 $restriction_type = 'ARRAY';
671 2         3 $restriction = [ keys %{$restriction} ];
  2         9  
672             }
673             # Arrays of strings matched (and completed) char-by-char...
674 8 100       20 if ($restriction_type eq 'ARRAY') {
    50          
675 7         12 my @restrictions = @{$restriction};
  7         23  
676             $option{-guarantee}
677             = '\A(?:'
678             . join('|', map {
679 7         17 join(q{}, map { "(?:\Q$_\E" } split(q{}, $_))
  30         66  
  34         123  
680             . ')?' x length($_)
681             } @restrictions)
682             . ')\z'
683             ;
684 7 50       23 if ($option{-complete} == $DEFAULT_COMPLETER) {
685 7         20 $option{-complete} = \@restrictions;
686             }
687             }
688             # Regexes matched as-is...
689             elsif ($restriction_type eq 'Regexp') {
690 1         4 $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         48 my @keys = ($option{-prompt} =~ m{$KL_EXTRACT}gxms);
703              
704             # Convert default to a -default...
705 4         29 my @defaults = ($option{-prompt} =~ m{$KL_DEF_EXTRACT}gxms);
706 4 50       21 if (@defaults > 1) {
    100          
707 0         0 _warn( ambiguous =>
708             "prompt(): -keyletters found too many defaults"
709             )
710             }
711             elsif (@defaults) {
712 2         12 push @_, -default => $defaults[0];
713             }
714              
715             # Convert key letters to a -guarantee...
716 4         10 @keys = ( map({uc} @keys), map({lc} @keys) );
  10         25  
  10         23  
717 4 100       11 if (@defaults == 1) {
718 2         5 push @keys, q{};
719             }
720 4         14 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         6 push @_, '-keyletters_implement';
728 2         4 $redo = 1;
729             }
730              
731             # Specify a set of return constraints...
732             elsif ($arg =~ /^-must$/) {
733 6 50       18 _opt_err('Missing', -must, 'constraint hash') if !@_;
734 6         10 my $must = shift @_;
735 6 50       32 _opt_err('Invalid', -must, 'hash reference')
736             if ref($must) ne 'HASH';
737 6         11 for my $errmsg (keys %{$must}) {
  6         21  
738 6         20 $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   354 if ($flag eq '-echo' && !eval { no warnings 'deprecated'; require Term::ReadKey }) {
  36         93  
  36         7049  
  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   296 if (!eval { no warnings 'deprecated'; require Term::ReadKey }) {
  36         102  
  36         81163  
  1         3  
  1         133  
779 1         6 _warn( bareword => "Warning: next input will be in plaintext\n");
780             }
781 1         8 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       4 _opt_err('Missing', '-menu', 'menu specification') if !@_;
814 1 50       5 $option{-menu} = ref $_[0] ? shift(@_) : \shift(@_);
815 1         3 $option{-prompt} .= $MENU_MK;
816 1         4 $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         9 elsif ($arg =~ /^-\w+$/) { _warn(misc => "prompt(): Unknown option $arg ignored"); }
828              
829             # Anything else is part fo the prompt...
830 96         277 else { $option{-prompt} .= $arg; }
831              
832             # Handle option bundling...
833 218 100 100     2349 redo DECODING if $redo && $arg =~ s{\A -.{$redo} (?=.)}{-}xms;
834             }
835             }
836              
837             # Precompute top-level menu, if menuing...
838 97 100       295 if (exists $option{-menu}) {
839             $option{-menu} = _build_menu($option{-menu},
840             undef,
841             $option{-number}||$option{-integer}
842 1   33     12 );
843             }
844              
845             # Handle return magic on -single...
846 97 100 50     304 if (defined $option{-single} && length($option{-echo}('X')//'echoself')) {
      66        
847 6   50 0   18 $option{-return} //= sub{ "\n" };
  0         0  
848             }
849              
850             # Adjust prompt as necessary...
851 97 100       685 if ($option{-argv}) {
    100          
    100          
    50          
852 1   33     4 my $progname = $option{-prompt} // $0;
853 1         4 $progname =~ s{^.*/}{}xms;
854              
855 1         2 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         20 }
868             elsif (!defined $option{-prompt}) {
869 3         8 $option{-prompt} = '> ';
870             }
871             elsif ($option{-prompt} =~ m{ \S \z}xms) {
872             # If prompt doesn't end in whitespace, make it so...
873 79         263 $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     264 $option{-history} //= $option{-prompt};
882              
883             # Verify any default satisfies any constraints...
884 97 100 100     320 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         253 return \%option;
893             }
894              
895             #====[ Error Handlers ]=========================================
896              
897             sub _opt_err {
898 2     2   9 my ($problem, $option, $expectation, $found) = @_;
899 2 100       7 if (@_ > 3) {
900 1         17 Carp::croak "prompt(): $problem value for $option (expected $expectation, but found $found)";
901             }
902             else {
903 1         7 Carp::croak "prompt(): $problem value for $option (expected $expectation)";
904             }
905             }
906              
907             sub _warn {
908 5     5   53 my ($category, @message) = @_;
909              
910 5 100       113 return if !warnings::enabled($category);
911              
912 3         424 my $message = join(q{},@message);
913 3 100       112 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   416 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         247 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   337 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         196 my $input = ${$input_ref};
  121         260  
947 121 50 66     192 if (${$input_ref} =~ m{^\R?$}xms && exists $opt_ref->{-def}) {
  121         696  
948 11 100       34 return 1 if $opt_ref->{-def_nocheck};
949             $input = $opt_ref->{-def}
950 10         16 }
951 120         299 chomp $input;
952              
953 120         163 my $failed;
954             # Integer constraint is hard-coded...
955 120 100 100     524 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     668 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     364 my $must_ref = $opt_ref->{-must} // {};
966 120         204 my @must_keys = sort keys %{$must_ref};
  120         477  
967 120 50       316 my %clean_key_for = map { $_ => (/^\d+[.:]?\s*(.*)/s ? $1 : $_) } @must_keys;
  36         188  
968 120         239 my @must_kv_list = map { $clean_key_for{$_} => $must_ref->{$_} } @must_keys;
  36         94  
969              
970             # Combine -yesno and -must constraints...
971             my %constraint_for = (
972 120   50     539 %{ $extras // {} },
973 120   100     183 %{ $opt_ref->{-yesno}{must} // {} },
  120         626  
974             @must_kv_list,
975             );
976             my @constraints = (
977 120   50     498 keys %{ $extras // {} },
978 120   100     604 keys %{ $opt_ref->{-yesno}{must} // {} },
979 120         319 @clean_key_for{@must_keys},
980             );
981              
982             # User-specified constraints...
983 120 100 100     720 if (!$failed && keys %constraint_for) {
984             CONSTRAINT:
985 62         128 for my $msg (@constraints) {
986 62         101 my $constraint = $constraint_for{$msg};
987 36 100   36   331 next CONSTRAINT if eval { no warnings; local $_ = $input; match($input, $constraint); };
  36         93  
  36         8717  
  62         83  
  62         113  
  62         199  
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       2274 . "(must $msg) "
    100          
993             ;
994 22         56 last CONSTRAINT;
995             }
996             }
997              
998             # If any constraint not satisfied...
999 120 100       4200 if ($failed) {
1000             # Return failure if not actually prompting at the moment...
1001 28 50       64 return 0 if !$outputter_ref;
1002              
1003             # Redraw post-menu prompt with failure message appended...
1004 28         177 $failed =~ s{.*$MENU_MK}{}xms;
1005 28         80 $outputter_ref->(-style => _wipe_line(), $failed);
1006              
1007             # Reset input collector...
1008 28         56 ${$input_ref} = q{};
  28         49  
1009              
1010             # Reset faked input, if any...
1011 28 50 33     84 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   318 no warnings 'exiting';
  36         117  
  36         27145  
1017 28         120 next INPUT;
1018             }
1019              
1020             # Otherwise succeed...
1021 92         384 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   248 my ($in_fh, $outputter_ref, $opt_ref) = @_;
1027              
1028             # Set-up for timeouts...
1029 97   50     416 my $fileno = fileno($in_fh) // -1;
1030 97   66     444 my $has_timeout = exists $opt_ref->{-timeout} && $fileno >= 0;
1031 97         178 my $timeout = $opt_ref->{-timeout};
1032 97         211 my $readbits = q{};
1033 97 50 33     351 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         234 my $local_fake_input;
1039             my $orig_fake_input;
1040 97 100 66     576 if (defined $fake_input && length($fake_input) > 0) {
1041 4         42 $fake_input =~ s{ \A (.*) \R? }{}xm;
1042 4         20 $orig_fake_input = $local_fake_input = $1;
1043             }
1044              
1045             return sub {
1046 97     97   243 my ($extra_constraints) = @_;
1047              
1048             INPUT:
1049 97         182 while (1) {
1050 125 50 33     440 if (!$has_timeout || select $readbits, undef, undef, $timeout) {
1051 125         216 my $input;
1052              
1053             # Real input comes from real filehandles...
1054 125 100       317 if (!defined $local_fake_input) {
1055 121         473 $input = readline $in_fh;
1056             }
1057             # Fake input has to be typed...
1058             else {
1059 4         6 $input = $local_fake_input;
1060 4         4000819 sleep 1;
1061 4         223 for ($local_fake_input =~ m/\X/g) {
1062 24         125 _simulate_typing();
1063 24         434 $outputter_ref->(-echostyle => $opt_ref->{-echo}($_));
1064             }
1065 4         145 readline $in_fh;
1066              
1067             # Check for simulated EOF...
1068 4 50       115 if ($input =~ m{^ \s* (?: \cD | \cZ ) }xms) {
1069 0         0 $input = undef;
1070             }
1071             }
1072              
1073 125 50       390 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       330 if (defined $input) {
1081 115         439 _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     598 ? substr($input, 0, 1)
1088             : $input;
1089             }
1090             else {
1091 0         0 return;
1092             }
1093             }
1094             }
1095 97         1000 }
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   64 state $TYPING_SPEED = 0.07; # seconds per character
1107 24         683385 select undef, undef, undef, rand $TYPING_SPEED;
1108             }
1109              
1110             sub _term_width {
1111 36     36   335 my ($term_width) = eval { no warnings 'deprecated'; Term::ReadKey::GetTerminalSize(\*STDERR) };
  36     28   121  
  36         37785  
  28         49  
  28         354  
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   253 my ($in_fh, $outputter_ref, $opt_ref) = @_;
1222              
1223 36     36   356 my $has_readkey = eval { no warnings 'deprecated'; require Term::ReadKey };
  36         86  
  36         14618  
  97         184  
  97         13816  
1224              
1225             # If no per-character reads, fall back on buffered input...
1226 97 50 33     689 if (!-t $in_fh || !$has_readkey) {
1227 97         338 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   329 no strict 'refs';
  36         69  
  36         156257  
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   6 my ($source_ref, $initial_prompt, $is_numeric) = @_;
1660 1   50     8 my $prompt = ($initial_prompt//q{}) . qq{\n};
1661 1         3 my $final = q{};
1662 1         4 my %value_for;
1663             my %key_for;
1664 1         0 my @selectors;
1665              
1666 1         3 for my $source_type (ref $source_ref) {
1667 1 50       6 if ($source_type eq 'HASH') {
    50          
    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 0         0 $_ = 'ARRAY';
1674 0         0 continue;
1675             }
1676             elsif ($source_type eq 'SCALAR') {
1677 0         0 $source_ref = [ split "\n", ${$source_ref} ];
  0         0  
1678 0         0 $_ = 'ARRAY';
1679 0         0 continue;
1680             }
1681             elsif ($source_type eq 'ARRAY') {
1682 1         2 my @source = @{$source_ref};
  1         3  
1683 1 50       9 @selectors = $is_numeric ? (1..@source) : ('a'..'z','A'..'Z');
1684 1 50       4 if (!keys %value_for) {
1685 1         12 @value_for{@selectors} = @source;
1686             }
1687             ITEM:
1688 1         3 for my $tag (@selectors) {
1689 10   50     21 my $item = shift(@source) // last ITEM;
1690 10         12 chomp $item;
1691 10         26 $prompt .= sprintf("%4s. $item\n", $tag);
1692 10         14 $final = $tag;
1693             }
1694 1 50       5 if (@source) {
1695 0         0 _warn( misc =>
1696             "prompt(): Too many menu items. Ignoring the final " . @source
1697             );
1698             }
1699             }
1700             }
1701              
1702 1 0       6 my $constraint = $is_numeric ? '(?:' . join('|',@selectors) .')'
    50          
1703             : $final =~ /[A-Z]/ ? "[a-zA-$final]"
1704             : "[a-$final]";
1705 1 50       3 my $constraint_desc = $is_numeric ? "[1-$selectors[-1]]" : $constraint;
1706 1         3 $constraint = '\A\s*' . $constraint . '\s*\Z';
1707              
1708             return {
1709 1         65 data => $source_ref,
1710             key_for => \%key_for,
1711             value_for => \%value_for,
1712             prompt => "$prompt\n",
1713             is_numeric => $is_numeric,
1714             constraint => { "Enter $constraint_desc: " => qr/$constraint|$MENU_ESC/ },
1715             };
1716             }
1717              
1718             # Vocabulary that _stylize understands...
1719             my %synonyms = (
1720             bold => [qw],
1721             dark => [qw],
1722             faint => [qw],
1723             underline => [qw],
1724             italic => [qw],
1725             blink => [qw],
1726             reverse => [qw],
1727             concealed => [qw],
1728             reset => [qw],
1729             bright_ => [qw< bright\s+ vivid\s+ >],
1730             red => [qw< scarlet vermilion crimson ruby cherry cerise cardinal carmine
1731             burgundy claret chestnut copper garnet geranium russet
1732             salmon titian coral cochineal rose cinnamon ginger gules >],
1733             yellow => [qw< gold golden lemon cadmium daffodil mustard primrose tawny
1734             amber aureate canary champagne citrine citron cream goldenrod honey straw >],
1735             green => [qw< olive jade pea emerald lime chartreuse forest sage vert >],
1736             cyan => [qw< aqua aquamarine teal turquoise ultramarine >],
1737             blue => [qw< azure cerulean cobalt indigo navy sapphire >],
1738             magenta => [qw< amaranthine amethyst lavender lilac mauve mulberry orchid periwinkle
1739             plum pomegranate violet purple aubergine cyclamen fuchsia modena puce
1740             purpure >],
1741             black => [qw< charcoal ebon ebony jet obsidian onyx raven sable slate >],
1742             white => [qw< alabaster ash chalk ivory milk pearl silver argent >],
1743             );
1744              
1745             # Back-mapping to standard terms...
1746             my %normalize
1747             = map { join('|', map { "$_\\b" } reverse sort @{$synonyms{$_}}) => $_ }
1748             keys %synonyms;
1749              
1750             my $BACKGROUND = qr{
1751             (\S+) \s+ (?: behind | beneath | below | under(?:neath)? )\b
1752             | \b (?:upon|over|on) \s+ (?:an?)? \s+ (.*?) \s+ (?:background|bg|field) \b
1753             | \b (?:upon\s+ | over\s+ | (?:(on|upon|over)\s+a\s+)? (?:background|bg|field) \s+ (?:of\s+|in\s+)? | on\s+) (\S+)
1754             }ixms;
1755              
1756             # Convert a description to ANSI colour codes...
1757             sub _stylize {
1758 0   0 0   0 my $spec = shift // q{};
1759              
1760             # Handle arrays and hashes as args...
1761 0 0       0 if (ref($spec) eq 'ARRAY') {
    0          
1762 0         0 $spec = join q{ }, @{$spec};
  0         0  
1763             }
1764             elsif (ref($spec) eq 'HASH') {
1765 0         0 $spec = join q{ }, keys %{$spec};
  0         0  
1766             }
1767              
1768             # Ignore punctuation...
1769 0         0 $spec =~ s/[^\w\s]//g;
1770              
1771             # Handle backgrounds...
1772 0         0 $spec =~ s/$BACKGROUND/on_$+/g;
1773              
1774             # Apply standard translations...
1775 0         0 for my $pattern (keys %normalize) {
1776 0   0     0 $spec =~ s{\b(on_|\b) $pattern}{($1//q{}).$normalize{$pattern}}geixms;
  0         0  
1777             }
1778              
1779             # Ignore anything unknown...
1780 0 0 0     0 $spec =~ s{((?:on_)?(?:(ansi\d+|rgb\d+)|(\S+)))}{ $2 || exists $synonyms{$3} ? $1 : q{} }gxmse;
  0         0  
1781              
1782             # Build ANSI terminal codes around text...
1783 0         0 my $raw_text = join q{}, @_;
1784 0         0 my ($prews, $text, $postws) = $raw_text =~ m{\A (\s*) (.*?) (\s*) \Z}xms;
1785 0         0 my @style = split /\s+/, $spec;
1786 0 0       0 return $prews
1787             . ( @style ? Term::ANSIColor::colored(\@style, $text) : $text )
1788             . $postws;
1789             }
1790              
1791             # Build a subroutine that prints printable chars to the specified filehandle...
1792             sub _std_printer_to {
1793 0     0   0 my ($out_filehandle, $opt_ref) = @_;
1794 36     36   369 no strict 'refs';
  36         75  
  36         14394  
1795 0         0 _autoflush($out_filehandle);
1796 0 0       0 if (eval { require Term::ANSIColor}) {
  0         0  
1797             return sub {
1798 0     0   0 my $style = shift;
1799 0 0       0 return tell($out_filehandle) if $style eq -tell;
1800 0         0 my @loc = (@_);
1801 0         0 s{\e}{^}gxms for @loc;
1802 0         0 print {$out_filehandle} _stylize($opt_ref->{$style}(@loc), @loc);
  0         0  
1803 0         0 };
1804             }
1805             else {
1806             return sub {
1807 0     0   0 my $style = shift;
1808 0 0       0 return tell($out_filehandle) if $style eq -tell;
1809 0         0 my @loc = (@_);
1810 0         0 s{\e}{^}gxms for @loc;
1811 0         0 print {$out_filehandle} @loc;
  0         0  
1812 0         0 };
1813             }
1814             }
1815              
1816             # Build a subroutine that prints to nowhere...
1817             sub _null_printer {
1818 97     149   397 return sub {};
        97      
1819             }
1820              
1821             1; # Magic true value required at end of module
1822             __END__