File Coverage

blib/lib/Term/Form/ReadLine.pm
Criterion Covered Total %
statement 48 332 14.4
branch 3 154 1.9
condition 0 52 0.0
subroutine 15 27 55.5
pod 2 3 66.6
total 68 568 11.9


line stmt bran cond sub pod time code
1             package Term::Form::ReadLine;
2              
3 2     2   2130 use warnings;
  2         25  
  2         196  
4 2     2   14 use strict;
  2         4  
  2         58  
5 2     2   31 use 5.10.1;
  2         6  
6              
7             our $VERSION = '0.562';
8 2     2   11 use Exporter 'import';
  2         3  
  2         134  
9             our @EXPORT_OK = qw( read_line );
10              
11 2     2   12 use parent qw( Term::Form );
  2         4  
  2         53  
12              
13 2     2   223 use Carp qw( croak );
  2         4  
  2         161  
14 2     2   11 use List::Util qw( none );
  2         5  
  2         214  
15              
16 2     2   12 use Term::Choose::LineFold qw( line_fold print_columns );
  2         4  
  2         149  
17 2     2   11 use Term::Choose::Constants qw( :all );
  2         4  
  2         768  
18 2     2   15 use Term::Choose::Screen qw( :all );
  2         3  
  2         419  
19 2     2   16 use Term::Choose::Util qw( get_term_width get_term_height );
  2         3  
  2         169  
20 2     2   12 use Term::Choose::ValidateOptions qw( validate_options );
  2         3  
  2         255  
21              
22              
23             my $Plugin;
24              
25             BEGIN {
26 2 50   2   13 if ( $^O eq 'MSWin32' ) {
27 0         0 require Term::Choose::Win32;
28 0         0 require Win32::Console::ANSI;
29 0         0 $Plugin = 'Term::Choose::Win32';
30             }
31             else {
32 2         21 require Term::Choose::Linux;
33 2         8686 $Plugin = 'Term::Choose::Linux';
34             }
35             }
36              
37              
38             sub new {
39 1     1 1 3 my $class = shift;
40 1 50       5 croak "new: called with " . @_ . " arguments - 0 or 1 arguments expected." if @_ > 1;
41 1         3 my ( $opt ) = @_;
42 1         4 my $instance_defaults = _defaults();
43 1 50       3 if ( defined $opt ) {
44 0 0       0 croak "new: The (optional) argument is not a HASH reference." if ref $opt ne 'HASH';
45 0         0 my $caller = 'new';
46 0         0 validate_options( _valid_options(), $opt, $caller );
47 0         0 for my $key ( keys %$opt ) {
48 0 0       0 $instance_defaults->{$key} = $opt->{$key} if defined $opt->{$key};
49             }
50             }
51 1         4 my $self = bless $instance_defaults, $class;
52 1         10 $self->{backup_instance_defaults} = { %$instance_defaults };
53 1         6 $self->{plugin} = $Plugin->new();
54 1         22 return $self;
55             }
56              
57              
58             sub _valid_options {
59             return {
60 0     0   0 codepage_mapping => '[ 0 1 ]',
61             show_context => '[ 0 1 ]',
62             clear_screen => '[ 0 1 2 ]',
63             color => '[ 0 1 2 ]',
64             hide_cursor => '[ 0 1 2 ]', # hide_cursor == 2 # documentation
65             no_echo => '[ 0 1 2 ]',
66             page => '[ 0 1 2 ]', # undocumented
67             history => 'Array_Str',
68             default => 'Str',
69             footer => 'Str', # undocumented
70             info => 'Str',
71             };
72             }
73              
74              
75             sub _defaults {
76             return {
77 1     1   13 clear_screen => 0,
78             codepage_mapping => 0,
79             color => 0,
80             default => '',
81             footer => '',
82             hide_cursor => 1,
83             history => [],
84             info => '',
85             no_echo => 0,
86             page => 1,
87             show_context => 0,
88             };
89             }
90              
91              
92             sub __before_readline {
93 0     0     my ( $self, $m, $term_w ) = @_;
94 0 0         if ( $self->{show_context} ) {
95 0           my @pre_text_array;
96 0 0         if ( $m->{diff} ) {
97 0           my $line = '';
98 0           my $line_w = 0;
99 0           for my $i ( reverse( 0 .. $m->{diff} - 1 ) ) {
100 0 0         if ( $line_w + $m->{str}[$i][1] > $term_w ) {
101 0           unshift @pre_text_array, $line;
102 0           $line = $m->{str}[$i][0];
103 0           $line_w = $m->{str}[$i][1];
104             }
105             else {
106 0           $line = $m->{str}[$i][0] . $line;
107 0           $line_w = $m->{str}[$i][1] + $line_w;
108             }
109             }
110 0           my $top_pre_line_w = $self->{i}{max_key_w} + $line_w;
111 0 0         if ( $top_pre_line_w <= $term_w ) {
112 0           my $empty_w = $term_w - $top_pre_line_w;
113 0           unshift @pre_text_array, $self->{i}{prompt} . ( ' ' x $empty_w ) . $line;
114             }
115             else {
116 0           my $empty_w = $term_w - $line_w;
117 0           unshift @pre_text_array, ' ' x $empty_w . $line;
118 0           unshift @pre_text_array, $self->{i}{prompt};
119             }
120 0           $self->{i}{keys}[0] = '';
121             }
122             else {
123 0 0         if ( ( $m->{str_w} + $self->{i}{max_key_w} ) <= $term_w ) {
124 0           $self->{i}{keys}[0] = $self->{i}{prompt};
125             }
126             else {
127 0 0         if ( length $self->{i}{prompt} ) { #
128 0           unshift @pre_text_array, $self->{i}{prompt};
129             }
130 0           $self->{i}{keys}[0] = '';
131             }
132             }
133 0           $self->{i}{pre_text} = join "\n", @pre_text_array;
134 0           $self->{i}{pre_text_row_count} = scalar @pre_text_array;
135             }
136             else {
137 0           $self->{i}{keys}[0] = $self->{i}{prompt};
138             }
139             }
140              
141              
142             sub __after_readline {
143 0     0     my ( $self, $m, $term_w ) = @_;
144 0           my $count_chars_after = @{$m->{str}} - ( @{$m->{p_str}} + $m->{diff} );
  0            
  0            
145 0 0 0       if ( ! $self->{show_context} || ! $count_chars_after ) {
146 0           $self->{i}{post_text} = '';
147 0           $self->{i}{post_text_row_count} = 0;
148 0           return;
149             }
150 0           my @post_text_array;
151 0           my $line = '';
152 0           my $line_w = 0;
153 0           for my $i ( ( @{$m->{str}} - $count_chars_after ) .. $#{$m->{str}} ) {
  0            
  0            
154 0 0         if ( $line_w + $m->{str}[$i][1] > $term_w ) {
155 0           push @post_text_array, $line;
156 0           $line = $m->{str}[$i][0];
157 0           $line_w = $m->{str}[$i][1];
158 0           next;
159             }
160 0           $line = $line . $m->{str}[$i][0];
161 0           $line_w = $line_w + $m->{str}[$i][1];
162             }
163 0 0         if ( $line_w ) {
164 0           push @post_text_array, $line;
165             }
166 0           $self->{i}{post_text} = join "\n", @post_text_array;
167 0           $self->{i}{post_text_row_count} = scalar @post_text_array;
168             }
169              
170              
171             sub __print_footer {
172 0     0     my ( $self ) = @_;
173 0           my $empty = get_term_height() - $self->{i}{info_row_count} - 1;
174 0           my $footer_line = sprintf $self->{i}{footer_fmt}, $self->{i}{page_count};
175 0 0         if ( $empty > 0 ) {
176 0           print "\n" x $empty;
177 0           print $footer_line;
178 0           print up( $empty );
179             }
180             else {
181 0 0         if ( get_term_height >= 2 ) { ##
182 0           print "\n";
183 0           print $footer_line;
184 0           print up( 1 );
185             }
186             }
187             }
188              
189              
190             sub __modify_readline_options {
191 0     0     my ( $self ) = @_;
192 0 0 0       if ( length $self->{footer} && $self->{page} != 2 ) {
193 0           $self->{page} = 2;
194             }
195 0 0 0       if ( $self->{page} == 2 && $self->{clear_screen} != 1 ) {
196 0           $self->{clear_screen} = 1;
197             }
198 0           $self->{history} = [ reverse grep { length } @{$self->{history}} ];
  0            
  0            
199             }
200              
201              
202             sub __select_history {
203 0     0     my ( $self, $m, $prompt, $history_up ) = @_;
204 0 0         if ( ! @{$self->{history}} ) {
  0            
205 0           return $m;
206             }
207 0           my $current = join '', map { $_->[0] } @{$m->{str}};
  0            
  0            
208 0 0   0     if ( none { $_ eq $current } @{$self->{history}} ) {
  0            
  0            
209 0           $self->{i}{curr_string} = $current;
210             }
211 0           my @history = @{$self->{history}};
  0            
212 0 0         if ( ! defined $self->{i}{history_idx} ) {
213 0           $self->{i}{history_idx} = @history;
214             # first up-key pressed -> last history entry and not curr_string
215             }
216 0   0       push @history, $self->{i}{curr_string} // '';
217 0 0         if ( $history_up ) {
218 0 0         if ( $self->{i}{history_idx} == 0 ) {
219 0           $self->{i}{beep} = 1;
220             }
221             else {
222 0           --$self->{i}{history_idx};
223             }
224             }
225             else {
226 0 0         if ( $self->{i}{history_idx} >= $#history ) {
227 0           $self->{i}{beep} = 1;
228             }
229             else {
230 0           ++$self->{i}{history_idx};
231             }
232             }
233 0           my $list = [ [ $prompt, $history[$self->{i}{history_idx}] ] ];
234 0           $m = $self->__string_and_pos( $list );
235 0           return $m;
236             }
237              
238              
239             sub __prepare_prompt {
240 0     0     my ( $self, $term_w, $prompt ) = @_;
241 0 0         if ( ! length $prompt ) {
242 0           $self->{i}{prompt} = '';
243 0           $self->{i}{max_key_w} = 0;
244 0           return;
245             }
246 0           my @color;
247 0 0         if ( $self->{color} ) {
248 0           $prompt =~ s/${\PH}//g;
  0            
249 0 0         $prompt =~ s/(${\SGR_ES})/push( @color, $1 ) && ${\PH}/ge;
  0            
  0            
  0            
250             }
251 0           $prompt = $self->__sanitized_string( $prompt );
252 0           $self->{i}{max_key_w} = print_columns( $prompt );
253 0 0         if ( $self->{i}{max_key_w} > $term_w / 3 ) {
254 0           $self->{i}{max_key_w} = int( $term_w / 3 );
255 0           $prompt = $self->__unicode_trim( $prompt, $self->{i}{max_key_w} );
256             }
257 0 0         if ( @color ) {
258 0           $prompt =~ s/${\PH}/shift @color/ge;
  0            
  0            
259 0           $prompt .= normal();
260             }
261 0           $self->{i}{prompt} = $prompt;
262             }
263              
264              
265             sub __init_readline {
266 0     0     my ( $self, $term_w, $prompt ) = @_;
267 0 0         if ( $self->{clear_screen} == 0 ) {
    0          
268 0           print "\r" . clear_to_end_of_screen();
269             }
270             elsif ( $self->{clear_screen} == 1 ) {
271 0           print clear_screen();
272             }
273 0 0         if ( length $self->{info} ) {
274 0           my $info_w = $term_w + EXTRA_W;
275 0           my @info = line_fold( $self->{info}, { width => $info_w, color => $self->{color}, join => 0 } );
276 0           $self->{i}{info_row_count} = @info;
277 0 0         if ( $self->{clear_screen} == 2 ) {
278 0           print clear_to_end_of_line();
279 0           print join( "\n" . clear_to_end_of_line(), @info ), "\n";
280             }
281             else {
282 0           print join( "\n", @info ), "\n";
283             }
284             }
285             else {
286 0           $self->{i}{info_row_count} = 0;
287             }
288 0           $self->{i}{seps}[0] = $self->{i}{sep} = ''; # in __readline
289 0           $self->{i}{char_trimmed} = '~ ';
290 0           $self->{i}{char_trimmed_w} = length $self->{i}{char_trimmed};
291 0           $self->{i}{curr_row} = 0; # in __readlline and __string_and_pos
292 0           $self->{i}{pre_text_row_count} = 0;
293 0           $self->{i}{post_text_row_count} = 0;
294 0           $self->__prepare_prompt( $term_w, $prompt );
295 0 0         if ( $self->{show_context} ) {
296 0           $self->{i}{arrow_left} = '';
297 0           $self->{i}{arrow_right} = '';
298 0           $self->{i}{arrow_w} = 0;
299 0           $self->{i}{avail_w} = $term_w;
300             }
301             else {
302 0           $self->{i}{arrow_left} = '<';
303 0           $self->{i}{arrow_right} = '>';
304 0           $self->{i}{arrow_w} = 1;
305 0           $self->__available_width( $term_w );
306             }
307 0           $self->__threshold_width();
308 0 0         if ( $self->{page} == 2 ) {
309 0           $self->{i}{page_count} = 1;
310 0           $self->{i}{print_footer} = 1;
311 0           $self->__prepare_footer_fmt( $term_w );
312 0           $self->__print_footer();
313             }
314             else {
315 0           $self->{i}{print_footer} = 0;
316             }
317 0           my $list = [ [ $prompt, $self->{default} ] ];
318 0           my $m = $self->__string_and_pos( $list );
319 0           return $m;
320             }
321              
322              
323             sub read_line {
324 0 0   0 0   if ( ref $_[0] eq __PACKAGE__ ) {
325 0           croak "\"read_line\" is a function. The method is called \"readline\"";
326             }
327 0           my $ob = __PACKAGE__->new();
328 0           delete $ob->{backup_instance_defaults};
329 0           return $ob->readline( @_ );
330             }
331              
332              
333             sub readline {
334 0     0 1   my ( $self, $prompt, $opt ) = @_;
335 0 0         $prompt = '' if ! defined $prompt;
336 0 0         croak "readline: a reference is not a valid prompt." if ref $prompt;
337 0 0         $opt = {} if ! defined $opt;
338 0 0         if ( ! ref $opt ) {
    0          
339 0           $opt = { default => $opt };
340             }
341             elsif ( ref $opt ne 'HASH' ) {
342 0           croak "readline: the (optional) second argument must be a string or a HASH reference";
343             }
344 0 0         if ( %$opt ) {
345 0           my $caller = 'readline';
346 0           validate_options( _valid_options( $caller ), $opt, $caller );
347 0           for my $key ( keys %$opt ) {
348 0 0         $self->{$key} = $opt->{$key} if defined $opt->{$key};
349             }
350             }
351 0           $self->__modify_readline_options();
352 0 0         if ( $^O eq 'MSWin32' ) {
353 0 0         print $self->{codepage_mapping} ? "\e(K" : "\e(U";
354             }
355 0           local $| = 1;
356             local $SIG{INT} = sub {
357 0     0     $self->__reset(); #
358 0           print "^C\n";
359 0           exit;
360 0           };
361 0           $self->__init_term();
362 0           my $term_w = get_term_width();
363 0           my $m = $self->__init_readline( $term_w, $prompt );
364 0           my $big_step = 10;
365 0           my $up_before = 0;
366              
367 0           CHAR: while ( 1 ) {
368 0 0         if ( $self->{i}{beep} ) {
369 0           print bell();
370 0           $self->{i}{beep} = 0;
371             }
372 0           my $tmp_term_w = get_term_width();
373 0 0         if ( $tmp_term_w != $term_w ) {
374 0           $term_w = $tmp_term_w;
375 0           $self->{default} = join( '', map { $_->[0] } @{$m->{str}} );
  0            
  0            
376 0           $m = $self->__init_readline( $term_w, $prompt );
377             }
378 0 0         if ( $self->{show_context} ) {
379 0 0         if ( ( $self->{i}{pre_text_row_count} + 2 + $self->{i}{post_text_row_count} ) >= get_term_height() ) { ##
380 0           $self->{show_context} = 0;
381 0           $up_before = 0;
382 0           $self->{default} = join( '', map { $_->[0] } @{$m->{str}} );
  0            
  0            
383 0           $m = $self->__init_readline( $term_w, $prompt );
384             }
385 0           $self->{i}{context_count} = $self->{i}{pre_text_row_count} + $self->{i}{post_text_row_count};
386             }
387 0 0         if ( $up_before ) {
388 0           print up( $up_before );
389             }
390 0           my $p = "\r" . clear_to_end_of_line();
391 0 0 0       if ( $self->{i}{prev_context_count} || $self->{i}{context_count} ) {
392             my $count = $self->{i}{prev_context_count} // 0 > $self->{i}{context_count} // 0
393             ? $self->{i}{prev_context_count}
394 0 0 0       : $self->{i}{context_count};
      0        
395 0           ++$count; # Home
396 0           $p .= ( down( 1 ) . clear_to_end_of_line() ) x $count;
397 0           $p .= up( $count );
398             }
399 0           print $p;
400 0           $self->__before_readline( $m, $term_w );
401 0           $up_before = $self->{i}{pre_text_row_count};
402 0 0         if ( $self->{hide_cursor} ) {
403 0           print hide_cursor(); # '__print_readline' activates cursor
404             }
405 0 0         if ( length $self->{i}{pre_text} ) {
406 0           print $self->{i}{pre_text}, "\n";
407             }
408              
409 0           $self->__after_readline( $m, $term_w );
410 0 0         if ( length $self->{i}{post_text} ) {
411 0           print "\n" . $self->{i}{post_text};
412             }
413 0 0         if ( $self->{i}{post_text_row_count} ) {
414 0           print up( $self->{i}{post_text_row_count} );
415             }
416 0           $self->{i}{prev_context_count} = $self->{i}{context_count};
417 0           $self->__print_readline( $m );
418 0           my $char = $self->{plugin}->__get_key_OS();
419 0 0         if ( ! defined $char ) {
420 0           $self->__reset();
421 0           warn "EOT: $!";
422 0           return;
423             }
424             # reset $m->{avail_w} to default:
425 0           $m->{avail_w} = $self->{i}{avail_w};
426 0           $self->__threshold_char_count( $m );
427 0 0 0       if ( $char == NEXT_get_key ) { next CHAR }
  0 0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
428 0           elsif ( $char == KEY_TAB ) { next CHAR }
429 0 0         elsif ( $char == VK_PAGE_UP || $char == CONTROL_P ) { for ( 1 .. $big_step ) { last if $m->{pos} == 0; $self->__left( $m ) } }
  0            
  0            
430 0 0         elsif ( $char == VK_PAGE_DOWN || $char == CONTROL_N ) { for ( 1 .. $big_step ) { last if $m->{pos} == @{$m->{str}}; $self->__right( $m ) } }
  0            
  0            
  0            
431 0           elsif ( $char == CONTROL_U ) { $self->__ctrl_u( $m ) }
432 0           elsif ( $char == CONTROL_K ) { $self->__ctrl_k( $m ) }
433 0           elsif ( $char == VK_RIGHT || $char == CONTROL_F ) { $self->__right( $m ) }
434 0           elsif ( $char == VK_LEFT || $char == CONTROL_B ) { $self->__left( $m ) }
435 0           elsif ( $char == VK_END || $char == CONTROL_E ) { $self->__end( $m ) }
436 0           elsif ( $char == VK_HOME || $char == CONTROL_A ) { $self->__home( $m ) }
437 0           elsif ( $char == KEY_BSPACE || $char == CONTROL_H ) { $self->__bspace( $m ) }
438 0           elsif ( $char == VK_DELETE || $char == CONTROL_D ) { $self->__delete( $m ) }
439 0           elsif ( $char == VK_UP || $char == CONTROL_S ) { $m = $self->__select_history( $m, $prompt, 1 ) }
440 0           elsif ( $char == VK_DOWN || $char == CONTROL_T ) { $m = $self->__select_history( $m, $prompt, 0 ) }
441             elsif ( $char == CONTROL_X ) {
442 0 0         if ( @{$m->{str}} ) {
  0            
443 0           my $list = [ [ $prompt, '' ] ];
444 0           $m = $self->__string_and_pos( $list );
445             }
446             else {
447 0           $self->__reset( $self->{i}{info_row_count} + $self->{i}{pre_text_row_count} );
448 0           return;
449             }
450             }
451             elsif ( $char == VK_INSERT ) {
452 0           $self->{i}{beep} = 1;
453             }
454             elsif ( $char == LINE_FEED || $char == CARRIAGE_RETURN ) {
455             # LINE_FEED == CONTROL_J, CARRIAGE_RETURN == CONTROL_M
456 0           $self->__reset( $self->{i}{info_row_count} + $self->{i}{pre_text_row_count} );
457 0           return join( '', map { $_->[0] } @{$m->{str}} );
  0            
  0            
458             }
459             else {
460 0           $char = chr $char;
461 0           utf8::upgrade $char;
462 0           $self->__add_char( $m, $char );
463             }
464             }
465             }
466              
467              
468             1;
469              
470             __END__