File Coverage

blib/lib/Circle/FE/Term/Widget/Scroller.pm
Criterion Covered Total %
statement 36 142 25.3
branch 0 50 0.0
condition 0 15 0.0
subroutine 12 25 48.0
pod n/a
total 48 232 20.6


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-2017 -- leonerd@leonerd.org.uk
4              
5             package Circle::FE::Term::Widget::Scroller;
6              
7 1     1   1746 use strict;
  1         1  
  1         40  
8 1     1   5 use feature qw( switch );
  1         2  
  1         116  
9 1     1   7 use constant type => "Scroller";
  1         2  
  1         69  
10 1     1   806 no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  1         9  
  1         6  
11              
12 1     1   75 use Circle::FE::Term;
  1         2  
  1         19  
13              
14 1     1   471 use Convert::Color 0.06;
  1         15864  
  1         36  
15 1     1   466 use Convert::Color::XTerm;
  1         1969  
  1         22  
16 1     1   519 use POSIX qw( strftime );
  1         5035  
  1         5  
17 1     1   1581 use String::Tagged;
  1         4522  
  1         29  
18 1     1   696 use Text::Balanced qw( extract_bracketed );
  1         13423  
  1         80  
19 1     1   546 use Tangence::ObjectProxy '0.16'; # watch with iterators
  1         28481  
  1         1512  
20              
21             # Guess that we can do 256 colours on xterm or any -256color terminal
22             my $AS_TERM = ( $ENV{TERM} eq "xterm" or $ENV{TERM} =~ m/-256color$/ ) ? "as_xterm" : "as_vga";
23              
24             sub build
25             {
26 0     0     my $class = shift;
27 0           my ( $obj, $tab ) = @_;
28              
29 0           my $widget = Circle::FE::Term::Widget::Scroller::Widget->new(
30             classes => $obj->prop( "classes" ),
31             gravity => "bottom",
32             );
33              
34 0           my $self = bless {
35             tab => $tab,
36             widget => $widget,
37             last_datestamp => "",
38             last_datestamp_top => "",
39             };
40              
41 0 0   0     $widget->set_on_scrolled( sub { $self->maybe_request_more if $_[1] < 0 } );
  0            
42              
43             $tab->adopt_future(
44             $obj->watch_property_with_iter(
45             "displayevents", "last",
46             on_set => sub {
47 0     0     die "This should not happen\n";
48             },
49             on_push => sub {
50 0     0     $self->insert_event( bottom => $_ ) for @_;
51             },
52             on_shift => sub {
53 0     0     my ( $count ) = @_;
54 0           $count -= $self->{iter_idx};
55 0 0         $widget->shift( $count ) if $count > 0;
56             },
57             )->then( sub {
58 0     0     ( $self->{iter}, undef, my $max ) = @_;
59 0           $self->{iter_idx} = $max + 1;
60              
61 0           $self->maybe_request_more;
62             })
63 0           );
64              
65 0           return $widget;
66             }
67              
68             sub maybe_request_more
69             {
70 0     0     my $self = shift;
71              
72 0           my $widget = $self->{widget};
73 0           my $idx = $self->{iter_idx};
74              
75 0           my $height = $widget->window->lines;
76              
77 0 0         return if $self->{iter_fetching};
78              
79             # Stop if we've got at least 2 screenfuls more, or we're out of things to iterate
80 0 0 0       if( $widget->lines_above > $height * 2 or !$idx ) {
81 0           $widget->set_loading( 0 );
82 0           return;
83             }
84              
85 0           my $more = $height * 3;
86 0 0         $more = $idx if $more > $idx;
87              
88 0           $self->{iter_fetching} = 1;
89 0           $widget->set_loading( 1 );
90              
91             my $f = $self->{iter}->next_backward( $more )
92             ->on_done( sub {
93 0     0     ( $self->{iter_idx}, my @more ) = @_;
94              
95 0           $self->{iter_fetching} = 0;
96              
97 0           $self->insert_event( top => $_ ) for reverse @more;
98 0           $self->maybe_request_more;
99 0           });
100              
101 0           $self->{tab}->adopt_future( $f );
102             }
103              
104             sub insert_event
105             {
106 0     0     my $self = shift;
107 0           my ( $end, $ev ) = @_;
108              
109 0           my ( $event, $time, $args ) = @$ev;
110              
111 0           my $tab = $self->{tab};
112              
113 0           my @time = localtime( $time );
114              
115 0           my $datestamp = strftime( Circle::FE::Term->get_theme_var( "datestamp" ), @time );
116 0           my $timestamp = strftime( Circle::FE::Term->get_theme_var( "timestamp" ), @time );
117              
118 0           my $format = Circle::FE::Term->get_theme_var( $event );
119 0 0         defined $format or $format = "No format defined for event $event";
120              
121 0           my @items = ( $self->format_event( $timestamp . $format, $args ) );
122              
123 0           my $widget = $self->{widget};
124 0           given( $end ) {
125 0           when( "bottom" ) {
126             unshift @items, $self->format_event( Circle::FE::Term->get_theme_var( "datemessage" ), { datestamp => $datestamp } )
127 0 0         if $datestamp ne $self->{last_datestamp};
128              
129 0           $widget->push( @items );
130 0           $self->{last_datestamp} = $datestamp;
131             }
132 0           when( "top" ) {
133             push @items, $self->format_event( Circle::FE::Term->get_theme_var( "datemessage" ), { datestamp => $self->{last_datestamp_top} } )
134 0 0 0       if $datestamp ne $self->{last_datestamp_top} and length $self->{last_datestamp_top};
135              
136 0           $widget->unshift( @items );
137 0           $self->{last_datestamp_top} = $datestamp;
138 0 0         $self->{last_datestamp} = $datestamp if !length $self->{last_datestamp};
139             }
140             }
141             }
142              
143             sub format_event
144             {
145 0     0     my $self = shift;
146 0           my ( $format, $args ) = @_;
147              
148 0           my $str = String::Tagged->new();
149 0           $self->_apply_formatting( $format, $args, $str );
150              
151 0           my $indent = 4;
152 0 0 0       if( grep { $_ eq "indent" } $str->tagnames and
  0            
153             my $extent = $str->get_tag_missing_extent( 0, "indent" ) ) {
154             # TODO: Should use textwidth not just char. count
155 0           $indent = $extent->end;
156             }
157              
158 0           return Tickit::Widget::Scroller::Item::RichText->new( $str, indent => $indent );
159             }
160              
161             my %colourcache;
162             sub _convert_colour
163             {
164 0     0     my $self = shift;
165 0           my ( $colspec ) = @_;
166              
167 0 0         return undef if !defined $colspec;
168              
169             return $colourcache{$colspec} ||= sub {
170 0 0   0     return Convert::Color->new( "rgb8:$1$1$2$2$3$3" )->$AS_TERM->index if $colspec =~ m/^#([0-9A-F])([0-9A-F])([0-9A-F])$/i;
171 0 0         return Convert::Color->new( "rgb8:$1$2$3" )->$AS_TERM->index if $colspec =~ m/^#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i;
172 0 0         return Convert::Color->new( "vga:$colspec" )->index if $colspec =~ m/^[a-z]+$/;
173              
174 0           print STDERR "TODO: Unknown colour spec $colspec\n";
175 0           6; # TODO
176 0   0       }->();
177             }
178              
179             sub _apply_formatting
180             {
181 0     0     my $self = shift;
182 0           my ( $format, $args, $str ) = @_;
183              
184 0           while( length $format ) {
185 0 0         if( $format =~ s/^\$(\w+)// ) {
    0          
186 0 0         my $val = exists $args->{$1} ? $args->{$1} : "";
187 0 0         defined $val or $val = "";
188              
189 0 0         my @parts = ref $val eq "ARRAY" ? @$val : ( $val );
190              
191 0           foreach my $part ( @parts ) {
192 0 0         my ( $text, %format ) = ref $part eq "ARRAY" ? @$part : ( $part );
193              
194             # Tickit::Widget::Scroller::Item::Text doesn't like C0, C1 or DEL
195             # control characters. Replace them with U+FFFD
196 0           $text =~ s/[\x00-\x1f\x80-\x9f\x7f]/\x{fffd}/g;
197              
198 0           foreach (qw( fg bg )) {
199 0 0         defined $format{$_} or next;
200 0           $format{$_} = $self->_convert_colour( Circle::FE::Term->translate_theme_colour( $format{$_} ) );
201             }
202              
203 0           $str->append_tagged( $text, %format );
204             }
205             }
206             elsif( $format =~ m/^\{/ ) {
207 0           my $piece = extract_bracketed( $format, "{}" );
208 0           s/^{//, s/}$// for $piece;
209              
210 0 0         if( $piece =~ m/^\?\$/ ) {
    0          
211             # A conditional expansion in three parts
212             # {?$varname|IFTRUE|IFFALSE}
213 0           my ( $varname, $iftrue, $iffalse ) = split( m/\|/, $piece, 3 );
214 0           $varname =~ s/^\?\$//;
215              
216 0 0         if( defined $args->{$varname} ) {
217 0           $self->_apply_formatting( $iftrue, $args, $str );
218             }
219             else {
220 0           $self->_apply_formatting( $iffalse, $args, $str );
221             }
222             }
223             elsif( $piece =~ m/ / ) {
224 0           my ( $code, $content ) = split( m/ /, $piece, 2 );
225              
226 0           my ( $type, $arg ) = split( m/:/, $code, 2 );
227              
228 0           my $start = length $str->str;
229              
230 0           $self->_apply_formatting( $content, $args, $str );
231              
232 0           my $end = length $str->str;
233              
234 0 0 0       $arg = $self->_convert_colour( $arg ) if $type eq "fg" or $type eq "bg";
235 0           $str->apply_tag( $start, $end - $start, $type => $arg );
236             }
237             else {
238 0           $self->_apply_formatting( $piece, $args, $str );
239             }
240             }
241             else {
242 0           $format =~ s/^([^\$\{]+)//;
243 0           my $val = $1;
244 0           $str->append( $val );
245             }
246             }
247             }
248              
249             package Circle::FE::Term::Widget::Scroller::Widget;
250              
251 1     1   6 use base qw( Tickit::Widget::Scroller );
  1         2  
  1         591  
252             Tickit::Widget::Scroller->VERSION( 0.15 ); # on_scrolled
253             use Tickit::Widget::Scroller::Item::RichText;
254              
255             sub new
256             {
257             my $class = shift;
258             return $class->SUPER::new( @_,
259             gen_bottom_indicator => "gen_bottom_indicator",
260             gen_top_indicator => "gen_top_indicator",
261             );
262             }
263              
264             sub clear_lines
265             {
266             my $self = shift;
267              
268             undef @{ $self->{lines} };
269              
270             my $window = $self->window or return;
271             $window->clear;
272             $window->restore;
273             }
274              
275             sub push
276             {
277             my $self = shift;
278             my $below_before = $self->lines_below;
279             $self->SUPER::push( @_ );
280             if( $below_before ) {
281             $self->{more_count} += $self->lines_below - $below_before;
282             $self->update_indicators;
283             }
284             }
285              
286             sub set_loading
287             {
288             my $self = shift;
289             my ( $loading ) = @_;
290              
291             return if $loading == ( $self->{loading} // 0 );
292              
293             $self->{loading} = $loading;
294             $self->update_indicators;
295             }
296              
297             sub gen_bottom_indicator
298             {
299             my $self = shift;
300             my $below = $self->lines_below;
301             if( !$below ) {
302             undef $self->{more_count};
303             return;
304             }
305              
306             if( $self->{more_count} ) {
307             return sprintf "-- +%d [%d more] --", $below - $self->{more_count}, $self->{more_count};
308             }
309             else {
310             return sprintf "-- +%d --", $below;
311             }
312             }
313              
314             sub gen_top_indicator
315             {
316             my $self = shift;
317             return $self->{loading} ? " Loading... " : undef;
318             }
319              
320             0x55AA;