File Coverage

blib/lib/Config/Model/TermUI.pm
Criterion Covered Total %
statement 52 79 65.8
branch 6 24 25.0
condition 2 11 18.1
subroutine 12 14 85.7
pod 1 3 33.3
total 73 131 55.7


line stmt bran cond sub pod time code
1             #
2             # This file is part of Config-Model
3             #
4             # This software is Copyright (c) 2005-2022 by Dominique Dumont.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU Lesser General Public License, Version 2.1, February 1999
9             #
10             package Config::Model::TermUI 2.153; # TRIAL
11              
12 1     1   16099 use Carp;
  1         3  
  1         63  
13 1     1   7 use utf8; # so literals and identifiers can be in UTF-8
  1         3  
  1         5  
14 1     1   28 use v5.12; # or later to get "unicode_strings" feature
  1         4  
15 1     1   5 use strict;
  1         2  
  1         19  
16 1     1   5 use warnings;
  1         2  
  1         28  
17 1     1   461 use open qw(:std :utf8); # undeclared streams in UTF-8
  1         1271  
  1         7  
18 1     1   185 use Encode qw(decode_utf8);
  1         2  
  1         65  
19              
20 1     1   24 use Term::ReadLine;
  1         3  
  1         40  
21              
22 1     1   7 use base qw/Config::Model::SimpleUI/;
  1         2  
  1         541  
23              
24             my $completion_sub = sub {
25             my ( $self, $text, $line, $start ) = @_;
26              
27             my @choice = $self->{current_node}->get_element_name;
28             my @ret = grep { /^$text/ } @choice ;
29             return @ret;
30             };
31              
32             my $leaf_completion_sub = sub {
33             my ( $self, $text, $line, $start ) = @_;
34              
35             my @choice = $self->{current_node}->get_element_name( cargo_type => 'leaf' );
36             my @ret = grep { /^$text/ } @choice ;
37             return @ret;
38             };
39              
40             my $fix_completion_sub = sub {
41             my ( $self, $text, $line, $start ) = @_;
42              
43             my @choice = $self->{current_node}->get_element_name;
44             push @choice, '!';
45             my @ret = grep { /^$text/ } @choice ;
46             return @ret;
47             };
48              
49             my $ll_completion_sub = sub {
50             my ( $self, $text, $line, $start ) = @_;
51              
52             my @choice = $self->{current_node}->get_element_name;
53             push @choice, '-nz';
54             my @ret = grep { /^$text/ } @choice ;
55             return @ret;
56             };
57              
58             # BUG: autocompletion does not really work on a hash element with an index
59             # containing white space (i.e. something like std_id:"abc def",
60              
61             my $cd_completion_sub = sub {
62             my ( $self, $text, $line, $start ) = @_;
63              
64             # we know that text begins with 'cd '
65             my $cmd = $line;
66             $cmd =~ s/cd\s+//;
67              
68             # convert usual cd_ism ( '..' '/foo') to grab syntax ( '-' '! foo')
69             #$text =~ s(^/) (! );
70             $cmd =~ s(^\.\.$)(-)g;
71             #$text =~ s(/) ( )g;
72              
73             my $new_item;
74             while ( not defined $new_item ) {
75              
76             # grab in tolerant mode
77             #print "Grabbing $cmd\n";
78             eval {
79             $new_item = $self->{current_node}->grab( step => $cmd, type => 'node', mode => 'strict', autoadd => 0 );
80             };
81             chop $cmd;
82             }
83              
84             #print "Grab got ",$new_item->location,"\n";
85              
86             my @choice = length($line) > 3 ? () : ( '!', '-' );
87             my $new_type = $new_item->get_type;
88              
89             my @cargo = $new_item->get_element_name( cargo_type => 'node' );
90             foreach my $elt_name (@cargo) {
91             if ( $new_item->element_type($elt_name) =~ /hash|list/ ) {
92             push @choice, "$elt_name:";
93             foreach my $idx ( $new_item->fetch_element($elt_name)->fetch_all_indexes ) {
94             # my ($idx) = ($raw_idx =~ /([^\n]{1,40})/ );
95             # $idx .= '...' unless $raw_idx eq $idx ;
96             push @choice, "$elt_name:" . ($idx =~ /[^\w._-]/ ? qq("$idx") : $idx );
97             }
98             }
99             else {
100             push @choice, $elt_name;
101             }
102             }
103              
104             # filter possible choices according to input
105             my @ret = grep { /^$text/ } @choice ;
106              
107             return @ret;
108             };
109              
110             my $path_completion_sub = sub {
111             my ( $self, $text, $line, $start, $node_only ) = @_;
112              
113             # we know that text begins with a command
114             my $cmd = $line;
115             $cmd =~ s/^\w+\s+//;
116              
117             my $new_item;
118             while ( not defined $new_item ) {
119             # grab in tolerant mode
120             # print "Grabbing $cmd\n";
121             eval {
122             $new_item = $self->{current_node}->grab( step => $cmd, type => 'node', mode => 'strict', autoadd => 0 );
123             };
124             chop $cmd;
125             }
126              
127             #print "Grab got ",$new_item->location,"\n";
128              
129             my @choice;
130             my $new_type = $new_item->get_type;
131              
132             my @children = $node_only ? $new_item->get_element_name( cargo_type => 'node' )
133             : $new_item->get_element_name();
134             # say "Children: @children";
135             foreach my $elt_name (@children) {
136             if ( $new_item->element_type($elt_name) =~ /^(hash|list)$/ ) {
137             push @choice, "$elt_name:" unless $node_only;
138             foreach my $idx ( $new_item->fetch_element($elt_name)->fetch_all_indexes ) {
139             # my ($idx) = ($raw_idx =~ /([^\n]{1,40})/ );
140             # $idx .= '...' unless $raw_idx eq $idx ;
141             push @choice, "$elt_name:" . ($idx =~ /[^\w._-]/ ? qq("$idx") : $idx );
142             }
143             }
144             else {
145             push @choice, $elt_name;
146             }
147             }
148              
149             # filter possible choices according to input
150             my @ret = grep { /^$text/ } @choice ;
151              
152             return @ret;
153             };
154              
155             # like path completion, but allow only completion on a node
156             my $node_completion_sub = sub {
157             return $path_completion_sub->(@_, 1);
158             };
159              
160             my %completion_dispatch = (
161             cd => $cd_completion_sub,
162             desc => $completion_sub,
163             display => $completion_sub,
164             ll => $ll_completion_sub,
165             ls => $path_completion_sub,
166             tree => $node_completion_sub,
167             info => $path_completion_sub,
168             check => $completion_sub,
169             fix => $fix_completion_sub,
170             clear => $completion_sub,
171             set => $leaf_completion_sub,
172             delete => $leaf_completion_sub,
173             reset => $completion_sub,
174             );
175              
176             sub completion {
177 14     14 0 20215 my ( $self, $text, $line, $start ) = @_;
178              
179 14         38 my $space_idx = index $line, ' ';
180 14         84 my ( $main, $cmd ) = split m/\s+/, $line, 2; # /;
181             #warn " comp main cmd is '$main' (space_idx $space_idx)\n";
182              
183 14 100 66     88 if ( $space_idx > 0 and defined $completion_dispatch{$main} ) {
    50          
184 13         56 my $i = $self->{current_node}->instance;
185             # say "Input: ['$text', '$line', $start], ";
186              
187 13         59 my @choices = $completion_dispatch{$main}->( $self, $text, $line, $start );
188             # say "Choices: ['", join("', '",@choices),"']";
189 13         56 return @choices;
190             }
191             elsif ( not $cmd ) {
192 1         42 return grep { /^$text/ } $self->simple_ui_commands() ;
  18         69  
193             }
194              
195 0         0 return ();
196             }
197              
198             sub new {
199 1     1 0 2 my $type = shift;
200 1         6 my %args = @_;
201              
202 1         2 my $self = {};
203              
204 1         3 foreach my $p (qw/root title prompt/) {
205 3 50       11 $self->{$p} = delete $args{$p}
206             or croak "TermUI->new: Missing $p parameter";
207             }
208              
209 1         5 $self->{current_node} = $self->{root};
210              
211 1         18 my $term = Term::ReadLine->new( $self->{title} );
212              
213 1     0   15739 my $sub_ref = sub { $self->completion(@_); };
  0         0  
214              
215 1         15 my $word_break_string = "\\\t\n' `\@\$><;|&{(";
216              
217 1 50       23 if ( $term->ReadLine eq "Term::ReadLine::Gnu" ) {
    50          
218              
219             # See Term::ReadLine::Gnu / Custom Completion
220 0         0 my $attribs = $term->Attribs;
221 0         0 $attribs->{completion_function} = $sub_ref;
222 0         0 $attribs->{completer_word_break_characters} = $word_break_string;
223             # this method is available only on Term::ReadLine::Gnu > 1.32
224 0 0       0 $term->enableUTF8 if $term->can('enableUTF8');
225             }
226             elsif ( $term->ReadLine eq "Term::ReadLine::Perl" ) {
227 1     1   12 no warnings "once";
  1         4  
  1         430  
228 0         0 warn "utf-8 support has not beed tested with Term::ReadLine::Perl. ",
229             "You should install Term::ReadLine::Gnu.\n";
230 0         0 $readline::rl_completion_function = $sub_ref;
231 0         0 &readline::rl_set( rl_completer_word_break_characters => $word_break_string );
232              
233             # &readline::rl_set('TcshCompleteMode', 'On');
234             }
235             else {
236 1         124 warn "You should install Term::ReadLine::Gnu for autocompletion and utf-8 support.\n";
237             }
238              
239 1         30 $self->{term} = $term;
240              
241 1         18 foreach my $p (qw//) {
242 0 0       0 $self->{$p} = delete $args{$p} if defined $args{$p};
243             }
244              
245 1         58 bless $self, $type;
246             }
247              
248             sub run_loop {
249 0     0 1   my $self = shift;
250              
251 0           my $term = $self->{term};
252              
253 0   0       my $OUT = $term->OUT || \*STDOUT;
254 0           my $user_cmd;
255 0           while ( defined( $user_cmd = $term->readline( $self->prompt ) ) ) {
256 0 0 0       last if $user_cmd eq 'exit' or $user_cmd eq 'quit';
257 0           $user_cmd = decode_utf8($user_cmd,1);
258             #print $OUT "cmd: $user_cmd\n";
259 0           my $res = $self->run($user_cmd);
260 0 0 0       print $OUT $res, "\n" if defined $res and $res;
261             ## $term->addhistory($_) if defined $_ && /\S/;
262             }
263 0           print "\n";
264              
265 0           my $instance = $self->{root}->instance;
266 0 0         if ( $instance->c_count ) {
267 0 0         if ($instance->has_changes) {
268 0           $instance->say_changes;
269 0           $user_cmd = $term->readline("write back data before exit ? (Y/n)");
270 0 0         $instance->write_back unless $user_cmd =~ /n/i;
271 0           print "\n";
272             }
273             }
274             }
275              
276             1;
277              
278             # ABSTRACT: Interactive command line interface for cme
279              
280             __END__
281              
282             =pod
283              
284             =encoding UTF-8
285              
286             =head1 NAME
287              
288             Config::Model::TermUI - Interactive command line interface for cme
289              
290             =head1 VERSION
291              
292             version 2.153
293              
294             =head1 SYNOPSIS
295              
296             use Config::Model;
297             use Config::Model::TermUI ;
298              
299             # define configuration tree object
300             my $model = Config::Model->new;
301             $model->create_config_class(
302             name => "Foo",
303             element => [
304             [qw/foo bar/] => {
305             type => 'leaf',
306             value_type => 'string'
307             },
308             ]
309             );
310             $model ->create_config_class (
311             name => "MyClass",
312              
313             element => [
314              
315             [qw/foo bar/] => {
316             type => 'leaf',
317             value_type => 'string'
318             },
319             hash_of_nodes => {
320             type => 'hash', # hash id
321             index_type => 'string',
322             cargo => {
323             type => 'node',
324             config_class_name => 'Foo'
325             },
326             },
327             ],
328             ) ;
329              
330             my $inst = $model->instance(root_class_name => 'MyClass' );
331              
332             my $root = $inst->config_root ;
333              
334             # put data
335             my $steps = 'foo=FOO hash_of_nodes:fr foo=bonjour -
336             hash_of_nodes:en foo=hello ';
337             $root->load( steps => $steps );
338              
339             my $ui = Config::Model::TermUI->new(
340             root => $root ,
341             title => 'My class ui',
342             prompt => 'class ui',
343             );
344              
345             # engage in user interaction
346             $ui -> run_loop ;
347              
348             print $root->dump_tree ;
349              
350             Once the synopsis above has been saved in C<my_test.pl>, you can achieve the
351             same interactions as with C<Config::Model::SimpleUI>. Except that you can use
352             TAB completion:
353              
354             class ui:$ ls
355             foo bar hash_of_nodes
356             class ui:$ ll hash_of_nodes
357             name value type comment
358             hash_of_nodes <Foo> node hash keys: "en" "fr"
359              
360             class ui:$ cd hash_of_nodes:en
361             class ui: hash_of_nodes:en $ ll
362             name value type comment
363             foo hello string
364             bar [undef] string
365              
366             class ui: hash_of_nodes:en $ set bar=bonjour
367             class ui: hash_of_nodes:en $ ll
368             name value type comment
369             foo hello string
370             bar bonjour string
371              
372             class ui: hash_of_nodes:en $ ^D
373              
374             At the end, the test script dumps the configuration tree. The modified
375             C<bar> value can be found in there:
376              
377             foo=FOO
378             hash_of_nodes:en
379             foo=hello
380             bar=bonjour -
381             hash_of_nodes:fr
382             foo=bonjour - -
383              
384             =head1 DESCRIPTION
385              
386             This module provides a helper to construct pure ASCII user interface
387             on top of L<Term::ReadLine>. To get better interaction you must
388             install either L<Term::ReadLine::Gnu> or L<Term::ReadLine::Perl>.
389              
390             Depending on your installation, either L<Term::ReadLine::Gnu> or
391             L<Term::ReadLine::Perl> is used. See L<Term::ReadLine> to
392             override default choice.
393              
394             =head1 Dependencies
395              
396             This module is optional and depends on L<Term::ReadLine> to work. To
397             reduce the dependency list of L<Config::Model>, C<Term::ReadLine> is
398             only recommended. L<cme> gracefully degrades to
399             L<Config::Model::SimpleUI> when necessary.
400              
401             =head1 USER COMMAND SYNTAX
402              
403             See L<Config::Model::SimpleUI/"USER COMMAND SYNTAX">.
404              
405             =head1 CONSTRUCTOR
406              
407             =head2 parameters
408              
409             =over
410              
411             =item root
412              
413             Root node of the configuration tree
414              
415             =item title
416              
417             UI title
418              
419             =item prompt
420              
421             UI prompt. The prompt will be completed with the location of the
422             current node.
423              
424             =back
425              
426             =head1 Methods
427              
428             =head2 run_loop
429              
430             Engage in user interaction until user enters '^D' (CTRL-D).
431              
432             =head1 BUGS
433              
434             =over
435              
436             =item *
437              
438             Auto-completion is not complete.
439              
440             =item *
441              
442             Auto-completion provides wrong choice when you try to C<cd> in a hash
443             where the index contains a white space. I.e. the correct command is
444             C<cd foo:"a b"> instead of C<cd foo: "a b"> as proposed by auto
445             completion.
446              
447             =back
448              
449             =head1 AUTHOR
450              
451             Dominique Dumont, (ddumont at cpan dot org)
452              
453             =head1 SEE ALSO
454              
455             L<Config::Model>,
456             L<Config::Model::Instance>,
457             L<Config::Model::Node>,
458              
459             =head1 AUTHOR
460              
461             Dominique Dumont
462              
463             =head1 COPYRIGHT AND LICENSE
464              
465             This software is Copyright (c) 2005-2022 by Dominique Dumont.
466              
467             This is free software, licensed under:
468              
469             The GNU Lesser General Public License, Version 2.1, February 1999
470              
471             =cut