File Coverage

blib/lib/Prompt/ReadKey/Sequence.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             package Prompt::ReadKey::Sequence;
4 2     2   41855 use Moose;
  0            
  0            
5              
6             use Prompt::ReadKey;
7             use Prompt::ReadKey::Util;
8              
9             use Tie::RefHash;
10             use Set::Object qw(set);
11              
12             use List::Util qw(first);
13              
14             has items => (
15             isa => "ArrayRef",
16             is => "rw",
17             default => sub { [ ] },
18             );
19              
20             has default_prompt => (
21             init_arg => "prompt",
22             isa => "Str",
23             is => "rw",
24             );
25              
26             has default_options => (
27             init_arg => "options",
28             isa => "ArrayRef[HashRef]",
29             is => "rw",
30             default => sub { [ ] },
31             );
32              
33             has item_arguments => (
34             isa => "HashRef[HashRef]",
35             is => "rw",
36             default => sub { tie my %hash, 'Tie::RefHash'; \%hash },
37             );
38              
39             has prompt_object => (
40             isa => "Object",
41             is => "rw",
42             default => sub { Prompt::ReadKey->new },
43             );
44              
45             has additional_prompt_args => (
46             isa => "ArrayRef",
47             is => "rw",
48             default => sub { [] },
49             );
50              
51             has prompt_format => (
52             isa => "Str",
53             is => "rw",
54             default => '%(prompt)s (%(item_num)d/%(item_count)d) [%(option_keys)s] ',
55             );
56              
57             has movement => (
58             isa => "Bool",
59             is => "rw",
60             default => 1,
61             );
62              
63             has wait => (
64             isa => "Bool",
65             is => "rw",
66             default => 1,
67             );
68              
69             has wait_help => (
70             isa => "Str",
71             is => "rw",
72             default => "Wait with this item, and reprompt later.",
73             );
74              
75             has wait_keys => (
76             isa => "ArrayRef",
77             is => "rw",
78             default => sub { [qw(w)] },
79             );
80              
81             has prev_help => (
82             isa => "Str",
83             is => "rw",
84             default => "Skip to previous item.",
85             );
86              
87             has prev_keys => (
88             isa => "ArrayRef[Str]",
89             is => "rw",
90             default => sub { ["k", "\x{1b}[A", "\x{1b}[D" ] }, # up arrow, left arrow
91             );
92              
93             has next_help => (
94             isa => "Str",
95             is => "rw",
96             default => "Skip to next item.",
97             );
98              
99             has next_keys => (
100             isa => "ArrayRef[Str]",
101             is => "rw",
102             default => sub { ["j", "\x{1b}[B", "\x{1b}[C" ] }, # down arrow, right arrow
103             );
104              
105             # trés ugly...
106             # perhaps it should be converted to CPS style code
107             sub run {
108             my ( $self, @args ) = @_;
109              
110             my @items = $self->_get_arg_or_default( "items", @args );
111              
112             my $item_args = $self->_get_arg_or_default( "item_arguments", @args );
113              
114             tie my %answers, 'Tie::RefHash';
115              
116             my $cur_item = 0;
117             my $done = set();
118              
119             foreach my $arg (qw(options prompt prompt_format)) {
120             unshift @args, $arg => scalar( $self->_get_arg_or_default($arg, @args) );
121             }
122              
123             @answers{@items} = map { $self->get_prompt_object_and_args( @args, item => $_ ) } @items;
124              
125             loop: while ( $done->size < @items ) {
126             my $item = $items[$cur_item];
127              
128             local $@;
129              
130             my $option = $self->prompt_for_item(
131             @args,
132             %{ $answers{$item} }, # reuse the existing objects, and also pass default_option if it was already answered
133             done => $done,
134             done_count => $done->size,
135             items => \@items,
136             item_count => scalar(@items),
137             last_item => $#items,
138             item_index => $cur_item,
139             item_num => $cur_item + 1,
140             item => $item,
141             );
142              
143             if ( $option ) {
144             if ( $option->{sequence_command} ) {
145             if ( my $cb = $option->{callback} ) {
146             $self->$cb(
147             @args,
148             option => $option,
149             item_index => $cur_item,
150             cur_item_ref => \$cur_item,
151             items => \@items,
152             done => $done,
153             answer => $answers{$item},
154             answers => \%answers,
155             );
156             } else {
157             die "Sequence commands must have a callback";
158             }
159              
160             next loop;
161             } else {
162             $answers{$item}{default_option} = $option;
163              
164             $done->insert($item);
165             $cur_item = first { not exists $answers{ $items[$_] }{default_option} } 0 .. $#items;
166             $cur_item ||= 0;
167             }
168             } else {
169             # move to the end of the queue
170             push @items, splice( @items, $cur_item, 1 );
171             }
172             }
173              
174             return $self->return_answers(
175             answers => \%answers,
176             items => \@items,
177             );
178             }
179              
180             sub get_prompt_object_and_args {
181             my ( $self, %args ) = @_;
182              
183             my $prompt_object = $self->_get_arg_or_default( "prompt_object", %args );
184             my @prompt_args = $self->_get_arg_or_default( "additional_prompt_args", %args );
185              
186             my $item = $args{item};
187              
188             return {
189             %{ $self->_get_arg_or_default( item_arguments => %args )->{$item} || {} },
190             item => $item,
191             prompt_object => $prompt_object,
192             additional_prompt_args => \@prompt_args,
193             }
194             }
195              
196             sub return_answers {
197             my ( $self, %args ) = @_;
198              
199             my $answers = $args{answers};
200              
201             foreach my $item ( keys %$answers ) {
202             my ( $obj, $args, $opt ) = @{ $answers->{$item} }{qw(prompt_object additional_prompt_args default_option)};
203             $answers->{$item} = $obj->option_to_return_value( @$args, option => $opt );
204             }
205              
206             return $answers;
207             }
208              
209             sub prompt_for_item {
210             my ( $self, %args ) = @_;
211              
212             my ( $prompt, $args ) = @args{qw(prompt_object additional_prompt_args)};
213              
214             $prompt->prompt(
215             %args,
216             @$args,
217             $self->create_movement_options( %args ),
218             return_option => 1,
219             );
220             }
221              
222             sub create_movement_options {
223             my ( $self, %args ) = @_;
224              
225             my $item_count = $args{item_count};
226              
227             return if $item_count == 1; # no movement if there's just one item
228              
229             my $done_count = $args{done_count};
230             my $cur_item = $args{item_index};
231             my $last_item = $args{last_item};
232              
233             my @additional = _get_arg( additional_options => %args );
234              
235             push @additional, $self->create_prev_command(%args) if $cur_item > 0;
236             push @additional, $self->create_next_command(%args) if $cur_item < $last_item;
237             push @additional, $self->create_wait_command(%args) if $item_count > ( $done_count + 1 ); # this is not the last remaining item
238              
239             return ( additional_options => \@additional );
240             }
241              
242             sub create_prev_command {
243             my ( $self, @args ) = @_;
244              
245             $self->create_movement_option(
246             @args,
247             name => "prev",
248             doc => $self->_get_arg_or_default( prev_help => @args ),
249             keys => [ $self->_get_arg_or_default( prev_keys => @args ) ],
250             callback => sub {
251             my ( $self, %args ) = @_;
252             ${ $args{cur_item_ref} }--;
253             },
254             );
255             }
256              
257             sub create_next_command {
258             my ( $self, @args ) = @_;
259              
260             $self->create_movement_option(
261             @args,
262             name => "next",
263             doc => $self->_get_arg_or_default( next_help => @args ),
264             keys => [ $self->_get_arg_or_default( next_keys => @args ) ],
265             callback => sub {
266             my ( $self, %args ) = @_;
267             ${ $args{cur_item_ref} }++;
268             },
269             );
270             }
271              
272             sub create_wait_command {
273             my ( $self, @args ) = @_;
274              
275             $self->create_movement_option(
276             @args,
277             name => "wait",
278             doc => $self->_get_arg_or_default( wait_help => @args ),
279             keys => [ $self->_get_arg_or_default( wait_keys => @args ) ],
280             callback => sub {
281             my ( $self, %args ) = @_;
282             push @{ $args{items} }, splice( @{ $args{items} }, $args{item_index}, 1 );
283             },
284             );
285             }
286              
287             sub create_movement_option {
288             my ( $self, %args ) = @_;
289              
290             return {
291             name => $args{name},
292             doc => $args{doc},
293             keys => $args{keys},
294             callback => $args{callback},
295             sequence_command => 1,
296             };
297             }
298              
299             sub set_option_for_item {
300             my ( $self, %args ) = @_;
301              
302             my $item = $args{item};
303              
304             $args{done}->insert($item);
305              
306             $args{answers}{$item}{default_option} = $args{option};
307             }
308              
309             sub set_option_for_remaining_items {
310             my ( $self, %args ) = @_;
311              
312             $args{done}->insert(@{ $args{items} });
313              
314             my $option = $args{option};
315              
316             $_->{default_option} ||= $option for values %{ $args{answers} };
317             }
318              
319             sub set_option_for_all_items {
320             my ( $self, %args ) = @_;
321              
322             $args{done}->insert(@{ $args{items} });
323              
324             my $option = $args{option};
325              
326             $_->{default_option} = $option for values %{ $args{answers} };
327             }
328              
329             __PACKAGE__
330              
331             __END__
332              
333             =pod
334              
335             =head1 NAME
336              
337             Prompt::ReadKey::Sequence - Prompt for a series of items with additional
338             movement options.
339              
340             =head1 SYNOPSIS
341              
342             use Prompt::ReadKey::Sequence;
343              
344             my $seq = Prompt::ReadKey::Sequence->new(
345             options => ..,
346             items => \@items,
347             );
348              
349             my $answers = $seq->run;
350              
351             my $first_answer = $answers->{ $item[0] };
352              
353             =head1 DESCRIPTION
354              
355             =cut
356              
357