File Coverage

blib/lib/Circle/FE/Term/Widget/Entry.pm
Criterion Covered Total %
statement 9 59 15.2
branch 0 14 0.0
condition n/a
subroutine 3 10 30.0
pod n/a
total 12 83 14.4


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