File Coverage

blib/lib/Circle/FE/Term/Widget/Entry.pm
Criterion Covered Total %
statement 9 63 14.2
branch 0 16 0.0
condition 0 3 0.0
subroutine 3 11 27.2
pod n/a
total 12 93 12.9


line stmt bran cond sub pod time code
1             # You may distribute under the terms of the GNU General Public License
2             #
3             # (C) Paul Evans, 2010-2015 -- leonerd@leonerd.org.uk
4              
5             package Circle::FE::Term::Widget::Entry;
6              
7 1     1   1812 use strict;
  1         3  
  1         28  
8 1     1   4 use constant type => "Entry";
  1         2  
  1         536  
9              
10             sub build
11             {
12 0     0     my $class = shift;
13 0           my ( $obj, $tab ) = @_;
14              
15 0           my $autoclear = $obj->prop("autoclear");
16              
17 0   0       my $want_typing = $obj->can_property( "want_typing" ) && $obj->prop( "want_typing" );
18              
19 0           my $prehistory;
20             my $history_index;
21              
22 0           my $pending_count = 0;
23              
24             my $widget = Circle::FE::Term::Widget::Entry::Widget->new(
25             classes => $obj->prop( "classes" ),
26             tab => $tab,
27              
28             on_enter => sub {
29 0     0     my ( $self, $line ) = @_;
30              
31 0           $pending_count++;
32 0           $self->send_pending( $pending_count );
33              
34             $tab->adopt_future(
35             $obj->call_method(
36             enter => $self->text,
37             )->on_done( sub {
38 0           $pending_count--;
39 0           $self->send_pending( $pending_count );
40             })->on_fail( sub {
41 0           my ( $message ) = @_;
42 0           warn "Failed while sending text:\n$message";
43 0           $pending_count--;
44 0           $self->send_pending( $pending_count );
45             })
46 0           );
47              
48 0 0         $self->set_text( "" ) if $autoclear;
49 0           undef $history_index;
50             },
51              
52             ( $want_typing ? (
53             on_typing => sub {
54 0     0     my ( $typing ) = @_;
55 0           $tab->adopt_future( $obj->call_method( typing => $typing ) );
56             }
57 0 0         ) : () ),
58             );
59              
60             $tab->adopt_future(
61             $obj->watch_property_with_initial(
62             "text",
63             on_set => sub {
64 0     0     my ( $text ) = @_;
65 0 0         $text = "" unless defined $text;
66 0           $widget->set_text( $text );
67             },
68             )
69 0           );
70              
71             $tab->adopt_future(
72             $obj->watch_property_with_initial(
73             "history",
74       0     on_updated => sub {}, # We ignore this, we just want a local cache
75             )
76 0           );
77              
78             $tab->adopt_future(
79             $obj->watch_property_with_initial(
80             "completions",
81       0     on_updated => sub {}, # We ignore this, we just want a local cache
82             )
83 0           );
84              
85             $widget->bind_keys(
86             Up => sub {
87 0     0     my $widget = shift;
88              
89 0           my $history = $obj->prop("history");
90 0 0         if( !defined $history_index ) {
    0          
91 0           $prehistory = $widget->text;
92 0 0         return 1 unless @$history;
93 0           $history_index = $#$history;
94             }
95             elsif( $history_index == 0 ) {
96             # Don't move
97 0           return 1;
98             }
99             else {
100 0           $history_index--;
101             }
102              
103 0           my $line = $history->[$history_index];
104 0           $widget->set_text( $line );
105 0           $widget->set_position( length( $line ) ); # TODO: accept negative
106              
107 0           return 1;
108             },
109             Down => sub {
110 0     0     my $widget = shift;
111              
112 0           my $history = $obj->prop("history");
113 0 0         return 1 unless defined $history_index;
114 0 0         if( $history_index < $#$history ) {
115 0           $history_index++;
116             }
117             else {
118 0           $widget->set_text( $prehistory );
119 0           undef $history_index;
120 0           return 1;
121             }
122              
123 0           my $line = $history->[$history_index];
124 0           $widget->set_text( $line );
125 0           $widget->set_position( length( $line ) );
126              
127 0           return 1;
128             },
129 0           );
130              
131 0           $widget->{obj} = $obj;
132              
133 0           return $widget;
134             }
135              
136             Tickit::Style->load_style( <<'EOF' );
137             Circle::FE::Term::Widget::Entry::Widget.topic {
138             bg: "blue";
139             }
140              
141             EOF
142              
143             package Circle::FE::Term::Widget::Entry::Widget;
144              
145 1     1   6 use base qw( Tickit::Widget::Entry );
  1         2  
  1         304  
146             Tickit::Window->VERSION( '0.42' );
147              
148             Tickit::Async->VERSION( '0.21' ); # ->cancel_timer
149              
150             use Tickit::Style -copy;
151              
152             use constant KEYPRESSES_FROM_STYLE => 1;
153              
154             style_definition base =>
155             '' => "tab_complete";
156              
157             use Tickit::Utils qw( textwidth );
158             use List::Util qw( max );
159              
160             use constant PEN_UNDER => Tickit::Pen->new( u => 1 );
161              
162             sub new
163             {
164             my $class = shift;
165             my %args = @_;
166              
167             my $tab = delete $args{tab};
168             my $on_typing = delete $args{on_typing};
169              
170             if( $on_typing ) {
171             my $on_enter = $args{on_enter};
172             $args{on_enter} = sub {
173             my ( $self ) = @_;
174             $self->stopped_typing;
175             $on_enter->( @_ );
176             };
177             }
178              
179             my $self = $class->SUPER::new( %args );
180              
181             $self->{tab} = $tab;
182             $self->{on_typing} = $on_typing;
183              
184             return $self;
185             }
186              
187             sub on_key
188             {
189             my $self = shift;
190             my ( $ev ) = @_;
191              
192             $self->{tab}->activated;
193              
194             my $redo_tab_complete;
195             if( my $popup = delete $self->{tab_complete_popup} ) {
196             $popup->hide;
197             $redo_tab_complete++
198             }
199              
200             my $ret = $self->SUPER::on_key( @_ );
201              
202             if( $redo_tab_complete ) {
203             if( $ev->type eq "text" or $ev->str eq "Backspace" ) {
204             $self->key_tab_complete;
205             }
206             }
207              
208             if( $ret && $self->{on_typing} and length $self->text ) {
209             my $tickit = $self->window->tickit;
210              
211             if( $self->{typing_timer_id} ) {
212             $tickit->cancel_timer( $self->{typing_timer_id} );
213             }
214             else {
215             $self->started_typing;
216             }
217              
218             $self->{typing_timer_id} = $tickit->timer( after => 5, sub { $self->stopped_typing });
219             }
220              
221             return $ret;
222             }
223              
224             sub started_typing
225             {
226             my $self = shift;
227             $self->{on_typing}->( 1 );
228             }
229              
230             sub stopped_typing
231             {
232             my $self = shift;
233             $self->window->tickit->cancel_timer( $self->{typing_timer_id} ) if defined $self->{typing_timer_id};
234             undef $self->{typing_timer_id};
235             $self->{on_typing}->( 0 );
236             }
237              
238             sub key_tab_complete
239             {
240             my $widget = shift;
241              
242             my $obj = $widget->{obj};
243              
244             my ( $partial ) = substr( $widget->text, 0, $widget->position ) =~ m/(\S*)$/;
245             my $plen = length $partial or return 1;
246              
247             my $at_sol = ( $widget->position - $plen ) == 0;
248              
249             my @matches;
250             my $matchgroup;
251             foreach my $group ( values %{ $obj->prop("completions") } ) {
252             next if $group->prop("only_at_sol") and not $at_sol;
253              
254             my @more = grep { $_ =~ m/^\Q$partial\E/i } @{ $group->prop("items") };
255              
256             push @matches, @more;
257             $matchgroup = $group if @more;
258             }
259              
260             return 1 unless @matches;
261              
262             my $add = $matches[0];
263             foreach my $more ( @matches[1..$#matches] ) {
264             # Find the common prefix
265             my $diffpos = 1;
266             $diffpos++ while lc substr( $add, 0, $diffpos ) eq lc substr( $more, 0, $diffpos );
267              
268             return 1 if $diffpos == 1;
269              
270             $add = substr( $add, 0, $diffpos - 1 );
271             }
272              
273             if( @matches == 1 ) {
274             # No others meaning only one initially
275             $add .= ( $matchgroup->prop("suffix_sol") and $at_sol ) ? $matchgroup->prop("suffix_sol")
276             : " ";
277             }
278              
279             $widget->text_splice( $widget->position - $plen, $plen, $add );
280              
281             if( @matches > 1 ) {
282             # Split matches on next letter
283             my %next;
284             foreach ( @matches ) {
285             my $l = substr( $_, $plen, 1 );
286             push @{ $next{$l} }, $_;
287             }
288              
289             my @possibles = map {
290             @{ $next{$_} } == 1 ? $next{$_}[0]
291             : substr( $next{$_}[0], 0, $plen + 1 )."..."
292             } sort keys %next;
293              
294             # TODO: Wrap these into a flow
295              
296             # TODO: need scrolloffs
297             my $popup = $widget->window->make_popup(
298             -(scalar @possibles), $widget->position - $widget->{scrolloffs_co} - $plen,
299             scalar @possibles, max( map { textwidth($_) } @possibles ),
300             );
301              
302             $popup->pen->chattrs({ bg => 'green', fg => 'black' });
303              
304             $popup->bind_event( expose => sub {
305             my ( $win, undef, $info ) = @_;
306             my $rb = $info->rb;
307              
308             foreach my $line ( 0 .. $#possibles ) {
309             my $str = $possibles[$line];
310              
311             $rb->goto( $line, 0 );
312              
313             $rb->text( substr( $str, 0, $plen + 1 ), PEN_UNDER );
314             $rb->text( substr( $str, $plen + 1 ) ) if length $str > $plen + 1;
315             $rb->erase_to( $win->cols );
316             }
317             } );
318              
319             $popup->show;
320              
321             $widget->{tab_complete_popup} = $popup;
322             }
323              
324             return 1;
325             }
326              
327             sub send_pending
328             {
329             my $self = shift;
330             my ( $count ) = @_;
331              
332             $self->{pending_count} = $count;
333              
334             if( $count ) {
335             my $win = $self->{pending_window} ||= do {
336             my $win = $self->window->make_hidden_sub( 0, $self->window->cols - 12, 1, 12 );
337             my $countr = \$self->{pending_count};
338             $win->bind_event( expose => sub {
339             my ( $win, undef, $info ) = @_;
340             my $rb = $info->rb;
341              
342             $rb->goto( 0, 0 );
343             $rb->text( "Sending $$countr..." );
344             $rb->erase_to( 12 );
345             });
346             $win->pen->chattrs({ fg => "black", bg => "cyan", i => 1 });
347             $win;
348             };
349              
350             if( !$win->is_visible ) {
351             # TODO: Use Tickit->timer when it comes out
352             $win->tickit->loop->watch_time( after => 0.5, code => sub {
353             $win->show if $self->{pending_count} and !$win->is_visible;
354             });
355             }
356              
357             $win->expose if $win->is_visible;
358             }
359             elsif( my $win = $self->{pending_window} ) {
360             $win->hide;
361             }
362             }
363              
364             0x55AA;