File Coverage

blib/lib/IO/Prompter.pm
Criterion Covered Total %
statement 430 847 50.7
branch 234 548 42.7
condition 110 310 35.4
subroutine 52 82 63.4
pod 1 1 100.0
total 827 1788 46.2


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