File Coverage

blib/lib/Term/Completion.pm
Criterion Covered Total %
statement 32 309 10.3
branch 9 160 5.6
condition 1 44 2.2
subroutine 7 18 38.8
pod 8 9 88.8
total 57 540 10.5


line stmt bran cond sub pod time code
1             package Term::Completion;
2            
3 3     3   73125 use strict;
  3         7  
  3         106  
4 3     3   14 use warnings;
  3         6  
  3         100  
5 3     3   17 use Carp qw(croak);
  3         7  
  3         192  
6 3     3   2971 use IO::Handle;
  3         24749  
  3         11023  
7            
8             require Exporter;
9             our @ISA = qw(Exporter);
10             our @EXPORT_OK = qw(Complete);
11            
12             our $VERSION = '1.00';
13            
14             our %DEFAULTS = (
15             # input/output channels
16             in => \*STDIN,
17             out => \*STDOUT,
18             # key definitions
19             tab => qr/\t/,
20             list => qr/\cd/,
21             'kill' => qr/\cu/,
22             erase => qr/[\177\010]/, # BS and DEL
23             wipe => qr/\cw/,
24             enter => qr/[\r\n]/,
25             up => qr/\cp|\x1b\[[AD]/, # CTRL-p, up arrow, left arrow
26             down => qr/\cn|\x1b\[[BC]/, # CTRL-n, down arrow, right arrow
27             # key definitions for paging
28             quit => qr/[\ccq]/, # CTRL-C or q
29             # output parameters
30             prompt => '',
31             columns => 80, # default, if no Term::Size available
32             rows => 24,
33             bell => "\a",
34             page_str => '--more--',
35             eol => "\r\n",
36             del_one => "\b \b",
37             # help
38             help => undef,
39             helptext => undef,
40             # default: empty list of choices
41             choices => [],
42             default => ''
43             );
44            
45             # selection which TTY handler to use
46             sub import
47             {
48 4     4   128 my $class = shift;
49 4         7 my @syms;
50             # TODO Win32?
51 4 50       29 my $termhandler = ($^O !~ /interix/i ? 'Term::Completion::_readkey' :
52             'Term::Completion::_POSIX');
53 4         15 foreach(@_) {
54 3 100 33     38 if(/^:posix$/) {
    50          
    100          
    50          
55 1         3 $termhandler = 'Term::Completion::_POSIX';
56             }
57             elsif(/^:stty$/) {
58 0         0 $termhandler = 'Term::Completion::_stty';
59             }
60             elsif(/^:readkey$/) {
61 1         2 $termhandler = 'Term::Completion::_readkey';
62             }
63             elsif(/^:DEFAULT$/ || !/^:/) {
64 0         0 push(@syms, $_);
65             }
66             else {
67 1         309 croak __PACKAGE__ . " does not export '$_'";
68             }
69             }
70 3         198 eval "require $termhandler;";
71 3 50       21 if($@) {
72 0         0 croak "Cannot initialize ".__PACKAGE__.", error occurred while loading auxiliary class $termhandler:\n$@";
73             }
74 3         44 push(@ISA, $termhandler);
75 3         374 $class->export_to_level(1, $class, @syms);
76             }
77            
78             sub _get_defaults
79             {
80 2     2   39 my %def = %DEFAULTS;
81 2         29 delete @def{qw(columns rows)};
82 2         31 return %def;
83             }
84            
85             sub new
86             {
87 2     2 1 1568 my __PACKAGE__ $class = shift;
88            
89 2 50       12 if(ref $class) {
90 0         0 $class = ref $class;
91             }
92 2         5 my %args = @_;
93 2         10 my $this = bless({$class->_get_defaults, %args}, $class);
94 2         11 return $this;
95             }
96            
97             #sub DESTROY
98             #{
99             # my __PACKAGE__ $this = shift;
100             # 1;
101             #}
102            
103             # old style interface
104             sub Complete
105             {
106 0     0 1   my $prompt = shift;
107 0 0         $prompt = '' unless defined $prompt;
108            
109 0           my @choices;
110 0 0 0       if (ref $_[0] || $_[0] =~ /^\*/) {
111 0           @choices = sort @{$_[0]};
  0            
112             } else {
113 0           @choices = sort(@_);
114             }
115            
116 0           __PACKAGE__->new(
117             prompt => $prompt,
118             choices => \@choices
119             )->complete;
120             }
121            
122             # sub get_key
123             # virtual - defined in tty driver classes
124            
125             sub show_help
126             {
127 0     0 0   my __PACKAGE__ $this = shift;
128 0   0       my $text = $this->{helptext} || '';
129 0           $text =~ s/\r?\n|\n?\r/$this->{eol}/g;
130 0           $this->{out}->print($text);
131             }
132            
133             sub complete
134             {
135 0     0 1   my __PACKAGE__ $this = shift;
136            
137 0           my $return = $this->{default};
138 0           my $r = length($return);
139            
140 0 0 0       if(defined $this->{helptext} && !defined $this->{help}) {
141 0           $this->show_help();
142             }
143            
144             # we grab full control of the terminal, switch off echo
145 0           $this->set_raw_tty();
146            
147 0           my $tab_pressed = 0; # repeated tab counter
148 0           my $choice_num; # selector
149             my @choice_cycle;
150 0           my $eof = 0;
151            
152             # handle terminal size changes
153             # save any existing signal handler
154 0 0         if(exists $SIG{'WINCH'}) {
155 0           $this->{_sig_winch} = $SIG{WINCH};
156             # set new signal handler
157             local $SIG{'WINCH'} = sub {
158 0 0   0     if($this->{_sig_winch}) {
159 0           &{$this->{_sig_winch}};
  0            
160             }
161             # write new prompt and completion line
162 0           $this->{out}->print($this->{eol}, $this->{prompt}, $return);
163 0           };
164             }
165            
166             # main loop for completion
167             LOOP: {
168 0           local $_ = '';
  0            
169 0           $this->{out}->print($this->{prompt}, $return);
170 0           my $key;
171 0   0       GETC: while (defined($key = $this->get_key) && ($_ .= $key, $_ !~ $this->{enter})) {
172             CASE: {
173             # deal with arrow key escape sequences
174 0 0 0       if(/^\x1b([^\[])/ || /^\x1b\[(?:[A-Z]|\d+~)(.)/) {
  0            
175             # unknown ESC sequence: just keep the last key typed
176 0           $_ = $1;
177 0           redo CASE;
178             }
179            
180             # (TAB) attempt completion
181 0 0         $_ =~ $this->{tab} && do {
182 0 0         if($tab_pressed++) {
183 0           $this->show_choices($return);
184 0           redo LOOP;
185             }
186 0           my @match = $this->get_choices($return);
187 0 0         if (@match == 0) {
188             # sound bell if there is no match
189 0           $this->bell();
190             } else {
191 0           my $l = length(my $test = shift(@match));
192 0 0         if(@match) {
    0          
193             # sound bell if multiple choices
194 0           $this->bell();
195             }
196             elsif($this->{delim}) {
197 0           $test .= $this->{delim};
198 0           $l++;
199             }
200 0           foreach my $cmp (@match) {
201 0           until (substr($cmp, 0, $l) eq substr($test, 0, $l)) {
202 0           $l--;
203             }
204             }
205 0           my $add = $l - $r;
206 0 0         if($add) {
207 0           $this->{out}->print($test = substr($test, $r, $add));
208             # reset counter if something was added
209 0           $tab_pressed = 0;
210 0           $choice_num = undef;
211 0           $return .= $test;
212 0           $r += $add;
213             }
214             }
215 0           last CASE;
216             };
217            
218 0           $tab_pressed = 0; # reset repeated tab counter
219            
220             # (^D) completion list
221 0 0         $_ =~ $this->{list} && do {
222 0           $this->show_choices($return);
223 0           redo LOOP;
224             };
225            
226             # on-demand help
227 0 0         if(defined $this->{help}) {
228 0 0         $_ =~ $this->{help} && do {
229 0 0         if(defined $this->{helptext}) {
230 0           $this->{out}->print($this->{eol});
231 0           $this->show_help();
232             }
233 0           redo LOOP;
234             };
235             }
236            
237             # (^U) kill
238 0 0         $_ =~ $this->{'kill'} && do {
239 0 0         if ($r) {
240             # start over on a new line
241 0           $r = 0;
242 0           $return = "";
243 0           $this->{out}->print($this->{eol});
244 0           $choice_num = undef;
245 0           redo LOOP;
246             }
247 0           last CASE;
248             };
249            
250             # (DEL) || (BS) erase
251 0 0         $_ =~ $this->{erase} && do {
252 0 0         if($r) {
253 0           $this->{out}->print($this->{del_one});
254 0           chop($return);
255 0           $r--;
256 0           $choice_num = undef;
257             }
258 0           last CASE;
259             };
260            
261             # ^W wipe until separator
262 0 0         $_ =~ $this->{wipe} && do {
263 0 0         if($r) {
264 0           my $sep = '';
265 0 0         $sep = $this->{sep} if defined $this->{sep};
266 0 0         $sep .= $this->{delim} if defined $this->{delim};
267 0 0 0       if(length($sep) && $return =~ s/((?:^|[$sep$sep]+)[^$sep$sep]*[$sep$sep]*)$//s) {
268 0           my $cut = $1;
269 0           $this->{out}->print($this->{del_one} x length($cut));
270 0           $r = length($return);
271 0           $choice_num = undef;
272             }
273             }
274 0           last CASE;
275             };
276            
277             # up (CTRL-P)
278 0 0         $_ =~ $this->{up} && do {
279 0 0         unless(defined $choice_num) {
280 0           @choice_cycle = $this->get_choices($return);
281 0 0         if(defined $choice_cycle[$#choice_cycle]) {
282 0           $choice_num = $#choice_cycle;
283             }
284             } else {
285 0 0         if($choice_num <= 0) {
286 0           $choice_num = @choice_cycle; # TODO get_choices returns number in scalar context?
287             }
288 0           $choice_num--;
289             }
290             #TODO only delete/print differences, not full string
291 0 0         unless(defined $choice_num) {
292 0           $this->bell();
293             } else {
294 0           $this->{out}->print($this->{del_one} x length($return));
295 0           $return = $choice_cycle[$choice_num];
296 0           $this->{out}->print($return);
297 0           $r = length($return);
298             }
299 0           last CASE;
300             };
301            
302             # down (CTRL-N)
303 0 0         $_ =~ $this->{down} && do {
304 0 0         unless(defined $choice_num) {
305 0           @choice_cycle = $this->get_choices($return);
306 0 0         if(defined $choice_cycle[0]) {
307 0           $choice_num = 0;
308             }
309             } else {
310 0 0         if(++$choice_num >= @choice_cycle) {
311 0           $choice_num = 0;
312             }
313             }
314             #TODO only delete/print differences, not full string
315 0 0         unless(defined $choice_num) {
316 0           $this->bell();
317             } else {
318 0           $this->{out}->print($this->{del_one} x length($return));
319 0           $return = $choice_cycle[$choice_num];
320 0           $this->{out}->print($return);
321 0           $r = length($return);
322             }
323 0           last CASE;
324             };
325            
326             # printable char
327 0 0         ord >= 32 && do {
328 0           $return .= $_;
329 0           $r++;
330 0           $this->{out}->print($_);
331 0           $choice_num = undef;
332 0           last CASE;
333             };
334            
335 0 0         $_ !~ /^\x1b/ && do {
336             # sound bell and reset any unknown key
337 0           $this->bell();
338 0           $_ = '';
339             };
340 0           next GETC; # nothing matched - get new character
341             } # :ESAC
342 0           $_ = '';
343             } # while getc != enter
344 0           $this->{out}->print($this->{eol});
345 0           $return = $this->post_process($return);
346             # only validate if we had input
347 0 0         my $match = defined($key) ? $this->validate($return) : $return;
348 0 0         unless(defined $match) {
349 0           redo LOOP;
350             }
351 0           $return = $match;
352             } # end LOOP
353            
354 0           $this->reset_tty;
355 0           delete $this->{_sig_winch};
356            
357 0           return $return;
358             }
359            
360             sub validate
361             {
362 0     0 1   my __PACKAGE__ $this = shift;
363 0           my $return = shift;
364 0 0         unless($this->{validate}) {
    0          
365 0           return $return;
366             }
367             elsif(ref $this->{validate}) {
368             # arrayref with message to print and code ref
369 0           my ($msg, $cb) = @{$this->{validate}};
  0            
370 0           my $match = &$cb($return);
371 0 0         unless(defined $match) {
372 0           $this->{out}->print($msg,$this->{eol});
373 0           return;
374             }
375 0           return $match;
376             }
377            
378             # we may have several validation options
379 0           my @vals = split(/[\s,]+/, $this->{validate});
380            
381 0           VALIDATE_OPTIONS: foreach my $val (@vals) {
382            
383 0 0         if($val eq 'lowercase') {
384 0           $return = lc($return);
385             }
386            
387 0 0         if($val eq 'uppercase') {
388 0           $return = uc($return);
389             }
390            
391 0 0         if($val eq 'match_one') {
392 0           my @choices = $this->get_choices('');
393 0           my @matches = grep(/^\Q$return\E/, @choices);
394             MATCH: {
395 0 0         if(@matches == 1) {
  0 0          
396             # unique match at beginning
397 0           $return = $matches[0];
398 0           last MATCH;
399             }
400             elsif(@matches == 0) {
401 0           @matches = grep(/\Q$return\E/, @choices);
402 0 0         if(@matches == 1) {
403             # unique match anywhere
404 0           $return = $matches[0];
405 0           last MATCH;
406             }
407             }
408 0           $this->{out}->print("ERROR: Answer '$return' does not match a unique item!",$this->{eol});
409 0           $return = undef;
410 0           last VALIDATE_OPTIONS;
411             } # MATCH
412             }
413            
414 0 0         if($val eq 'nonempty') {
415 0 0         unless(length $return) {
416 0           $this->{out}->print("ERROR: Empty input not allowed!",$this->{eol});
417 0           $return = undef;
418 0           last VALIDATE_OPTIONS;
419             }
420             }
421            
422 0 0         if($val eq 'nonblank') {
423 0 0 0       unless(length $return && $return =~ /\S/) {
424 0           $this->{out}->print("ERROR: Blank input not allowed!",$this->{eol});
425 0           $return = undef;
426 0           last VALIDATE_OPTIONS;
427             }
428             }
429            
430 0 0         if($val eq 'fromchoices') {
431 0 0 0       if(length($return) && !grep($return eq $_, $this->get_choices(''))) {
432 0           $this->{out}->print("ERROR: You must choose one item from the list!",$this->{eol});
433 0           $return = undef;
434 0           last VALIDATE_OPTIONS;
435             }
436             }
437            
438 0 0         if($val eq 'numeric') {
439 0 0         unless($return =~ /^-?(?:\.\d+|\d+\.?\d*)$/) {
440 0           $this->{out}->print("ERROR: Value must be numeric!",$this->{eol});
441 0           $return = undef;
442 0           last VALIDATE_OPTIONS;
443             }
444             }
445            
446 0 0         if($val eq 'integer') {
447 0 0         unless($return =~ /^-?\d+$/) {
448 0           $this->{out}->print("ERROR: Value must be an integer number!",$this->{eol});
449 0           $return = undef;
450 0           last VALIDATE_OPTIONS;
451             }
452             }
453            
454 0 0         if($val eq 'nonzero') {
455 0 0         if($return == 0) {
456 0           $this->{out}->print("ERROR: Value must be a non-zero value!",$this->{eol});
457 0           $return = undef;
458 0           last VALIDATE_OPTIONS;
459             }
460             }
461            
462 0 0         if($val eq 'positive') {
463 0 0         unless($return > 0.0) {
464 0           $this->{out}->print("ERROR: Value must be a positive value!",$this->{eol});
465 0           $return = undef;
466 0           last VALIDATE_OPTIONS;
467             }
468             }
469            
470             } # end validation options
471            
472             # TODO die on unknown validate option?
473 0           return $return;
474             }
475            
476             sub bell
477             {
478 0     0 1   my __PACKAGE__ $this = shift;
479 0           my $bell = $this->{bell};
480 0 0         $this->{out}->print($bell) if $bell;
481             }
482            
483             sub get_choices
484             {
485 0     0 1   my __PACKAGE__ $this = shift;
486 0   0       grep(defined && /^\Q$_[0]/,@{$this->{choices}});
  0            
487             }
488            
489             sub show_choices
490             {
491 0     0 1   my __PACKAGE__ $this = shift;
492 0           my $return = shift;
493             # start new line - cursor was on input line
494 0           $this->{out}->print($this->{eol});
495 0           $this->_show_choices($this->get_choices($return));
496             }
497            
498             sub _show_choices {
499 0     0     my __PACKAGE__ $this = shift;
500 0           my @choices = @_;
501            
502 0           my $eol = $this->{eol};
503 0 0         unless(@choices) {
504 0           return 1;
505             }
506 0 0 0       if(defined $this->{columns} && $this->{columns} == 0) {
507             # poor man's solution:
508 0           $this->{out}->print(join($eol, @choices), $eol);
509 0           return 1;
510             }
511            
512             # find width of widest entry
513 0           my $MAXWIDTH = 0;
514 0   0       grep(length > $MAXWIDTH && ($MAXWIDTH = length), @choices);
515 0           $MAXWIDTH++; # add one for a blank between the columns
516            
517 0 0         if(exists $SIG{'WINCH'}) {
518 0           $this->{_winch} = 0;
519             local $SIG{'WINCH'} = sub {
520 0     0     $this->{_winch}++;
521 0 0         if($this->{_sig_winch}) {
522 0           return &{$this->{_sig_winch}};
  0            
523             }
524 0           };
525             }
526            
527 0           my ($COLUMNS,$ROWS) = ($this->{columns}, $this->{rows});
528 0 0 0       START_PAGING: {
529 0           ($COLUMNS,$ROWS) = $this->get_term_size()
530             unless $COLUMNS && $ROWS;
531 0           my $maxwidth = $MAXWIDTH;
532 0 0         my $columns = $maxwidth >= $COLUMNS ? 1 : int($COLUMNS / $maxwidth);
533            
534             ## if there's enough margin to intersperse among the columns, do so.
535 0           $maxwidth += int(($COLUMNS % $maxwidth) / $columns);
536 0           my $lines = int((@choices + $columns - 1) / $columns);
537 0           $columns-- while ((($lines * $columns) - @choices + 1) > $lines);
538            
539 0           my $i = 0;
540 0           my $page_lines = 0;
541 0           for (my $l = 0; $l < $lines; $l++) {
542 0           my @line;
543 0   0       for(my $c = 0; $c < $columns && $i<@choices; $c++) {
544 0           push(@line, sprintf("%-${maxwidth}s", $_[$i++]));
545             }
546             # no paging if ROWS were set to 0
547 0 0 0       if($ROWS && ++$page_lines == $ROWS) {
548 0           $this->{out}->print($this->{page_str});
549 0           my $c = $this->get_key;
550             # delete pager line
551 0           $this->{out}->print($this->{del_one} x length($this->{page_str}));
552 0 0         if($c =~ $this->{quit}) {
    0          
    0          
553 0           return 1;
554             }
555             elsif($this->{_winch}) {
556             # winch signaled, restart paging
557 0           $this->{_winch} = 0;
558 0           $this->bell();
559 0           $this->{out}->print($this->{eol});
560 0           $COLUMNS = $ROWS = undef;
561 0           redo START_PAGING;
562             }
563             elsif($c =~ $this->{enter}) {
564 0           $page_lines--;
565             }
566             else {
567 0           $page_lines = 0;
568             }
569             }
570 0           $this->{out}->print(@line, $eol);
571             } # end loop over lines
572             } # end START_PAGING
573 0           1;
574             }
575            
576             sub post_process
577             {
578 0     0 1   my __PACKAGE__ $this = shift;
579 0           my $return = shift;
580 0           $return =~ s/^\s+|\s+$//sg;
581 0           $return;
582             }
583            
584             1;
585             __END__