File Coverage

blib/lib/IO/Prompter.pm
Criterion Covered Total %
statement 471 834 56.4
branch 137 374 36.6
condition 109 280 38.9
subroutine 49 75 65.3
pod 1 1 100.0
total 767 1564 49.0


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